Endless Runner-Tutorial für Unity

Egal, wie groß die Welt in Videospielen ist, sie hat immer ein Ende. Manche Spiele versuchen jedoch, die unendliche Welt nachzubilden. Solche Spiele fallen in die Kategorie „Endless Runner“.

Endless Runner ist eine Art Spiel, bei dem sich der Spieler ständig vorwärts bewegt, während er Punkte sammelt und Hindernissen ausweicht. Das Hauptziel besteht darin, das Ende des Levels zu erreichen, ohne in die Hindernisse zu fallen oder mit ihnen zu kollidieren. Oft wiederholt sich das Level jedoch unendlich und der Schwierigkeitsgrad wird allmählich erhöht, bis der Spieler mit dem Hindernis kollidiert.

Spielverlauf von Subway Surfers

Wenn man bedenkt, dass selbst moderne Computer/Spielkonsolen nur über eine begrenzte Rechenleistung verfügen, ist es unmöglich, eine wirklich unendliche Welt zu erschaffen.

Wie also erschaffen manche Spiele die Illusion einer unendlichen Welt? Die Antwort ist durch die Wiederverwendung der Bausteine ​​(auch bekannt als Objektpooling). Mit anderen Worten: Sobald der Block hinter oder außerhalb der Kameraansicht liegt, wird er nach vorne verschoben.

Um ein Endlosläuferspiel in Unity zu erstellen, müssen wir eine Plattform mit Hindernissen und einen Spieler-Controller erstellen.

Schritt 1: Erstellen Sie die Plattform

Wir beginnen mit der Erstellung einer gekachelten Plattform, die später im Prefab gespeichert wird:

  • Erstellen Sie ein neues GameObject und nennen Sie es "TilePrefab"
  • Neuen Würfel erstellen (Spielobjekt -> 3D-Objekt -> Würfel)
  • Bewegen Sie den Würfel innerhalb des Objekts "TilePrefab", ändern Sie seine Position auf (0, 0, 0) und skalieren Sie ihn auf (8, 0,4, 20).

  • Optional können Sie an den Seiten Schienen hinzufügen, indem Sie zusätzliche Würfel erstellen, wie folgt:

Für die Hindernisse habe ich 3 Hindernisvarianten, aber Sie können so viele machen, wie Sie brauchen:

  • Erstelle 3 GameObjects innerhalb des "TilePrefab"-Objekts und nenne sie "Obstacle1", "Obstacle2" und "Obstacle3"
  • Für das erste Hindernis erstellen Sie einen neuen Würfel und verschieben ihn in das Objekt "Obstacle1"
  • Skalieren Sie den neuen Würfel auf etwa die gleiche Breite wie die Plattform und verkleinern Sie seine Höhe (der Spieler muss springen, um dieses Hindernis zu vermeiden).
  • Erstellen Sie ein neues Material, nennen Sie es "RedMaterial" und ändern Sie seine Farbe in Rot. Weisen Sie es dann dem Würfel zu (dies dient nur dazu, das Hindernis von der Hauptplattform zu unterscheiden).

  • Für "Obstacle2" erstelle ein paar Würfel und platziere sie in einer dreieckigen Form, wobei du unten einen offenen Raum lässt (der Spieler muss sich ducken, um dieses Hindernis zu vermeiden)

  • Und schließlich wird "Obstacle3" ein Duplikat von "Obstacle1" und "Obstacle2" sein, kombiniert

  • Wählen Sie nun alle Objekte innerhalb der Hindernisse aus und ändern Sie deren Tag in "Finish". Dies wird später benötigt, um die Kollision zwischen Spieler und Hindernis zu erkennen.

Um eine unendliche Plattform zu generieren, benötigen wir einige Skripte, die das Objekt-Pooling und die Hindernisaktivierung handhaben:

  • Erstellen Sie ein neues Skript, nennen Sie es "SC_PlatformTile" und fügen Sie den folgenden Code darin ein:

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);
        }
    }
}
  • Erstellen Sie ein neues Skript, nennen Sie es "SC_GroundGenerator" und fügen Sie den folgenden Code darin ein:

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));
    }
}

Für den Startpunkt und den Endpunkt müssen wir zwei GameObjects erstellen, die jeweils am Anfang und am Ende der Plattform platziert werden sollen:

  • Weisen Sie Startpunkt- und Endpunktvariablen in SC_PlatformTile zu

  • Speichern Sie das Objekt "TilePrefab" im Prefab und entfernen Sie es aus der Szene
  • Erstellen Sie ein neues GameObject und nennen Sie es "_GroundGenerator"
  • Hängen Sie das Skript SC_GroundGenerator an das Objekt "_GroundGenerator" an
  • Ändern Sie die Position der Hauptkamera auf (10, 1, -9) und ändern Sie ihre Drehung auf (0, -55, 0).
  • Erstellen Sie ein neues GameObject, nennen Sie es "StartPoint" und ändern Sie seine Position auf (0, -2, -15).
  • Wählen Sie das Objekt "_GroundGenerator" aus und weisen Sie in SC_GroundGenerator die Variablen Main Camera, Start Point und Tile Prefab zu

Drücken Sie nun auf „Play“ und beobachten Sie, wie sich die Plattform bewegt. Sobald die Plattformkachel aus dem Blickfeld der Kamera verschwindet, wird sie wieder ans Ende zurückbewegt, wobei ein zufälliges Hindernis aktiviert wird, wodurch die Illusion eines unendlichen Levels entsteht (Springen Sie zu 0:11).

Die Kamera muss ähnlich wie im Video platziert werden, sodass die Plattformen auf die Kamera zu und dahinter zeigen, sonst wiederholen sich die Plattformen nicht.

Sharp Coder Videoplayer

Schritt 2: Erstellen Sie den Player

Die Spielerinstanz ist eine einfache Kugel mit einem Controller und der Möglichkeit zum Springen und Ducken.

  • Erstellen Sie eine neue Kugel (GameObject -> 3D-Objekt -> Kugel) und entfernen Sie deren Sphere Collider-Komponente
  • Weisen Sie ihm zuvor erstellte "RedMaterial" zu
  • Erstellen Sie ein neues GameObject und nennen Sie es "Player"
  • Bewegen Sie die Kugel innerhalb des Objekts "Player" und ändern Sie ihre Position auf (0, 0, 0).
  • Erstellen Sie ein neues Skript, nennen Sie es "SC_IRPlayer" und fügen Sie den folgenden Code darin ein:

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;
        }
    }
}
  • Fügen Sie das Skript SC_IRPlayer an das Objekt "Player" an (Sie werden feststellen, dass dadurch eine weitere Komponente namens Rigidbody hinzugefügt wurde).
  • Fügen Sie dem Objekt "Player" die Komponente BoxCollider hinzu

  • Platzieren Sie das Objekt "Player" etwas über dem Objekt "StartPoint", direkt vor der Kamera

Drücken Sie Play und verwenden Sie die W-Taste zum Springen und die S-Taste zum Ducken. Das Ziel besteht darin, roten Hindernissen auszuweichen:

Sharp Coder Videoplayer

Schauen Sie sich diesen Horizon Bending Shader an.