Poradnik Endless Runner dla Unity

W grach wideo, niezależnie od tego, jak duży jest świat, zawsze ma on swój koniec. Ale niektóre gry próbują naśladować nieskończony świat, takie gry należą do kategorii o nazwie Endless Runner.

Endless Runner to rodzaj gry, w której gracz nieustannie posuwa się do przodu, zbierając punkty i omijając przeszkody. Głównym celem jest dotarcie do końca poziomu bez wpadnięcia lub zderzenia z przeszkodami, 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 nowoczesne komputery/urządzenia do gier mają ograniczoną moc obliczeniową, niemożliwe jest stworzenie naprawdę nieskończonego świata.

Jak więc niektóre gry tworzą iluzję nieskończonego świata? Odpowiedzią jest ponowne wykorzystanie elementów składowych (tzw. łączenie obiektów), innymi słowy, gdy tylko blok znajdzie się za widokiem kamery lub poza nim, zostaje przesunięty do przodu.

Aby stworzyć grę typu „niekończący się biegacz” w Unity, będziemy musieli stworzyć platformę z przeszkodami i kontrolerem gracza.

Krok 1: Utwórz platformę

Zaczynamy od stworzenia wyłożonej kafelkami platformy, która będzie później przechowywana w Prefabrykatorze:

  • Utwórz nowy obiekt GameObject i wywołaj go "TilePrefab"
  • Utwórz nową kostkę (Object Game -> Obiekt 3D -> Kostka)
  • Przesuń kostkę wewnątrz obiektu "TilePrefab", zmień jej położenie na (0, 0, 0) i przeskaluj na (8, 0.4, 20)

  • Opcjonalnie możesz dodać szyny po bokach, tworząc dodatkowe kostki, jak poniżej:

Jeśli chodzi o przeszkody, będę miał 3 odmiany przeszkód, ale możesz stworzyć ich dowolną liczbę:

  • Utwórz 3 obiekty GameObject wewnątrz obiektu "TilePrefab" i nazwij je "Obstacle1", "Obstacle2" oraz "Obstacle3"
  • W przypadku pierwszej przeszkody utwórz nową kostkę i przesuń ją wewnątrz obiektu "Obstacle1"
  • Przeskaluj nową kostkę do mniej więcej tej samej szerokości co platforma i zmniejsz jej wysokość (gracz będzie musiał skoczyć, aby ominąć tę przeszkodę)
  • Stwórz nowy Materiał, nazwij go "RedMaterial" i zmień jego kolor na Czerwony, a następnie przypisz go do Kostki (w ten sposób odróżnisz przeszkodę od głównej platformy)

  • Dla "Obstacle2" utwórz kilka kostek i ułóż je w trójkąt, pozostawiając jedną otwartą przestrzeń na dole (gracz będzie musiał kucnąć, aby ominąć tę przeszkodę)

  • I na koniec, "Obstacle3" będzie duplikatem "Obstacle1" i "Obstacle2", połączonych razem

  • Teraz wybierz wszystkie obiekty znajdujące się wewnątrz przeszkód i zmień ich znacznik na "Finish", będzie to potrzebne później do wykrycia kolizji pomiędzy Graczem a przeszkodą.

Aby wygenerować nieskończoną platformę, będziemy potrzebować kilku skryptów, które obsłużą łą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 obiekt "Obstacle1", "Obstacle2" i "Obstacle3" do tablicy Przeszkody

Dla punktu początkowego i końcowego musimy utworzyć 2 obiekty GameObject, 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 Prefabrze i usuń go ze Sceny
  • Utwórz nowy obiekt GameObject i wywołaj 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 obiekt 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 wciśnij Play i obserwuj jak porusza się platforma. Gdy tylko płytka platformy zniknie z pola widzenia kamery, zostaje przesunięta z powrotem na koniec wraz z aktywacją losowej przeszkody, tworząc iluzję nieskończonego poziomu (przeskocz do 0:11).

Kamera musi być umieszczona podobnie do wideo, tak aby platformy były skierowane w stronę kamery i za nią, w przeciwnym razie platformy nie będą się powtarzać.

Sharp Coder Odtwarzacz wideo

Krok 2: Utwórz odtwarzacz

Instancja gracza będzie prostą Sferą korzystającą z kontrolera z możliwością skakania i kucania.

  • Utwórz nową Sferę (GameObject -> Obiekt 3D -> Sfera) i usuń jej komponent Sphere Collider
  • Przypisz do niego wcześniej utworzony plik "RedMaterial"
  • Utwórz nowy obiekt GameObject i wywołaj go "Player"
  • Przesuń Kulę wewnątrz obiektu "Player" i zmień jej położenie 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 powyżej obiektu "StartPoint", tuż przed kamerą

Naciśnij Play i użyj klawisza W, aby skoczyć, i klawisza S, aby kucnąć. Celem jest ominięcie czerwonych przeszkód:

Sharp Coder Odtwarzacz wideo

Sprawdź ten Horizon Bending Shader.

Źródło
📁EndlessRunner.unitypackage26.68 KB
Sugerowane artykuły
Tworzenie przesuwanej gry logicznej w Unity
Jak stworzyć grę inspirowaną Flappy Bird w Unity
Minigra w Unity | KOŚĆUnikaj
Jak zrobić grę w węża w Unity
Tworzenie gry 2D Brick Breaker w Unity
Samouczek gry logicznej typu „Dopasuj 3” w Unity
Farmowe zombie | Tworzenie gry platformowej 2D w Unity