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ą.
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ć.
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:
Sprawdź ten Horizon Bending Shader.