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