Tutorial Endless Runner pentru Unity

În jocurile video, oricât de mare este lumea, are întotdeauna un sfârșit. Dar unele jocuri încearcă să emuleze lumea infinită, astfel de jocuri se încadrează în categoria numită Endless Runner.

Endless Runner este un tip de joc în care jucătorul avansează constant în timp ce strânge puncte și evită obstacolele. Obiectivul principal este de a ajunge la sfârșitul nivelului fără a cădea sau a ciocni de obstacole, dar de multe ori, nivelul se repetă la infinit, crescând treptat dificultatea, până când jucătorul se ciocnește de obstacol.

Mod de joc Subway Surfers

Având în vedere că chiar și computerele/dispozitivele de jocuri moderne au o putere de procesare limitată, este imposibil să faci o lume cu adevărat infinită.

Deci, cum unele jocuri creează iluzia unei lumi infinite? Răspunsul este prin reutilizarea blocurilor de construcție (aka pooling de obiecte), cu alte cuvinte, de îndată ce blocul trece în spatele sau în afara vizualizării Camerei, este mutat în față.

Pentru a crea un joc de alergători fără sfârșit în Unity, va trebui să facem o platformă cu obstacole și un controler de jucător.

Pasul 1: Creați platforma

Începem prin a crea o platformă cu gresie care va fi ulterior stocată în Prefab:

  • Creați un nou GameObject și apelați-l "TilePrefab"
  • Creați un cub nou (GameObject -> Obiect 3D -> Cub)
  • Mutați cubul în interiorul obiectului "TilePrefab", schimbați-i poziția la (0, 0, 0) și scalați la (8, 0.4, 20)

  • Opțional, puteți adăuga șine pe laturi creând Cuburi suplimentare, astfel:

Pentru obstacole, voi avea 3 variante de obstacole, dar puteți face câte este nevoie:

  • Creați 3 GameObjects în interiorul obiectului "TilePrefab" și denumiți-le "Obstacle1", "Obstacle2" și "Obstacle3"
  • Pentru primul obstacol, creați un nou Cub și mutați-l în interiorul obiectului "Obstacle1"
  • Scalați noul Cub la aproximativ aceeași lățime ca platforma și reduceți înălțimea acestuia (jucătorul va trebui să sară pentru a evita acest obstacol)
  • Creează un nou Material, numește-l "RedMaterial" și schimbă-i culoarea în Roșu, apoi atribuie-l Cubului (acest lucru este doar pentru ca obstacolul să se distingă de platforma principală)

  • Pentru "Obstacle2" creați câteva cuburi și plasați-le într-o formă triunghiulară, lăsând un spațiu deschis în partea de jos (jucătorul va trebui să se ghemuiască pentru a evita acest obstacol)

  • Și, în sfârșit, "Obstacle3" va fi o copie a "Obstacle1" și "Obstacle2", combinate împreună

  • Acum selectați toate obiectele din interiorul Obstacole și schimbați eticheta lor la "Finish", aceasta va fi necesară mai târziu pentru a detecta coliziunea dintre Jucător și Obstacol.

Pentru a genera o platformă infinită, vom avea nevoie de câteva scripturi care se vor ocupa de gruparea obiectelor și activarea obstacolelor:

  • Creați un nou script, numiți-l "SC_PlatformTile" și inserați codul de mai jos în el:

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ți un nou script, numiți-l "SC_GroundGenerator" și inserați codul de mai jos în el:

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));
    }
}
  • Atașați scriptul SC_PlatformTile la obiectul "TilePrefab"
  • Atribuiți obiectele "Obstacle1", "Obstacle2" și "Obstacle3" matricei Obstacole

Pentru punctul de început și punctul final, trebuie să creăm 2 GameObjects care ar trebui să fie plasate la începutul și respectiv la sfârșitul platformei:

  • Atribuiți variabilele Punctul de început și Punctul final în SC_PlatformTile

  • Salvați obiectul "TilePrefab" în Prefab și eliminați-l din scenă
  • Creați un nou GameObject și apelați-l "_GroundGenerator"
  • Atașați scriptul SC_GroundGenerator la obiectul "_GroundGenerator"
  • Schimbați poziția camerei principale la (10, 1, -9) și schimbați-i rotația la (0, -55, 0)
  • Creați un nou GameObject, numiți-l "StartPoint" și schimbați-i poziția în (0, -2, -15)
  • Selectați obiectul "_GroundGenerator" și în SC_GroundGenerator atribuiți variabilele Camera principală, Punctul de pornire și Tile Prefab

Acum apăsați pe Play și observați cum se mișcă platforma. De îndată ce țiglă platformă iese din vizualizarea camerei, este mutată înapoi la sfârșit, cu un obstacol aleatoriu activat, creând iluzia unui nivel infinit (Săriți la 0:11).

Camera trebuie să fie plasată similar cu videoclipul, astfel încât platformele merg spre Cameră și în spatele acesteia, altfel platformele nu se vor repeta.

Sharp Coder Video player

Pasul 2: Creați playerul

Instanța jucătorului va fi o sferă simplă folosind un controler cu capacitatea de a sări și ghemui.

  • Creați o nouă sferă (GameObject -> 3D Object -> Sphere) și eliminați componenta Sphere Collider
  • Atribuiți-i "RedMaterial" creat anterior
  • Creați un nou GameObject și apelați-l "Player"
  • Mutați sfera în interiorul obiectului "Player" și schimbați-i poziția în (0, 0, 0)
  • Creați un nou script, numiți-l "SC_IRPlayer" și inserați codul de mai jos în el:

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;
        }
    }
}
  • Atașați scriptul SC_IRPlayer la obiectul "Player" (veți observa că a adăugat o altă componentă numită Rigidbody)
  • Adăugați componenta BoxCollider la obiectul "Player"

  • Plasați obiectul "Player" ușor deasupra obiectului "StartPoint", chiar în fața camerei

Apăsați Play și utilizați tasta W pentru a sări și tasta S pentru a vă ghemui. Obiectivul este evitarea obstacolelor roșii:

Sharp Coder Video player

Verificați acest Horizon Bending Shader.