Tutorial Endless Runner para Unity

Nos videogames, por maior que seja o mundo, 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 avança constantemente enquanto acumula pontos e evita obstáculos. O objetivo principal é chegar ao final do nível sem cair ou colidir com os obstáculos, mas muitas vezes o nível se repete infinitamente, aumentando gradativamente a dificuldade, até que o jogador colida com o obstáculo.

Jogabilidade dos surfistas de metrô

Considerando que mesmo os 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 é reutilizar os blocos de construção (também conhecidos como pooling de objetos), em outras palavras, assim que o bloco fica atrás ou fora da visualizaçã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 controlador de jogador.

Etapa 1: Crie a plataforma

Começamos criando uma plataforma lado a lado que será posteriormente armazenada no Prefab:

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

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

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

  • Crie 3 GameObjects dentro do objeto "TilePrefab" e nomeie-os como "Obstacle1", "Obstacle2" e "Obstacle3"
  • Para o primeiro obstáculo, crie um novo Cubo e mova-o dentro do objeto "Obstacle1"
  • Dimensione o novo cubo para aproximadamente a mesma largura da plataforma e reduza sua altura (o jogador precisará pular para evitar esse obstáculo)
  • Crie um novo Material, nomeie-o "RedMaterial" e mude sua cor para Vermelho, depois atribua-o ao Cubo (isso é apenas para distinguir o obstáculo da plataforma principal)

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

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

  • Agora selecione todos os Objetos dentro dos Obstáculos e mude sua tag para "Finish", isso será necessário posteriormente 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"
  • Atribua os objetos "Obstacle1", "Obstacle2" e "Obstacle3" à matriz Obstacles

Para o Start Point e End Point, precisamos criar 2 GameObjects que devem ser colocados no início e no final 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 mude sua posição para (0, -2, -15)
  • Selecione o objeto "_GroundGenerator" e em SC_GroundGenerator atribua as variáveis ​​Main Camera, Start Point e Tile Prefab

Agora pressione Play e observe como a plataforma se move. Assim que o bloco 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 a ilusão de um nível infinito (Pular para 0:11).

A Câmera deve ser posicionada de forma semelhante ao vídeo, para 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 controlador com capacidade de pular e agachar.

  • Crie uma nova esfera (GameObject -> 3D Object -> Sphere) e remova seu componente Sphere Collider
  • Atribua "RedMaterial" criado anteriormente a ele
  • Crie um novo GameObject e chame-o "Player"
  • Mova a esfera dentro do objeto "Player" e mude 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

Verifique este Horizon Bending Shader.

Artigos sugeridos
Tutorial para o jogo de quebra-cabeça Match-3 no Unity
Como fazer um jogo inspirado no Flappy Bird no Unity
Como fazer um jogo de cobra no Unity
Criando um jogo 2D Brick Breaker no Unity
Criando um jogo de quebra-cabeça deslizante no Unity
Minijogo no Unity | CUBEavoid
Fazenda Zumbis | Criação de jogo de plataforma 2D em Unity