Samouczek Endless Runner dla Unity
W grach wideo, bez względu na to, jak duży jest świat, zawsze ma on koniec. Ale niektóre gry próbują naśladować nieskończony świat, takie gry zaliczają się do kategorii Endless Runner.
Endless Runner to rodzaj gry, w której gracz nieustannie porusza się do przodu, zbierając punkty i unikając przeszkód. Głównym celem jest dotarcie do końca poziomu bez wpadnięcia na przeszkody lub zderzenia się z nimi, ale często poziom powtarza się w nieskończoność, stopniowo zwiększając trudność, aż gracz zderzy się z przeszkodą.
Biorąc pod uwagę, że nawet współczesne komputery/urządzenia do gier mają ograniczoną moc przetwarzania, stworzenie prawdziwie nieskończonego świata jest niemożliwe.
Jak więc niektóre gry tworzą iluzję nieskończonego świata? Odpowiedź brzmi: poprzez ponowne wykorzystanie bloków konstrukcyjnych (czyli łączenie obiektów), innymi słowy, gdy tylko blok znajdzie się za lub poza widokiem kamery, zostanie przeniesiony na przód.
Aby stworzyć grę typu endless runner w Unity, będziemy musieli stworzyć platformę z przeszkodami i kontrolerem gracza.
Krok 1: Utwórz platformę
Zaczynamy od stworzenia kafelkowej platformy, która zostanie później zapisana w Prefab:
- Utwórz nowy obiekt GameObject i nazwij go "TilePrefab"
- Utwórz nowy Cube (GameObject -> 3D Object -> Cube)
- Przesuń kostkę wewnątrz obiektu "TilePrefab", zmień jej pozycję na (0, 0, 0) i skaluj do (8, 0,4, 20)
- Opcjonalnie możesz dodać szyny po bokach, tworząc dodatkowe kostki w następujący sposób:
Jeśli chodzi o przeszkody, przygotuję 3 warianty przeszkód, ale możesz zrobić ich tyle, ile potrzebujesz:
- Utwórz 3 obiekty GameObject wewnątrz obiektu "TilePrefab" i nazwij je "Obstacle1", "Obstacle2" i "Obstacle3"
- W przypadku pierwszej przeszkody utwórz nową kostkę i przenieś ją do obiektu "Obstacle1"
- Skaluj nową kostkę tak, aby miała mniej więcej taką samą szerokość jak platforma i zmniejsz jej wysokość (gracz będzie musiał skakać, aby uniknąć tej przeszkody)
- Utwórz nowy materiał, nazwij go "RedMaterial" i zmień jego kolor na czerwony, a następnie przypisz go do kostki (to tylko po to, aby odróżnić przeszkodę od głównej platformy).
- W przypadku "Obstacle2" stwórz parę kostek i ułóż je w kształt trójkąta, pozostawiając jedną wolną przestrzeń na dole (gracz będzie musiał kucnąć, aby uniknąć tej przeszkody).
- I na koniec, "Obstacle3" będzie duplikatem "Obstacle1" i "Obstacle2" połączonych razem
- Teraz zaznacz wszystkie obiekty znajdujące się w przeszkodach i zmień ich tag na "Finish". Będzie to później potrzebne do wykrycia kolizji pomiędzy graczem a przeszkodą.
Aby wygenerować nieskończoną platformę, będziemy potrzebować kilku skryptów, które będą obsługiwać łączenie obiektów i aktywację przeszkód:
- Utwórz nowy skrypt, nazwij go "SC_PlatformTile" i wklej w nim poniższy kod:
SC_PlatformTile.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SC_PlatformTile : MonoBehaviour
{
public Transform startPoint;
public Transform endPoint;
public GameObject[] obstacles; //Objects that contains different obstacle types which will be randomly activated
public void ActivateRandomObstacle()
{
DeactivateAllObstacles();
System.Random random = new System.Random();
int randomNumber = random.Next(0, obstacles.Length);
obstacles[randomNumber].SetActive(true);
}
public void DeactivateAllObstacles()
{
for (int i = 0; i < obstacles.Length; i++)
{
obstacles[i].SetActive(false);
}
}
}
- Utwórz nowy skrypt, nazwij go "SC_GroundGenerator" i wklej w nim poniższy kod:
SC_GroundGenerator.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class SC_GroundGenerator : MonoBehaviour
{
public Camera mainCamera;
public Transform startPoint; //Point from where ground tiles will start
public SC_PlatformTile tilePrefab;
public float movingSpeed = 12;
public int tilesToPreSpawn = 15; //How many tiles should be pre-spawned
public int tilesWithoutObstacles = 3; //How many tiles at the beginning should not have obstacles, good for warm-up
List<SC_PlatformTile> spawnedTiles = new List<SC_PlatformTile>();
int nextTileToActivate = -1;
[HideInInspector]
public bool gameOver = false;
static bool gameStarted = false;
float score = 0;
public static SC_GroundGenerator instance;
// Start is called before the first frame update
void Start()
{
instance = this;
Vector3 spawnPosition = startPoint.position;
int tilesWithNoObstaclesTmp = tilesWithoutObstacles;
for (int i = 0; i < tilesToPreSpawn; i++)
{
spawnPosition -= tilePrefab.startPoint.localPosition;
SC_PlatformTile spawnedTile = Instantiate(tilePrefab, spawnPosition, Quaternion.identity) as SC_PlatformTile;
if(tilesWithNoObstaclesTmp > 0)
{
spawnedTile.DeactivateAllObstacles();
tilesWithNoObstaclesTmp--;
}
else
{
spawnedTile.ActivateRandomObstacle();
}
spawnPosition = spawnedTile.endPoint.position;
spawnedTile.transform.SetParent(transform);
spawnedTiles.Add(spawnedTile);
}
}
// Update is called once per frame
void Update()
{
// Move the object upward in world space x unit/second.
//Increase speed the higher score we get
if (!gameOver && gameStarted)
{
transform.Translate(-spawnedTiles[0].transform.forward * Time.deltaTime * (movingSpeed + (score/500)), Space.World);
score += Time.deltaTime * movingSpeed;
}
if (mainCamera.WorldToViewportPoint(spawnedTiles[0].endPoint.position).z < 0)
{
//Move the tile to the front if it's behind the Camera
SC_PlatformTile tileTmp = spawnedTiles[0];
spawnedTiles.RemoveAt(0);
tileTmp.transform.position = spawnedTiles[spawnedTiles.Count - 1].endPoint.position - tileTmp.startPoint.localPosition;
tileTmp.ActivateRandomObstacle();
spawnedTiles.Add(tileTmp);
}
if (gameOver || !gameStarted)
{
if (Input.GetKeyDown(KeyCode.Space))
{
if (gameOver)
{
//Restart current scene
Scene scene = SceneManager.GetActiveScene();
SceneManager.LoadScene(scene.name);
}
else
{
//Start the game
gameStarted = true;
}
}
}
}
void OnGUI()
{
if (gameOver)
{
GUI.color = Color.red;
GUI.Label(new Rect(Screen.width / 2 - 100, Screen.height / 2 - 100, 200, 200), "Game Over\nYour score is: " + ((int)score) + "\nPress 'Space' to restart");
}
else
{
if (!gameStarted)
{
GUI.color = Color.red;
GUI.Label(new Rect(Screen.width / 2 - 100, Screen.height / 2 - 100, 200, 200), "Press 'Space' to start");
}
}
GUI.color = Color.green;
GUI.Label(new Rect(5, 5, 200, 25), "Score: " + ((int)score));
}
}
- Dołącz skrypt SC_PlatformTile do obiektu "TilePrefab"
- Przypisz obiekty "Obstacle1", "Obstacle2" i "Obstacle3" do tablicy przeszkód
W punkcie początkowym i punkcie końcowym musimy utworzyć 2 obiekty gry, które należy umieścić odpowiednio na początku i na końcu platformy:
- Przypisz zmienne punktu początkowego i punktu końcowego w SC_PlatformTile
- Zapisz obiekt "TilePrefab" w Prefab i usuń go ze sceny
- Utwórz nowy obiekt GameObject i nazwij go "_GroundGenerator"
- Dołącz skrypt SC_GroundGenerator do obiektu "_GroundGenerator"
- Zmień pozycję kamery głównej na (10, 1, -9) i zmień jej obrót na (0, -55, 0)
- Utwórz nowy GameObject, nazwij go "StartPoint" i zmień jego pozycję na (0, -2, -15)
- Wybierz obiekt "_GroundGenerator" i w SC_GroundGenerator przypisz zmienne Main Camera, Start Point i Tile Prefab
Teraz naciśnij Play i obserwuj, jak porusza się platforma. Gdy tylko kafel platformy zniknie z pola widzenia kamery, wraca na koniec, a losowa przeszkoda zostaje aktywowana, tworząc iluzję nieskończonego poziomu (Przejdź do 0:11).
Kamerę należy umieścić podobnie do wideo, tak aby platformy znajdowały się w kierunku kamery i za nią, w przeciwnym razie platformy się nie powtórzą.
Krok 2: Utwórz odtwarzacz
Instancja gracza będzie prostą Sferą, wykorzystującą kontroler z możliwością skakania i kucania.
- Utwórz nową Sferę (GameObject -> 3D Object -> Sphere) i usuń jej komponent Sphere Collider
- Przypisz do niego wcześniej utworzone "RedMaterial"
- Utwórz nowy obiekt GameObject i nazwij go "Player"
- Przesuń sferę wewnątrz obiektu "Player" i zmień jej pozycję na (0, 0, 0)
- Utwórz nowy skrypt, nazwij go "SC_IRPlayer" i wklej w nim poniższy kod:
SC_IRPlayer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Rigidbody))]
public class SC_IRPlayer : MonoBehaviour
{
public float gravity = 20.0f;
public float jumpHeight = 2.5f;
Rigidbody r;
bool grounded = false;
Vector3 defaultScale;
bool crouch = false;
// Start is called before the first frame update
void Start()
{
r = GetComponent<Rigidbody>();
r.constraints = RigidbodyConstraints.FreezePositionX | RigidbodyConstraints.FreezePositionZ;
r.freezeRotation = true;
r.useGravity = false;
defaultScale = transform.localScale;
}
void Update()
{
// Jump
if (Input.GetKeyDown(KeyCode.W) && grounded)
{
r.velocity = new Vector3(r.velocity.x, CalculateJumpVerticalSpeed(), r.velocity.z);
}
//Crouch
crouch = Input.GetKey(KeyCode.S);
if (crouch)
{
transform.localScale = Vector3.Lerp(transform.localScale, new Vector3(defaultScale.x, defaultScale.y * 0.4f, defaultScale.z), Time.deltaTime * 7);
}
else
{
transform.localScale = Vector3.Lerp(transform.localScale, defaultScale, Time.deltaTime * 7);
}
}
// Update is called once per frame
void FixedUpdate()
{
// We apply gravity manually for more tuning control
r.AddForce(new Vector3(0, -gravity * r.mass, 0));
grounded = false;
}
void OnCollisionStay()
{
grounded = true;
}
float CalculateJumpVerticalSpeed()
{
// From the jump height and gravity we deduce the upwards speed
// for the character to reach at the apex.
return Mathf.Sqrt(2 * jumpHeight * gravity);
}
void OnCollisionEnter(Collision collision)
{
if(collision.gameObject.tag == "Finish")
{
//print("GameOver!");
SC_GroundGenerator.instance.gameOver = true;
}
}
}
- Dołącz skrypt SC_IRPlayer do obiektu "Player" (zauważysz, że dodał on kolejny komponent o nazwie Rigidbody)
- Dodaj komponent BoxCollider do obiektu "Player"
- Umieść obiekt "Player" nieco nad obiektem "StartPoint", tuż przed kamerą.
Naciśnij Play i użyj klawisza W, aby skoczyć i klawisza S, aby kucnąć. Celem jest unikanie czerwonych przeszkód:
Sprawdź ten Horizon Bending Shader.