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ą.

Rozgrywka w Subway Surfers

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ą.

Sharp Coder Odtwarzacz wideo

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:

Sharp Coder Odtwarzacz wideo

Sprawdź ten Horizon Bending Shader.