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.

Modalità di gioco di Subway Surfers

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.

Sharp Coder Lettore video

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:

Sharp Coder Lettore video

Dai un'occhiata a questo Horizon Bending Shader.