Tutorial Endless Runner per Unity
Nei videogiochi, non importa quanto sia grande il mondo, ha sempre una fine. Ma alcuni giochi cercano di emulare il mondo infinito e rientrano nella categoria chiamata Endless Runner.
Endless Runner è un tipo di gioco in cui il giocatore avanza costantemente raccogliendo punti ed evitando ostacoli. L'obiettivo principale è raggiungere la fine del livello senza cadere o scontrarsi con gli ostacoli, ma spesso il livello si ripete all'infinito, aumentando gradualmente la difficoltà, finché il giocatore non si scontra con l'ostacolo.
Considerando che anche i computer/dispositivi di gioco moderni hanno una potenza di elaborazione limitata, è impossibile creare un mondo veramente infinito.
Allora come fanno alcuni giochi a creare l'illusione di un mondo infinito? La risposta è riutilizzando i blocchi costitutivi (ovvero il pooling di oggetti), in altre parole, non appena il blocco va dietro o fuori dalla vista della telecamera, viene spostato in primo piano.
Per realizzare un gioco Endless Runner in Unity, dovremo creare una piattaforma con ostacoli e un controller per il giocatore.
Passaggio 1: creare la piattaforma
Iniziamo creando una piattaforma piastrellata che verrà successivamente archiviata nel Prefab:
- Crea un nuovo GameObject e chiamalo "TilePrefab"
- Crea un nuovo cubo (GameObject -> Oggetto 3D -> Cubo)
- Sposta il cubo all'interno dell'oggetto "TilePrefab", cambia la sua posizione in (0, 0, 0) e ridimensionalo in (8, 0.4, 20)
- Facoltativamente puoi aggiungere binari ai lati creando cubi aggiuntivi, come questo:
Per gli ostacoli, avrò 3 varianti di ostacolo, ma puoi realizzarne quante ne desideri:
- Crea 3 GameObjects all'interno dell'oggetto "TilePrefab" e chiamali "Obstacle1", "Obstacle2" e "Obstacle3"
- Per il primo ostacolo, crea un nuovo Cubo e spostalo all'interno dell'oggetto "Obstacle1"
- Scala il nuovo cubo fino a raggiungere la stessa larghezza della piattaforma e riduci la sua altezza (il giocatore dovrà saltare per evitare questo ostacolo)
- Crea un nuovo Materiale, chiamalo "RedMaterial" e cambia il suo colore in Rosso, quindi assegnalo al Cubo (questo serve solo per distinguere l'ostacolo dalla piattaforma principale)
- Per "Obstacle2" crea una coppia di cubi e posizionali a forma triangolare, lasciando uno spazio aperto sul fondo (il giocatore dovrà accovacciarsi per evitare questo ostacolo)
- E infine, "Obstacle3" sarà un duplicato di "Obstacle1" e "Obstacle2", combinati insieme
- Ora seleziona tutti gli oggetti all'interno degli ostacoli e modifica il loro tag in "Finish", questo sarà necessario in seguito per rilevare la collisione tra giocatore e ostacolo.
Per generare una piattaforma infinita avremo bisogno di un paio di script che gestiranno il pooling degli oggetti e l'attivazione degli ostacoli:
- Crea un nuovo script, chiamalo "SC_PlatformTile" e incolla al suo interno il codice seguente:
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);
}
}
}
- Crea un nuovo script, chiamalo "SC_GroundGenerator" e incolla al suo interno il codice seguente:
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));
}
}
- Allega lo script SC_PlatformTile all'oggetto "TilePrefab"
- Assegna gli oggetti "Obstacle1", "Obstacle2" e "Obstacle3" all'array Ostacoli
Per Start Point e End Point, dobbiamo creare 2 GameObjects che dovrebbero essere posizionati rispettivamente all'inizio e alla fine della piattaforma:
- Assegnare le variabili Punto iniziale e Punto finale in SC_PlatformTile
- Salva l'oggetto "TilePrefab" in Prefab e rimuovilo dalla scena
- Crea un nuovo GameObject e chiamalo "_GroundGenerator"
- Allega lo script SC_GroundGenerator all'oggetto "_GroundGenerator"
- Cambia la posizione della telecamera principale in (10, 1, -9) e cambia la sua rotazione in (0, -55, 0)
- Crea un nuovo GameObject, chiamalo "StartPoint" e cambia la sua posizione in (0, -2, -15)
- Seleziona l'oggetto "_GroundGenerator" e in SC_GroundGenerator assegna le variabili Main Camera, Start Point e Tile Prefab
Ora premi Play e osserva come si muove la piattaforma. Non appena la tessera della piattaforma esce dalla visuale della telecamera, viene spostata indietro all'estremità con l'attivazione di un ostacolo casuale, creando l'illusione di un livello infinito (Vai a 0:11).
La telecamera deve essere posizionata in modo simile al video, in modo che le piattaforme vadano verso la telecamera e dietro di essa, altrimenti le piattaforme non si ripeteranno.
Passaggio 2: crea il giocatore
L'istanza del giocatore sarà una semplice sfera che utilizza un controller con la capacità di saltare e accovacciarsi.
- Crea una nuova Sfera (GameObject -> Oggetto 3D -> Sfera) e rimuovi il suo componente Sphere Collider
- Assegnagli "RedMaterial" creato in precedenza
- Crea un nuovo GameObject e chiamalo "Player"
- Sposta la sfera all'interno dell'oggetto "Player" e cambia la sua posizione in (0, 0, 0)
- Crea un nuovo script, chiamalo "SC_IRPlayer" e incolla al suo interno il codice seguente:
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;
}
}
}
- Collega lo script SC_IRPlayer all'oggetto "Player" (noterai che ha aggiunto un altro componente chiamato Rigidbody)
- Aggiungi il componente BoxCollider all'oggetto "Player"
- Posiziona l'oggetto "Player" leggermente sopra l'oggetto "StartPoint", proprio di fronte alla fotocamera
Premi Gioca e usa il tasto W per saltare e il tasto S per abbassarti. L'obiettivo è evitare gli ostacoli rossi:
Controlla questo Horizon Bending Shader.