Tutorial di Endless Runner per Unity
Nei videogiochi, non importa quanto grande sia il mondo, ha sempre una fine. Ma alcuni giochi cercano di emulare il mondo infinito, tali giochi rientrano nella categoria chiamata Endless Runner.
Endless Runner è un tipo di gioco in cui il giocatore si muove costantemente in avanti mentre raccoglie punti ed evita 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 e i dispositivi di gioco moderni hanno una potenza di elaborazione limitata, è impossibile creare un mondo veramente infinito.
Quindi, come fanno alcuni giochi a creare l'illusione di un mondo infinito? La risposta è riutilizzando i blocchi di costruzione (alias object pooling), in altre parole, non appena il blocco va dietro o fuori dalla visuale della telecamera, viene spostato in primo piano.
Per creare un gioco endless runner in Unity, dovremo creare una piattaforma con ostacoli e un controller per il giocatore.
Fase 1: creare la piattaforma
Iniziamo creando una piattaforma piastrellata che verrà poi memorizzata in 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 delle rotaie ai lati creando dei cubi aggiuntivi, come questo:
Per gli ostacoli, avrò 3 varianti di ostacoli, ma puoi realizzarne quanti ne vuoi:
- 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"
- Ridimensiona il nuovo Cubo all'incirca alla stessa larghezza della piattaforma e riducine l'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 l'"Obstacle2" crea un paio di cubi e posizionali in una forma triangolare, lasciando uno spazio aperto nella parte inferiore (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 cambia il loro tag in "Finish", questo sarà necessario in seguito per rilevare la collisione tra il giocatore e l'ostacolo.
Per generare una piattaforma infinita avremo bisogno di un paio di script che gestiranno l'Object Pooling 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 il punto di partenza e il punto di arrivo, dobbiamo creare 2 GameObjects che devono essere posizionati rispettivamente all'inizio e alla fine della piattaforma:
- Assegna le variabili Punto di inizio e Punto di fine 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 di nuovo alla fine con un ostacolo casuale attivato, 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 siano disposte verso la telecamera e dietro di essa, altrimenti le piattaforme non si ripeteranno.
Passaggio 2: creare il lettore
L'istanza del giocatore sarà una semplice sfera dotata di un controller con la possibilità 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;
}
}
}
- Allega lo script SC_IRPlayer all'oggetto "Player" (noterai che ha aggiunto un altro componente chiamato Rigidbody)
- Aggiungere il componente BoxCollider all'oggetto "Player"
- Posiziona l'oggetto "Player" leggermente sopra l'oggetto "StartPoint", proprio di fronte alla telecamera
Premi Play e usa il tasto W per saltare e il tasto S per accovacciarti. L'obiettivo è evitare gli ostacoli rossi:
Dai un'occhiata a questo Horizon Bending Shader.