Videojuego de Plataformas con Unity3D

Curso de Unity 3D

20.  
23.  
25.  
39.  

Ponemos la imagen de fondo

Ponemos al prota en pantalla

Para poner el jugador en pantalla podemos usar el spritesheet> suministrado para este juego o descargar otros paquetes de internet, como los characters de Pixel Adventure (también están en la asset store):

  • Debe tener la animación de respirar.
  • Debe caer por gravedad
player juego plataformas

Tilemap Editor

Crear un nuevo tilemap

  1. Será necesario importar el paquete 2D Tilemap Editor.
  2. Botón derecho sobre el panel de jerarquía → 2D Object → Tilemap → Rectangle
  3. Menú Window -> 2D -> Tile Palette
  4. New Palette → Seleccionamos una carpeta en nuestro proyecto en la que se almacenarán los Tiles → Se creará una nueva paleta vacía.
  5. Seleccionamos los tiles independientes → Los arrastramos al panel de Tilemap → Se añadirán los tiles a nuestro Tilemap.

Insertar tiles en la escena

  1. Debemos tener seleccionado el GameObject del TileMap (Grid). Habrá que ajustar el grid size del grid para que se corresponda con el de los tiles que estamos añadiendo en pantalla. Por ejemplo, si el tamaño de los tiles que estamos insertando es de 32x32px, el grid size será X:0.32 e Y:0.32.
unity3d cell size

2. El punto origen del tile debería ser su centro, así que nos aseguramos de que tenemos los valores para el anchor con 0.5.

unity 3d tile anchor

3. Deberemos tener seleccionada la herramienta de pintar.

pintar tiles en unity

4. Si quiero desplazar tiles dentro del tile palette, debo:

  1. Tener la opción de edit seleccionada
  2. Seleccionar el tile con la herramienta de selección.
  3. Desplazar el tile con la herramienta de desplazar
desplazar tiles en unity 3d

Insertar tiles colisionables

  1. Seleccionamos el grid y le añadimos un nuevo tilemap que contendrá los tiles que sean colisionables.
  2. A este nuevo tilemap le añadimos el componente Tilemap Collider 2D.
  3. A la hora de añadir tiles colisionables a la escena, debemos fijarnos de que el Tilemap Activo sea el correspondiente (En nuestro caso Colisionable).
Videojuego de Plataformas con Unity3D 1

Recuerda que para que la colisión funcione, el player tenga un Box Collider 2D.

Utilizar el CompositeCollider para evitar que el personaje colisione con tiles individuales

  1. Añadimos el componente Composite Collider 2D al Tilemap Colisionables que está dentro del Grid.
  2. En el componente Tilemap Collider 2D marcamos la check Used By Composite.
  3. El Componente CompositeCollider precisa que el GameObject tenga un RigidBody. El Body Type de este RigidBody será static.

Para que el juegador no se quede enganchado a la plataforma intente saltar sobre ella, no llegue y la empuje horizontalmente, tendremos que asignarle al Tilemap Collider 2D un material sin fricción. Para ello, iremos a Panel de proyecto → Create → 2D → Physics Material 2D.

Filter mode

Por defecto, Unity suaviza los pixeles de las imágenes para evitar mostrar una imagen demasiado pixelada. Cuando estamos haciendo un videojuego de estilo retro, pixel art, etc. debemos deseleccionar esta opción (dejarla en Filter Mode: Point(no filter) ), para que se pueda apreciar correctamente la gráfica.

Desplazamiento horizontal

Para gestionar el movimiento del personaje podemos:

  • Usar fuerzas (como en este ejemplo, usando el RigidBody).
  • No usar fuerzas (utilizando transform.position para gestionar el desplazamiento). Para este caso es una solución peor, ya que cuando el jugador colisione contra una pared, lo que va a ocurrir es que se va a introducir dentro ligeramente. Es como si fuese un balón de playa que se introduce dentro del agua y luego es expulsado. Esto no ocurre cuando usamos fuerzas.
  • El protagonista tendrá, además, el componente Rigidbody2D.
  • Utilizaremos un objeto de tipo enum para los estados del jugador.
enum Controls {
    idle,
    jump,
    right,
    left
}
  • Gestionaremos el movimiento del personaje recogiendo la entrada del teclado en el Update.
void Move(){
    Vector2 movement = rb.velocity;
    if (controls == Controls.left){
      movement.x = -speed;
    }else if (controls == Controls.right){
      movement.x = speed;
    }else{
      movement.x = 0;
    }
    rb.velocity = movement;
}

Salto

La función Move quedaría así:

void Move(){
  ...
  if (controls == Controls.jump && isOnGround){
    movement.y =  jumpForce * Vector2.up.y;
  }
}

En Rigid Body -> Constraints pondremos restricciones para la rotación para que el jugador no pueda caer de cabeza.

private void OnCollisionStay2D(Collision2D collision){
        string tag = collision.gameObject.tag;
        if (tag.Contains("Ground") isOnGround = true;
}

private void OnCollisionExit2D(Collision2D collision){
        string tag = collision.gameObject.tag;
        if (tag.Contains("Ground"))isOnGround = false;
}

Añadir animaciones

En este caso, gestionaremos la activación de las distintas animaciones utilizando parámetros desde el panel animator.

Como podemos fluir desde cualquier animación a cualquier animación. Utilizaremos el estado Any State:

Videojuego de Plataformas con Unity3D 2

Para que las transiciones entre una animación y otra se produzcan de manera instantánea cuando el usuario pulse las teclas de movimiento, deben seleccionar dichas transiciones en el panel animator y cambiar algunos de sus parámetros en el panel inspector:

Videojuego de Plataformas con Unity3D 3

En función de las teclas que pulse el jugador, cambiaremos los parámetros de las correspondientes animaciones.

Como puedes ver, el parámetro de animación Grounded estará en función de que el Player esté tocando el suelo (grounded) y que no estemos saltando por los aires (rb.velocity.y). Hemos redondeado la velocidad con dos decimales (System.Math.Round(rb.velocity.y, 2)) porque Unity tiene un bug, que cuando usas un TilemapCollider con un CompositeCollider, cuando el personaje se está moviendo sólo horizontalmente, asigna un valor para la velocidad vertical cercano a cero, pero que no es exáctamente cero.

void UpdateAnimation(){
  if (isIdleAnim && (controls == Controls.left || controls == Controls.right)){
    isIdleAnim = false;
    animator.SetBool("Idle", false);
  }else if (!isIdleAnim && (controls != Controls.left && controls != Controls.right)){
    isIdleAnim = true;
    animator.SetBool("Idle", true);
  }

  if (!isGroundedAnim && grounded && System.Math.Round(rb.velocity.y, 2) == 0){
    isGroundedAnim = true;
    animator.SetBool("Grounded", true);
  }else if(isGroundedAnim && !grounded){
    isGroundedAnim = false;
    animator.SetBool("Grounded", false);
  }
}

Llamar al método animator.setBool() es algo costoso a nivel de rendimiento. Por eso hemos elaborado esta función para llamar a este método sólo cuando sea necesario. De esta forma…

  • Si estaba cargada la animación de estar en reposo (isIdleAnim) y nos ponemos en movimiento (controls == Controls.left || controls == Controls.right) desactivaremos el parámetro de estar en reposo (animator.SetBool(“Idle”, false))

De forma análoga, procederemos para activar desactivar todos los parámetros de animación: sólo cuando sea necesario.

Hacer que el prota encare la dirección correcta

spr.flipX = true;

Programación de enemigo

Añadir un enemigo que está quieto

Enemigo que camina

Enemy.cs
void Update () {
	transform.Translate(direction * speed * Vector2.right * Time.deltaTime, 0);
}

El enemigo cambia de dirección

float rayMargin = 0.5f;

void Start(){
    spr = GetComponent<SpriteRenderer>();
    boxCol = GetComponent<BoxCollider2D>(); 
}

void Update(){
    ...
    Vector2 rayOrigin = new Vector2(transform.position.x + direction * boxCol.size.x / 2 + direction * rayMargin, transform.position.y + rayMargin - boxCol.size.y / 2);
        RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.down, 5f);
        Debug.DrawRay(rayOrigin, Vector2.down, Color.red);
        if (hit.collider == null){
            direction = direction * -1;
            spr.flipX = direction == -1;
        }
}
rayo enemigo

Colisión contra el enemigo

El siguiente código hace que cuando impactemos contra un enemigo salgamos despedidos en dirección contraria.

Player.cs
void OnCollisionEnter2D(Collision2D col){
    ...
    if (tag == "Enemy"){
        isActive = false;
        float direccionEmpuje = Mathf.Sign(gameObject.transform.position.x - collision.gameObject.transform.position.x);
        rb.velocity = new Vector2(direccionEmpuje * 4f, 2f);
        StartCoroutine(ReActivate());
    }
}

Disparar balas

Cuando el jugador pulse la tecla espacio debe instanciarse una bala en la posición del jugador.

Marcaremos la check is Trigger del box collider para que las balas no empujen al player que las está disparando.

La bala se mueve en la dirección correcta

Utilizaremos un código similar a este para el protagonista:

Player.cs
Vector3 offset = new Vector2(0.3f, 0.1f);
Quaternion rotation = spr.flipX ? Quaternion.Euler(new Vector3(0, 180, 0)) : Quaternion.Euler(new Vector3(0, 0, 0));
Instantiate(bulletPrefab, (spr.flipX ? -1:1 ) *offset + transform.localPosition, rotation);

Siempre que podamos, usaremos localPosition en lugar de position ya que localPosition es más óptimo a nivel de rendimiento. Mientras que position tiene que desandar todo el camino de la jerarquía para llegar a la posición del objeto, localPosition toma la posición del objeto.

Bullet.cs
void Start(){
  rb = GetComponent<Rigidbody2D>();
  goRight = transform.rotation.y == 1 ? false : true;
}

void Update(){
  rb.velocity = goRight ?  Vector2.right*speed : Vector2.left*speed;
}

Plataforma móvil I

Utilizaremos un empty con dos hijos: la plataforma que voy a mover y el punto de destino. A la plataforma que queremos mover le asociaremos el siguiente script. La propiead target será el destino del movimiento, que irá cambiando según lo alcancemos.

Dado que la plataforma se está moviendo, para optimizar el proceso de gestión de reposicionamieno del BoxCollider, le asignaremos un RigidBody2D. A este RigidBody2D le asignaremos un body de tipo Kinematic.

Videojuego de Plataformas con Unity3D 4
MobilePlatform.cs
public class MobilePlatform : MonoBehaviour{
	public Transform target;
	public Transform platform;

	public float speed;
	private Vector3 start, end;

	void Start(){
		if (target != null){
			start = transform.position;
			end = target.position;
		}
	}

	void FixedUpdate(){
		if (target != null){
			float fixedSpeed = speed * Time.deltaTime;
			platform.position = Vector3.MoveTowards(platform.position, target.position, fixedSpeed);
		}
		if (platform.position == target.position){
			target.position = (target.position == start) ? end : start; 
		}
	}
}

Si queremos ver dibujada en pantalla la línea de la trayectoria, podemos vincular al objeto, además, la siguiente clase

DrawSceneLine.cs
public class DrawSceneLine : MonoBehaviour {

	public Transform from; //origen del desplazamiento
	public Transform to;//final del desplazamiento

	void OnDrawGizmosSelected(){

		if (from != null && to != null){
			Gizmos.color=Color.cyan;
			Gizmos.DrawLine(from.position,to.position);
			Gizmos.DrawSphere(from.position, 0.15f);
			Gizmos.DrawSphere(to.position, 0.15f);
		}
	}
}

Plataforma móvil II – sin colisión cuando entramos desde abajo

  1. En el Box Collider de la plataforma, marcamos la check Used By Efector
  2. Añadimos a la plataforma el componente Platform Effector 2D. Definimos un surface arc de menos de 180º. Este ángulo determina que cuando la colisión se produzca con un ángulo de incidencia menor del especificado, no habrá colisión. Se intenta que el ángulo de incidencia sea menor, por la mínima cantidad que la esquina de la plataforma. Yo he escogido 160º.
Videojuego de Plataformas con Unity3D 5

Plataforma móvil III – Evitar que la plataforma “escupa” al prota cuando se está moviendo hacia abajo

Cuando el player entre en la plataforma desde abajo, no habrá colisión. Cuando entre desde arriba, haremos al player hijo de la plataforma.

AttachPlayerToPlatform.cs
void OnCollisionEnter2D(Collision2D col){
	if (col.transform.tag == "Player"){
		if (col.transform.position.y > transform.position.y){
			col.transform.parent = transform;
		}
	}
}

void OnCollisionExit2D(Collision2D col) {
	if(col.gameObject.tag == "Player"){
		col.transform.parent = null;
	}
}

Descender de una plataforma

El método OnCollisionStay deja de ejecutarse al cabo de un tiempo de estar produciéndose. Es decir, después de un rato de estar sin movernos sobre una plataforma, el método OnCollisionStay dejará de lanzarse y por tanto no se detectará si hemos pulsado la tecla de ir hacia abajo. Para evitar esto, el RigidBody de las plataformas atravesables debe tener un Body Type Kinematic y su propiedad Sleeping Mode debe valer Never Sleep. Por tanto haremos un Tilemap específico para las plataformas atravesables.

using UnityEngine.Tilemaps;

private void OnCollisionStay2D(Collision2D col){
    string tag = col.gameObject.tag;

    if (tag.Contains("Ground")){
    	isOnGround = true;
      	if (controls == Controls.down && tag.Contains("Traversable")){
        	TilemapCollider2D tm = col.gameObject.GetComponent<TilemapCollider2D>();
        	tm.enabled = false;
        	StartCoroutine(EnablePlatform(tm));
      	}
    }
}

Barra de vida

  1. Añadimos al player un Canvas.
  2. Creamos un sprite cuadrado en el panel de proyecto ( botón derecho sobre el panel de proyecto → Create → 2D → Sprites → Square ).
  3. Vinculamos el Square al Image creado.
  4. Añadimos una Image al Canvas creado. Su Image Type debe ser Filled. El Fill Method debe ser Horizontal. El Fill Origin debe ser Left.
  5. Vinculamos el siguiente código al GameObject Image. health es una variable que representa la vida del jugador y que va de 0 a 1.
private void OnTriggerEnter(Collider other){
	if (other.gameObject.tag == "Bullet"){
		Destroy(other.gameObject);
		health--;
		healthImage.fillAmount = health/10f;
	}
}

Subir o bajar una escalera

Suponinendo que el botón de saltar también sirviese para subir escaleras, el código quedaría así:

void Update(){
    ...
    if (Input.GetKeyDown(KeyCode.UpArrow)) controls = Controls.jump;
    else if (isOnLadder && Input.GetKey(KeyCode.UpArrow)) controls = Controls.climbingLadder;
    ...
}

void Move(){
    if(isOnLadder && (controls == Controls.climbingLadder || controls == Controls.down))isClimbing = true;

    if (isClimbing){
        rb.gravityScale = 0;

        if (controls == Controls.climbingLadder){
            movement.y = ladderSpeed * Vector2.up.y;
            animator.SetBool("Climbing", true);

        } else if (controls == Controls.down){
            movement.y = -ladderSpeed * Vector2.up.y;
            animator.SetBool("Climbing", true);
        }else{
            movement.y = 0;
            animator.SetBool("Climbing", true);
        }
    }else{
        rb.gravityScale = 1;
        animator.SetBool("Climbing", false);
    }

...
private void OnTriggerEnter2D(Collider2D collision){
    if (collision.gameObject.tag == "Ladder")isOnLadder = true;
}

private void OnTriggerExit2D(Collider2D collision){
	if (collision.gameObject.tag == "Ladder"){
		isOnLadder = false;
		isClimbing = false;
		if(controls == Controls.climbingLadder) rb.AddForce(new Vector2(0, 50));
	}
}

Deslizar por paredes

← Exportar / importar Assets
Videojuego de Plataformas Vertical →

Aviso Legal | Política de privacidad