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.
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.
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:
Confira este Horizon Bending Shader.