Tutorial de Endless Runner para Unity
En los videojuegos, por muy grande que sea el mundo, siempre tiene un final. Pero algunos juegos intentan emular el mundo infinito, estos juegos entran en la categoría llamada Endless Runner.
Endless Runner es un tipo de juego en el que el jugador avanza constantemente mientras recoge puntos y evita obstáculos. El objetivo principal es llegar al final del nivel sin caerse ni chocar con los obstáculos, pero muchas veces el nivel se repite infinitamente, aumentando gradualmente la dificultad, hasta que el jugador choca con el obstáculo.
Teniendo en cuenta que incluso las computadoras y dispositivos de juego modernos tienen un poder de procesamiento limitado, es imposible crear un mundo verdaderamente infinito.
Entonces, ¿cómo crean algunos juegos la ilusión de un mundo infinito? La respuesta es reutilizando los bloques de construcción (también conocido como agrupación de objetos), en otras palabras, tan pronto como el bloque pasa a estar detrás o fuera de la vista de la cámara, se mueve al frente.
Para hacer un juego de carrera sin fin en Unity, necesitaremos crear una plataforma con obstáculos y un controlador de jugador.
Paso 1: Crear la plataforma
Comenzamos creando una plataforma en mosaico que luego se almacenará en Prefab:
- Crea un nuevo GameObject y llámalo "TilePrefab"
- Crear nuevo cubo (GameObject -> Objeto 3D -> Cubo)
- Mueva el cubo dentro del objeto "TilePrefab", cambie su posición a (0, 0, 0) y escale a (8, 0.4, 20)
- Opcionalmente puedes agregar rieles a los lados creando cubos adicionales, como este:
Para los obstáculos, tendré 3 variaciones de obstáculos, pero puedes hacer tantas como necesites:
- Crea 3 GameObjects dentro del objeto "TilePrefab" y nómbralos "Obstacle1", "Obstacle2" y "Obstacle3"
- Para el primer obstáculo, crea un nuevo cubo y muévelo dentro del objeto "Obstacle1"
- Escala el nuevo cubo hasta que tenga aproximadamente el mismo ancho que la plataforma y reduce su altura (el jugador tendrá que saltar para evitar este obstáculo).
- Crea un nuevo Material, nómbralo "RedMaterial" y cambia su color a Rojo, luego asígnalo al Cubo (esto es solo para que el obstáculo se distinga de la plataforma principal)
- Para el "Obstacle2" crea un par de cubos y colócalos en forma triangular, dejando un espacio abierto en la parte inferior (el jugador deberá agacharse para evitar este obstáculo)
- Y por último, "Obstacle3" será un duplicado de "Obstacle1" y "Obstacle2", combinados.
- Ahora selecciona todos los Objetos dentro de los Obstáculos y cambia su etiqueta a "Finish", esto será necesario más adelante para detectar la colisión entre el Jugador y el Obstáculo.
Para generar una plataforma infinita necesitaremos un par de scripts que manejarán la agrupación de objetos y la activación de obstáculos:
- Crea un nuevo script, llámalo "SC_PlatformTile" y pega el código a continuación dentro de él:
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 un nuevo script, llámalo "SC_GroundGenerator" y pega el código a continuación dentro de él:
Generador de tierra 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));
}
}
- Adjunte el script SC_PlatformTile al objeto "TilePrefab"
- Asignar objetos "Obstacle1", "Obstacle2" y "Obstacle3" a la matriz Obstáculos
Para el punto de inicio y el punto final, necesitamos crear 2 GameObjects que deben colocarse al inicio y al final de la plataforma respectivamente:
- Asignar variables de punto de inicio y punto final en SC_PlatformTile
- Guarde el objeto "TilePrefab" en Prefab y elimínelo de la escena
- Crea un nuevo GameObject y llámalo "_GroundGenerator"
- Adjunte el script SC_GroundGenerator al objeto "_GroundGenerator"
- Cambie la posición de la cámara principal a (10, 1, -9) y cambie su rotación a (0, -55, 0)
- Crea un nuevo GameObject, llámalo "StartPoint" y cambia su posición a (0, -2, -15)
- Seleccione el objeto "_GroundGenerator" y en SC_GroundGenerator asigne las variables Cámara principal, Punto de inicio y Prefab de mosaico
Ahora presiona Play y observa cómo se mueve la plataforma. Tan pronto como la plataforma sale de la vista de la cámara, se mueve hacia el final y se activa un obstáculo aleatorio, lo que crea la ilusión de un nivel infinito (Salta al minuto 0:11).
La cámara debe colocarse de manera similar al video, de modo que las plataformas vayan hacia la cámara y detrás de ella, de lo contrario, las plataformas no se repetirán.
Paso 2: Crea el reproductor
La instancia del jugador será una esfera simple que usará un controlador con la capacidad de saltar y agacharse.
- Crea una nueva esfera (GameObject -> Objeto 3D -> Esfera) y elimina su componente Sphere Collider
- Asignarle el "RedMaterial" creado previamente
- Crea un nuevo GameObject y llámalo "Player"
- Mueva la esfera dentro del objeto "Player" y cambie su posición a (0, 0, 0)
- Crea un nuevo script, llámalo "SC_IRPlayer" y pega el código a continuación dentro de él:
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;
}
}
}
- Adjunte el script SC_IRPlayer al objeto "Player" (notará que agregó otro componente llamado Rigidbody)
- Agregue el componente BoxCollider al objeto "Player"
- Coloque el objeto "Player" ligeramente por encima del objeto "StartPoint", justo delante de la cámara.
Pulsa Play y usa la tecla W para saltar y la tecla S para agacharte. El objetivo es evitar los obstáculos rojos:
Mira este Horizon Bending Shader.