Tutorial do Endless Runner para Unity

Em videogames, não importa quão grande o mundo seja, ele sempre tem um fim. Mas alguns jogos tentam emular o mundo infinito, tais jogos se enquadram na categoria chamada Endless Runner.

Endless Runner é um tipo de jogo onde o jogador está constantemente se movendo para frente enquanto coleta pontos e desvia de obstáculos. O objetivo principal é chegar ao fim do nível sem cair ou colidir com os obstáculos, mas muitas vezes, o nível se repete infinitamente, aumentando gradualmente a dificuldade, até que o jogador colida com o obstáculo.

Jogabilidade do Subway Surfers

Considerando que até mesmo computadores/dispositivos de jogos modernos têm poder de processamento limitado, é impossível criar um mundo verdadeiramente infinito.

Então como alguns jogos criam a ilusão de um mundo infinito? A resposta é reutilizando os blocos de construção (também conhecido como agrupamento de objetos), em outras palavras, assim que o bloco vai para trás ou para fora da visão da Câmera, ele é movido para a frente.

Para fazer um jogo de corrida sem fim em Unity, precisaremos fazer uma plataforma com obstáculos e um controle de jogador.

Etapa 1: Crie a plataforma

Começamos criando uma plataforma em mosaico que mais tarde será armazenada no Prefab:

  • Crie um novo GameObject e chame-o "TilePrefab"
  • Criar novo cubo (GameObject -> Objeto 3D -> Cubo)
  • Mova o cubo para dentro do objeto "TilePrefab", altere sua posição para (0, 0, 0) e dimensione para (8, 0,4, 20)

  • Opcionalmente, você pode adicionar trilhos nas laterais criando cubos adicionais, assim:

Para os obstáculos, terei 3 variações de obstáculos, mas você pode fazer quantos precisar:

  • Crie 3 GameObjects dentro do objeto "TilePrefab" e nomeie-os "Obstacle1", "Obstacle2" e "Obstacle3"
  • Para o primeiro obstáculo, crie um novo cubo e mova-o para dentro do objeto "Obstacle1"
  • Dimensione o novo cubo para aproximadamente a mesma largura da plataforma e diminua sua altura (o jogador precisará pular para evitar esse obstáculo)
  • Crie um novo material, nomeie-o como "RedMaterial" e mude sua cor para vermelho, então atribua-o ao cubo (isso é apenas para que o obstáculo seja diferenciado da plataforma principal)

  • Para o "Obstacle2" crie um par de cubos e coloque-os em forma triangular, deixando um espaço aberto na parte inferior (o jogador precisará se agachar para evitar esse obstáculo)

  • E por último, "Obstacle3" será uma duplicata de "Obstacle1" e "Obstacle2", combinados

  • Agora selecione todos os objetos dentro dos obstáculos e altere suas tags para "Finish", isso será necessário mais tarde para detectar a colisão entre o jogador e o obstáculo.

Para gerar uma plataforma infinita, precisaremos de alguns scripts que lidarão com o Pool de Objetos e a ativação de Obstáculos:

  • Crie um novo script, chame-o de "SC_PlatformTile" e cole o código abaixo dentro dele:

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);
        }
    }
}
  • Crie um novo script, chame-o de "SC_GroundGenerator" e cole o código abaixo dentro dele:

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));
    }
}
  • Anexe o script SC_PlatformTile ao objeto "TilePrefab"
  • Atribuir objeto "Obstacle1", "Obstacle2" e "Obstacle3" ao array Obstacles

Para o Ponto Inicial e Ponto Final, precisamos criar 2 GameObjects que devem ser colocados no início e no fim da plataforma, respectivamente:

  • Atribuir variáveis ​​de ponto inicial e ponto final em SC_PlatformTile

  • Salve o objeto "TilePrefab" no Prefab e remova-o da Cena
  • Crie um novo GameObject e chame-o "_GroundGenerator"
  • Anexe o script SC_GroundGenerator ao objeto "_GroundGenerator"
  • Altere a posição da câmera principal para (10, 1, -9) e altere sua rotação para (0, -55, 0)
  • Crie um novo GameObject, chame-o de "StartPoint" e altere sua posição para (0, -2, -15)
  • Selecione o objeto "_GroundGenerator" e em SC_GroundGenerator atribua as variáveis ​​Câmera Principal, Ponto Inicial e Prefab do Bloco

Agora pressione Play e observe como a plataforma se move. Assim que o ladrilho da plataforma sai da visão da câmera, ele é movido de volta para o final com um obstáculo aleatório sendo ativado, criando uma ilusão de um nível infinito (Pule para 0:11).

A Câmera deve ser posicionada de forma semelhante ao vídeo, de modo que as plataformas fiquem em direção à Câmera e atrás dela, caso contrário as plataformas não se repetirão.

Sharp Coder Reprodutor de vídeo

Etapa 2: Crie o Player

A instância do jogador será uma esfera simples usando um controle com a capacidade de pular e agachar.

  • Crie uma nova esfera (GameObject -> 3D Object -> Sphere) e remova seu componente Sphere Collider
  • Atribuir "RedMaterial" criado anteriormente a ele
  • Crie um novo GameObject e chame-o "Player"
  • Mova a esfera dentro do objeto "Player" e altere sua posição para (0, 0, 0)
  • Crie um novo script, chame-o de "SC_IRPlayer" e cole o código abaixo dentro dele:

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;
        }
    }
}
  • Anexe o script SC_IRPlayer ao objeto "Player" (você notará que ele adicionou outro componente chamado Rigidbody)
  • Adicione o componente BoxCollider ao objeto "Player"

  • Coloque o objeto "Player" ligeiramente acima do objeto "StartPoint", bem na frente da câmera

Pressione Play e use a tecla W para pular e a tecla S para agachar. O objetivo é evitar Obstáculos vermelhos:

Sharp Coder Reprodutor de vídeo

Confira este Horizon Bending Shader.