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.
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));
}
}
- Hängen Sie das Skript SC_PlatformTile an das Objekt "TilePrefab" an
- Weisen Sie dem Array „Hindernisse“ die Objekte "Obstacle1", "Obstacle2" und "Obstacle3" zu.
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.
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:
Schauen Sie sich diesen Horizon Bending Shader an.