Curso de Unity 3D | Plataformas con RayCast

Curso de Unity 3D

3.  
11.  
14.  
15.  
34.  

Por 9.99€ al mes tendrás acceso completo a todos los cursos. Sin matrícula ni permanencia.

Creamos un nuevo proyecto 2D.

Cuando terminemos…

  • La entidad player tendrá asociadas las clases Player, PlayerInputController2D (cuya propiedad Collision Mask será obstacle) y pertenecerá al layer player
  • La entidad MainCamera tendrá asociada la clase CameraFollow.
  • Las plataformas y el suelo tendrán asociadas el Layer obstacle.
  • Debemos asegurarnos de que la check Edit -> Project Settings -> Physics2D -> Auto Sync Transforms está marcada.

Configurando el proyecto

En Edit -> Project Settings -> Physics 2D …

  • Desmarcamos la check Reuse Collision Callbacks
  • Marcamos la check Auto Sync Transforms
Configuración rayos en Unity

El cubo cae por gravedad

Vincularemos un cubo a la clase Player. Esta clase, implementará a su vez el componente Controller2D, que es el que se ocupa de la mayoría de la lógica.

Player.cs
[RequireComponent (typeof (Controller2D))]
public class Player : MonoBehaviour{
	float gravity = -20;
	Vector3 velocity;
	Controller2D controller;

	void Start(){
		controller = GetComponent<Controller2D>();
	}

	void Update(){
		CalculateVelocity();	
		controller.Move(velocity*Time.deltaTime);
	}
	void CalculateVelocity() {
		velocity.y += gravity * Time.deltaTime;
	}
}
Controller2D.cs
public void Move(Vector3 velocity){
	transform.Translate(velocity);
}

El cubo con los rayos

A partir de la ubicación del cubo, el método VerticalCollisions se encarga de lanzar unos rayos (raycast) verticales hacia abajo. Para ello necesitaremos el punto de origen (raycastOrigin) y la dirección de cada uno de esos rayos.

Estos rayos nos devolverán más adelante cuál es el objeto con el que están colisionando.

No olvides que es necesario estar usando un BoxCollider2D

Controller2D.cs
public class Controller2D : RaycastController{
	public void Move(Vector3 velocity){
		UpdateRaycastOrigins();
		VerticalCollisions();
		transform.Translate(velocity);
	}

	public void VerticalCollisions(){
		for (int i = 0; i < verticalRayCount; i++){
			Vector2 rayOrigin = raycastOrigins.bottomLeft;
			rayOrigin += Vector2.right * verticalRaySpacing * i;
			Debug.DrawRay(rayOrigin, Vector2.up * -2, Color.red);
		}
	}
}
RaycastController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RaycastController : MonoBehaviour{
	const float dstBetweenRays = 0.25f;
	[HideInInspector]
	public int verticalRayCount = 4; // lanzaremos 4 rayos hacia abajo. Esto se podrá cambiar desde el inspector
	[HideInInspector]
	public float verticalRaySpacing = 1;
	public BoxCollider2D collider;
	// raycastOrigins es una estructura que contiene las coordenadas de cada una de las cuatro esquinas del cubo
	public RaycastOrigins raycastOrigins;

	public virtual void Start(){
		collider = GetComponent<BoxCollider2D>();
		CalculateRaySpacing();
	}
    
	public void UpdateRaycastOrigins(){
		Bounds bounds = collider.bounds;
		raycastOrigins.bottomLeft = new Vector2(bounds.min.x, bounds.min.y);
		raycastOrigins.bottomRight = new Vector2(bounds.max.x, bounds.min.y);
		raycastOrigins.topLeft = new Vector2(bounds.min.x, bounds.max.y);
		raycastOrigins.topRight = new Vector2(bounds.max.x, bounds.max.y);
	}

	public void CalculateRaySpacing(){
		Bounds bounds = collider.bounds;
		float boundsWidth = bounds.size.x;
		verticalRayCount = Mathf.RoundToInt(boundsWidth / dstBetweenRays);
		verticalRaySpacing = bounds.size.x / (verticalRayCount - 1);
	}

	public struct RaycastOrigins{
		public Vector2  topLeft, topRight, bottomLeft, bottomRight;
	}
}

Skin Width

Con el objetivo de que cuando el player esté descansando sobre el suelo y por tanto no haya separación con él podamos seguir lanzando un rayo vericalmente hacia abajo, vamos a aplicar una pequeña seaparación de los rayos respecto a los límites del cubo para que estos no salgan desde sus límites, sino desde un poco más adentro.

cubo con rayos en unity 3d


RaycastController.cs

public const float skinWidth = .015f;
...
void UpdateRaycastOrigins(){
	Bounds bounds = collider.bounds;
	bounds.Expand(skinWidth * -2);
	...
void CalculateRaySpacing(){
	Bounds bounds = collider.bounds;
	bounds.Expand(skinWidth * -2);
	...

Detección de colisión

Controller2D.cs
public LayerMask collisionMask;

public void Move(Vector3 velocity){
	UpdateRaycastOrigins();
	VerticalCollisions(ref velocity);
	transform.Translate(velocity);
}

void VerticalCollisions(ref Vector3 velocity){
	float directionY = Mathf.Sign(velocity.y);
	float rayLength = Mathf.Abs(velocity.y) + skinWidth;
	for (int i = 0; i < verticalRayCount; i++){
		...
		RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.up * directionY, rayLength, collisionMask);
		if (hit){
			velocity.y = (hit.distance - skinWidth) * directionY;
			rayLength = hit.distance;
		}
	}
}

A los objetos con los que deseamos colisionar debemos asignarles el layer Obstacle.

Debemos asignar al collisionMask del player que colisione con los objetos de la layer Obstacle.

uso de layers

Movimiento horizontal

Moveremos al player con las teclas a y d. Si usamos las flechas del teclado para mover al player podemos tener conflictos ya que las flechas tienen funciones dentro de Unity.

No te olvides de vincular la nueva clase PlayerInput.cs que vamos a crear al Player)

PlayerInput.cs
[RequireComponent(typeof(Player))]
public class PlayerInput : MonoBehaviour{
	Player player;
	void Start(){
		player = GetComponent<Player>();
	}
	void Update(){
		Vector2 directionalInput = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
		player.SetDirectionalInput(directionalInput);
	}
}
Player.cs
float moveSpeed = 6;
Vector2 directionalInput;

void CalculateVelocity(){
	velocity.x = directionalInput.x * moveSpeed;
	...
}

public void SetDirectionalInput(Vector2 input){
	directionalInput = input;
}

Lanzamos los rayos un poco adelante en la dirección en la que el player se está moviendo

Vamos a lanzar el rayo en la posición en la que estaremos un momento después. Esto nos permitirá anticipar las colisiones en la dirección del movimiento. Primero evaluaremos si habrá colisión y después ejecutaremos el movimiento.

Controller2D.cs
void VerticalCollisions(ref Vector3 velocity){
	...
	rayOrigin += Vector2.right * verticalRaySpacing * i;
	rayOrigin += Vector2.right * (verticalRaySpacing * i + velocity.x);
	...
}

Colisión horizontal

RaycastController.cs
[HideInInspector]
public int horizontalRayCount = 4;
[HideInInspector]
public float horizontalRaySpacing;

...
void CalculateRaySpacing(){
	...
	horizontalRayCount = Mathf.Clamp(horizontalRayCount, 2, int.MaxValue);
	horizontalRaySpacing = bounds.size.y / (horizontalRayCount - 1);
}
Controller2D.cs
public void Move(Vector3 velocity){
	UpdateRaycastOrigins();
	HorizontalCollisions(ref velocity);
	VerticalCollisions(ref velocity);
	transform.Translate(velocity);
}
void HorizontalCollisions(ref Vector3 velocity){
	float directionX = Mathf.Sign(velocity.x);
	float rayLength = Mathf.Abs(velocity.x) + skinWidth;
	for (int i = 0; i < horizontalRayCount; i++){
		Vector2 rayOrigin = (directionX == -1) ? raycastOrigins.bottomLeft : raycastOrigins.bottomRight;
		rayOrigin += Vector2.up * horizontalRaySpacing * i;
		RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.right * directionX, rayLength, collisionMask);
		Debug.DrawRay(rayOrigin , Vector2.right * directionX * rayLength, Color.blue);
		if (hit){
			velocity.x = (hit.distance - skinWidth) * directionX;
			rayLength = hit.distance;
		}
	}
}

Gestionar colisiones sólo cuando el player se mueva

Controller2D.cs
public void Move(Vector3 velocity){
	UpdateRaycastOrigins();
	HorizontalCollisions(ref velocity);
	VerticalCollisions(ref velocity);
	if (velocity.x != 0) {
		HorizontalCollisions(ref velocity);
	}

	if(velocity.y != 0){
		VerticalCollisions(ref velocity);
	}
	transform.Translate(velocity);
}

No acumular gravedad mientras estamos en una plataforma

Antes de ejecutar este paso, veremos que si después de caer sobre la plataforma salimos de la misma, el player bajará muy bruscamente hacia abajo. Para evitarlo, dejaremos de acumular velocidad cuando el Player esté sobre una plataforma.

Después de ejecutar este paso, el player caerá muy lentamente. Para corregir esto, ejecutaremos el paso siguiente.

Controler2D.cs
public CollisionInfo collisions;

void VerticalCollisions(ref Vector3 velocity){
	...
		if (hit){
			velocity.y = (hit.distance - skinWidth) * directionY;
			rayLength = hit.distance;
			collisions.below = directionY == -1; // Si está yendo hacia arriba, y colisiona, below valdrá true
			collisions.above = directionY == 1; // Si está llendo hacia abajo, y colisiona, above valdrá true
		}
	}
}

void HorizontalCollisions(ref Vector3 velocity){
	...
		if (hit){
			velocity.x = (hit.distance - skinWidth) * directionX;
			rayLength = hit.distance;
			collisions.left = directionX == -1; // Si está llendo hacia la izquierda, y colisiona, left valdrá true
			collisions.right = directionX == 1; // Si está llendo hacia la derecha, y colisiona, right valdrá true
		}
	}
}

public struct CollisionInfo{
	public bool above, below;
	public bool left, right;
}
Player.cs
void Update(){
	CalculateVelocity();
	controller.Move(velocity * Time.deltaTime);

	if (controller.collisions.above || controller.collisions.below){
		velocity.y = 0;
	}
}

Resetear collisionInfo

Si no volvemos a poner a false las variables abovebelowleft y right, continuarán indefinidamente valiendo true después de una colisión y por tanto la velocity.y del cubo se mantendrá a 0.

Controller2D.cs
public void Move(Vector3 velocity){
	UpdateRaycastOrigins();
	collisions.Reset();
	...
}

public struct CollisionInfo{
	...

	public void Reset(){
		above = below = false;
		left = right = false;
	}
}

Saltar

Player.cs
float jumpVelocity = 8;
public void OnJumpInputDown(){
	if (controller.collisions.below){
	velocity.y = jumpVelocity;
	}
}
PlayerInput.cs
Update(){
	...
	if (Input.GetKeyDown(KeyCode.Space)){
		player.OnJumpInputDown();
	}
}
}

Transformar jumpHeight y timeToJumpApex en gravity y velocity

jumpHeight y timeToJumpApex serán unidades más útiles para trabajar.

Player.cs
public float jumpHeight = 4;
public float timeToJumpApex = .4f;

void Start(){
	controller = GetComponent<Controller2D>();

	gravity = -(2 * jumpHeight) / Mathf.Pow(timeToJumpApex, 2);
	jumpVelocity = Mathf.Abs(gravity) * timeToJumpApex;
}

Deceleración progresiva en los cambios de dirección

Player.cs
float velocityXSmoothing;
float accelerationTimeAirborne = .2f;
float accelerationTimeGrounded = .1f;


void CalculateVelocity(){
	float targetVelocityX = directionalInput.x * moveSpeed;
	velocity.x = Mathf.SmoothDamp(velocity.x, targetVelocityX, ref velocityXSmoothing, (controller.collisions.below)?accelerationTimeGrounded: accelerationTimeAirborne);
	velocity.x = directionalInput.x * moveSpeed;
	velocity.y += gravity * Time.deltaTime;
}

Subiendo cuestas

Con el código que tenemos hasta ahora, el player subirá las cuestas muy lentamente. Vamos a adaptar nuestro código para que la subida se realice a la misma velocidad que cuando el player avanza horizontalmente.

Controller2D.cs
float maxClimbAngle = 80;

void HorizontalCollisions(ref Vector3 velocity){
	...
		if (hit){
			float slopeAngle = Vector2.Angle(hit.normal, Vector2.up);
			if(i == 0 && slopeAngle <= maxClimbAngle){
				ClimbSlope(ref velocity, slopeAngle);
			}
			...
		}
	}
}

void ClimbSlope(ref Vector3 velocity, float slopeAngle){
	float moveDistance = Mathf.Abs(velocity.x);
	velocity.y = Mathf.Sin(slopeAngle * Mathf.Deg2Rad) * moveDistance;
	velocity.x = Mathf.Cos(slopeAngle * Mathf.Deg2Rad) * moveDistance * Mathf.Sign(velocity.x); 
}

Corregir la imposibilidad de saltar mientras subimos una cuesta

El siguiente código nos permitirá saltar en una cuesta, pero sólo cuando estamos subiendo.

void ClimbSlope(ref Vector3 velocity, float slopeAngle){
	float moveDistance = Mathf.Abs(velocity.x);
	float climbVelocityY = Mathf.Sin(slopeAngle * Mathf.Deg2Rad) * moveDistance;
	if(velocity.y <= climbVelocityY){
		velocity.y = climbVelocityY;
		velocity.x = Mathf.Cos(slopeAngle * Mathf.Deg2Rad) * moveDistance * Mathf.Sign(velocity.x);
		collisions.below = true;
	}
}

Optimizamos el código para que la gestión de la colisión con la rampa sólo se ejecute cuando sea necesario

Ahora mismo, cuando el player choca de frente contra el inicio de una rampa, puede pasar que se quede trabado y no la suba. Esto se solucionará en los dos siguientes pasos (corrección del tembleque al colisionar de frente cuando estamos subiendo una rampa y corrección del tembleque al colisionar por arriba.)

Controller2D.cs
void HorizontalCollisions(ref Vector3 velocity){
	...
	if (hit){
		...
		if(!collisions.climbingSlope || slopeAngle > maxClimbAngle){
			velocity.x = (hit.distance - skinWidth) * directionX;
			rayLength = hit.distance;

			collisions.left = directionX == -1;
			collisions.right = directionX == 1;
		}
	}
}

void ClimbSlope(ref Vector3 velocity, float slopeAngle){
	...
	if(velocity.y <= climbVelocityY){
		...
		collisions.climbingSlope = true;
		collisions.slopeAngle = slopeAngle;
	}
}

public struct CollisionInfo{
	public bool above, below;
	public bool left, right;
	public bool climbingSlope;
	public float slopeAngle, slopeAngleOld;
	public void Reset(){
		above = below = false;
		left = right = false;
		climbingSlope = false;
		slopeAngleOld = slopeAngle;
	}
}

Corrección del tembleque al colisionar de frente cuando estamos subiendo una rampa

Esto ocurre sólo cuando el alto de la pared contra la que colisionamos no abarca toda la altura del player.

colisión del tembleque al colisionar de frente
void HorizontalCollisions(ref Vector3 velocity){
	...
	if (!collisions.climbingSlope || slopeAngle > maxClimbAngle) {
		velocity.x = (hit.distance - skinWidth) * directionX;
		rayLength = hit.distance;

		if (collisions.climbingSlope){
			velocity.y = Mathf.Tan(collisions.slopeAngle * Mathf.Deg2Rad) * Mathf.Abs(velocity.x);
		}
		collisions.left = directionX == -1;
		collisions.right = directionX == 1;
	}
	...
}

Corrección del tembleque al colisionar por arriba

problema al colisionar contra un techo
Controller2D.cs
void VerticalCollisions(ref Vector3 velocity){
	...
	for (int i = 0; i < verticalRayCount; i++){
		Vector2 rayOrigin =  raycastOrigins.bottomLeft;
		Vector2 rayOrigin = (directionY == -1) ? raycastOrigins.bottomLeft : raycastOrigins.topLeft;
		//Si el player está bajando, el rayo saladrá desde su parte inferior, pero si está subiendo, saldrá desde la superior
		...

		if (hit){
			velocity.y = (hit.distance - skinWidth) * directionY;
			rayLength = hit.distance;

			if (collisions.climbingSlope){
				velocity.x = velocity.y / Mathf.Tan(collisions.slopeAngle * Mathf.Deg2Rad) * Mathf.Sign(velocity.x);
			}

			collisions.below = directionY == -1;
			collisions.above = directionY == 1;
		}
	}
}

Bajando cuestas

Cuando la cuesta sea bastante pronunciada, hasta ahora, el cubo estaba bajando dando saltitos. Para corregirlo, utilizaremos el siguiente código.

Controller2D.cs
float maxDescendAngle = 80;

public void Move(Vector3 velocity){
	UpdateRaycastOrigins();
	collisions.Reset();
	if(velocity.y < 0){
		DescendSlope(ref velocity);
	}
	...

void DescendSlope(ref Vector3 velocity){
	float directionX = Mathf.Sign(velocity.x);
	Vector2 rayOrigin = (directionX == -1) ? raycastOrigins.bottomRight : raycastOrigins.bottomLeft;
	RaycastHit2D hit = Physics2D.Raycast(rayOrigin, -Vector2.up, Mathf.Infinity, collisionMask);
	if (hit){
		float slopeAngle = Vector2.Angle(hit.normal, Vector2.up);
		if(slopeAngle != 0 && slopeAngle <= maxDescendAngle){
			if(Mathf.Sign(hit.normal.x) == directionX){
				if(hit.distance - skinWidth <= Mathf.Tan(slopeAngle * Mathf.Deg2Rad) * Mathf.Abs(velocity.x)){
					float moveDistance = Mathf.Abs(velocity.x);
					float descendVelocityY = Mathf.Sin(slopeAngle * Mathf.Deg2Rad) * moveDistance;
					velocity.x = Mathf.Cos(slopeAngle * Mathf.Deg2Rad) * moveDistance * Mathf.Sign(velocity.x);
					velocity.y -= descendVelocityY;
					collisions.slopeAngle = slopeAngle;
					collisions.descendingSlope = true;
					collisions.below = true;
				}
			}
		}
	}
}

public struct CollisionInfo{
	...
	public bool descendingSlope;
	...
	public void Reset(){
		...
		descendingSlope = false;
		slopeAngle = 0;
	}
}

Corrección de la pequeña parada que hace el player al converger dos rampas de subida y bajada

Esto no ocurre en todos los casos, pero sí en algunos.

rampa de subida y bajada

Controller2D.cs
public void Move(Vector3 velocity) {
	UpdateRaycastOrigins();
	collisions.Reset();
	collisions.velocityOld = velocity;
	...
}

void HorizontalCollisions(ref Vector3 velocity){
	...
	if (i == 0 && slopeAngle <= maxClimbAngle) {
		if (collisions.descendingSlope){
			collisions.descendingSlope = false;
			velocity = collisions.velocityOld;
		}
		ClimbSlope(ref velocity, slopeAngle);
	}
	...
}

public struct CollisionInfo{
	public Vector3 velocityOld;
	...
}

Plataformas móviles

  • Cuando la plataforma se mueve en horizontal, el Player se quedará quieto mientras la plataforma avanza. Este será un problema que arreglemos en el siguiente paso.
  • La plataforma se moverá en una única dirección hasta salir por los límites de la pantalla.
PlatformController.cs
public class PlatformController : RaycastController{
	public Vector3 move;

	public override void Start(){
		base.Start();
	}

	void Update(){
		UpdateRaycastOrigins();
		Vector3 velocity = move * Time.deltaTime;
		transform.Translate(velocity);
    }
}

Habrá que darle valor al Vector3 en el inspector.

Mover correctamente al player sobre la plataforma

  • De momento sólo estamos controlando el movimiento del player sobre la plataforma cuando esta se mueve verticalmente.
  • Debes asegurarte de que la plataforma tiene rayos suficientes como para colisionar contra el Player.
  • El valor de la propiedad passengerMask será player. Para que funcione, asignaremos el layer player al objeto Player.
PlatformController.cs
public LayerMask passengerMask;
void Update(){
	...
	MovePassengers(velocity);
}


void MovePassengers(Vector3 velocity){
	HashSet<Transform> movedPassengers = new HashSet<Transform>();
	float directionX = Mathf.Sign(velocity.x);
	float directionY = Mathf.Sign(velocity.y);

	if (velocity.y != 0){
		float rayLength = Mathf.Abs(velocity.y) + skinWidth;
		for (int i = 0; i < verticalRayCount; i++){
			//Si el player está bajando, el rayo saladrá desde su parte inferior, pero si está subiendo, saldrá desde la superior
			Vector2 rayOrigin = (directionY == -1) ? raycastOrigins.bottomLeft : raycastOrigins.topLeft;
			rayOrigin += Vector2.right * (verticalRaySpacing * i);
			RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.up * directionY, rayLength, passengerMask);
			Debug.DrawRay(rayOrigin, Vector2.up * directionY, Color.blue); 

			if (hit){
				if (!movedPassengers.Contains(hit.transform)){
					movedPassengers.Add(hit.transform);
					float pushX = (directionY == 1) ? velocity.x : 0;
					float pushY = velocity.y - (hit.distance - skinWidth) * directionY;
					hit.transform.Translate(new Vector3(pushX, pushY));
				}
			}
		}
	}
}

Gestionar movimiento del player cuando la plataforma se mueve horizontalmente

Cuando implementemos el siguiente código, todavía tendremos errores, ya que la plataforma resbalará por debajo del player. Esto será algo que corregiremos en el siguiente paso.

PlatformController.cs
void MovePassengers(Vector3 velocity){
	...	
	if (velocity.x != 0) {
		float rayLength = Mathf.Abs (velocity.x) + skinWidth;
			
		for (int i = 0; i < horizontalRayCount; i ++) {
			Vector2 rayOrigin = (directionX == -1)?raycastOrigins.bottomLeft:raycastOrigins.bottomRight;
			rayOrigin += Vector2.up * (horizontalRaySpacing * i);
			RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.right * directionX, rayLength, passengerMask);

			if (hit) {
				if (!movedPassengers.Contains(hit.transform)) {
					movedPassengers.Add(hit.transform);
					float pushX = velocity.x - (hit.distance - skinWidth) * directionX;
					float pushY = -skinWidth;
						
					hit.transform.Translate(new Vector3(pushX,pushY));
				}
			}
		}
	}
}

Corregir errores del movimiento horizontal y hacia abajo de la plataforma

PlatformController.cs
void MovePassengers(Vector3 velocity){
	...
	// Si el pasajero está encima de una plataforma que se mueve hacia abajo u horizontalmente
	if (directionY == -1 || velocity.y == 0 && velocity.x != 0){
		float rayLength = skinWidth * 2;
		for (int i = 0; i < verticalRayCount; i++){
			//Si el player está bajando, el rayo saladrá desde su parte inferior, pero si está subiendo, saldrá desde la superior
			Vector2 rayOrigin = raycastOrigins.topLeft + Vector2.right * (verticalRaySpacing * i);
			RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.up, rayLength, passengerMask);
			if (hit){
				if (!movedPassengers.Contains(hit.transform)){
					movedPassengers.Add(hit.transform);
					float pushX = velocity.x;
					float pushY = velocity.y;
					hit.transform.Translate(new Vector3(pushX, pushY));
				}
			}
		}
	}
}


Gestionar la colisión de la plataforma con paredes

Renombramos la función MovePassengers como CalculatePassengerMovement y creamos la función MovePassenger.

PlatformController.cs
List<PassengerMovement> passengerMovement;

void Update(){
	UpdateRaycastOrigins();
	Vector3 velocity = move * Time.deltaTime;
		
	CalculatePassengerMovement(velocity);
	MovePassengers(velocity);
	MovePassengers(true);
	transform.Translate(velocity);
	MovePassengers(false);
}

void MovePassengers(bool beforeMovePlatform){
	foreach(PassengerMovement passenger in passengerMovement){
		if(passenger.moveBeforePlatform == beforeMovePlatform){
			passenger.transform.GetComponent<Controller2D>().Move(passenger.velocity);
		}
	}
}

void CalculatePassengerMovement(Vector3 velocity){
	passengerMovement = new List<PassengerMovement>();
	...
	if(velocity.y != 0){
		...
		if (!movedPassengers.Contains(hit.transform)){
			...
			hit.transform.Translate(new Vector3(pushX, pushY));
			passengerMovement.Add(new PassengerMovement(hit.transform, new Vector3(pushX, pushY), directionY==1, true));
		}
	...
				}
	if (velocity.x != 0){
		...
		if (!movedPassengers.Contains(hit.transform)){
			...
			hit.transform.Translate(new Vector3(pushX, pushY));
			passengerMovement.Add(new PassengerMovement(hit.transform, new Vector3(pushX, pushY), false, true));
		}
		...
	}	
	if(directionY == -1 || velocity.y == 0 && velocity.x != 0){
		...
		if (!movedPassengers.Contains(hit.transform)){
			...
			hit.transform.Translate(new Vector3(pushX, pushY));
			passengerMovement.Add(new PassengerMovement(hit.transform, new Vector3(pushX, pushY), true, false));
		}
		...
	}
}

struct PassengerMovement {
	public Transform transform;
	public Vector3 velocity;
	public bool standingOnPlatform;
	public bool moveBeforePlatform;

	public PassengerMovement(Transform _transform, Vector3 _velocity, bool _standingOnPlatform, bool _moveBeforePlatform) {
		transform = _transform;
		velocity = _velocity;
		standingOnPlatform = _standingOnPlatform;
		moveBeforePlatform = _moveBeforePlatform;
	}
}

Reducir el número de llamadas al método GetComponent

Añadimos a un diccionario el componente Controller2D del player, para en lo sucesivo no tener que llamar al método GetComponent.

PlatformController.cs
Dictionary<Transform, Controller2D> passengerDictionary = new Dictionary<Transform, Controller2D>();
void MovePassengers(bool beforeMovePlatform){
	foreach(PassengerMovement passenger in passengerMovement){
		if (!passengerDictionary.ContainsKey(passenger.transform)){
			passengerDictionary.Add(passenger.transform, passenger.transform.GetComponent<Controller2D>());
		}

		if(passenger.moveBeforePlatform == beforeMovePlatform){
			passenger.transform.GetComponent<Controller2D>().Move(passenger.velocity);
			passengerDictionary[passenger.transform].Move(passenger.velocity);
		}
	}
}

Corregir algunos problemas del player sobre la plataforma

  • A veces el player no puede saltar cuando la plataforma se desplaza verticalmente
  • Cuando la plataforma aterriza sobre el player este se desplaza horizontalmente.
PlatformController.cs
void MovePassengers(bool beforeMovePlatform){
	foreach(PassengerMovement passenger in passengerMovement){
		...
		if (passenger.moveBeforePlatform == beforeMovePlatform){
			passengerDictionary[passenger.transform].Move(passenger.velocity);
			passengerDictionary[passenger.transform].Move(passenger.velocity,passenger.standingOnPlatform);
		}
	}

Actualizamos el método move con el siguiente código:

Controller2D.cs
public void Move(Vector3 velocity, bool standingOnPlatform = false ){
	...
	if( standingOnPlatform){
		collisions.below = true;
	}
}

Solucionar que cuando la plataforma desciende superponiéndose sobre el player mientras este se mueve horizontalmente, el player deja de responder correctamente a los controles horizontales de movimiento

Cuando el cubo se superpone sobre la plataforma, su distancia a la misma será cero. En estos casos, queremos que igualmente, la plataforma se pueda mover correctamente horizontalmente, por eso hacemos un continue, para no tener en cuenta ese rayo.

Si el cubo lanza sus rayos contra una pared, el rayo sí que será tenido en cuenta, ya que su distancia no será cero, por tener un skinwidth de separación.

Controller2D.cs
void HorizontalCollisions(ref Vector3 velocity){
	...
	if (hit){
		if(hit.distance == 0){
			continue;
		}
		...

Definiendo waypoints para las plataformas

Una vez añadido el código, en el panel inspector definiremos los puntos entre los que se moverán las plataformas.

De momento sólo definiremos los puntos. La plataforma no se moverá entre ellos.

PlatformController.cs
public Vector3[] localWaypoints;

void OnDrawGizmos(){
	if(localWaypoints != null){
		Gizmos.color = Color.red;
		float size = .3f;

		for (int i = 0; i<localWaypoints.Length; i++) {
			Vector3 globalWaypointsPos = localWaypoints[i] + transform.position;
			Gizmos.DrawLine(globalWaypointsPos - Vector3.up * size, globalWaypointsPos + Vector3.up * size);
			Gizmos.DrawLine(globalWaypointsPos - Vector3.left * size, globalWaypointsPos + Vector3.left * size);
		}
	}
}

Definimos dos wayPonts; uno con coordenadas (0,0,0) y otro en el lugar al que queremos que se mueva la plataforma.

Evitar que la posición de destino del gizmo cambie

Para probar este punto tendremos que definir valores en el inspector para:

  • Local Waypoints (al menos dos puntos)
  • speed
PlatformController.cs
Vector3[] globalWaypoints;
public float speed;
int fromWaypointIndex;
float percentBetweenWaypoints;

public override void Start(){
	base.Start();
	globalWaypoints = new Vector3[localWaypoints.Length];
	for (int i =0; i < localWaypoints.Length; i++) {
		globalWaypoints[i] = localWaypoints[i] + transform.position;
	}
}

void OnDrawGizmos(){
	...
	for (int i = 0; i<localWaypoints.Length; i++) {
		Vector3 globalWaypointsPos = localWaypoints[i] + transform.position;
		Vector3 globalWaypointsPos = (Application.isPlaying) ? globalWaypoints[i] : localWaypoints[i] + transform.position;
		...


void Update(){
	...
	Vector3 velocity = move * Time.deltaTime;
	Vector3 velocity = CalculatePlatformMovement(); //Si no pones esta línea la plataforma no se moverá y no dará error
	...
}
Vector3 CalculatePlatformMovement(){
	int toWaypointIndex = fromWaypointIndex + 1;
	float distanceBetweenWaypoints = Vector3.Distance(globalWaypoints[fromWaypointIndex], globalWaypoints[toWaypointIndex]);
	percentBetweenWaypoints += Time.deltaTime * speed/distanceBetweenWaypoints;
	Vector3 newPos = Vector3.Lerp(globalWaypoints[fromWaypointIndex], globalWaypoints[toWaypointIndex], percentBetweenWaypoints);

	if(percentBetweenWaypoints >= 1){
		percentBetweenWaypoints = 0;
		fromWaypointIndex++;
		if(fromWaypointIndex >= globalWaypoints.Length - 1){
			fromWaypointIndex = 0;
			System.Array.Reverse(globalWaypoints);
		}
	}
	return newPos - transform.position;
}

La plataforma se mueve cíclicamente

Vamos a poder seleccionar si la plataforma volverá a su posición inicial recorriendo el camino inverso o llendo directamente desde el punto final al inicial.

Fíjate que tendremos que asignar una speed a la plataforma.

PlatformController.cs
public bool cyclic;
Vector3 CalculatePlatformMovement(){
	int toWaypointIndex = fromWaypointIndex + 1;
	fromWaypointIndex %= globalWaypoints.Length;
	int toWaypointIndex = (fromWaypointIndex + 1) % globalWaypoints.Length;
	...
	if(percentBetweenWaypoints >= 1){
		percentBetweenWaypoints = 0;
		fromWaypointIndex++;
		if(!cyclic){
			if(fromWaypointIndex >= globalWaypoints.Length - 1){
				fromWaypointIndex = 0;
				System.Array.Reverse(globalWaypoints);
			}
		}
	}

waitTime

waitTime será el tiempo que la plataforma se detiene en una posición concreta.

PlatformController.cs
public float waitTime;
float nextMoveTime;

Vector3 CalculatePlatformMovement(){
	if(Time.time < nextMoveTime){
		return Vector3.zero;
	}
	...
	if(percentBetweenWaypoints >= 1){
		...
		nextMoveTime = Time.time + waitTime;
	}
}

Aceleración del movimiento

PlatformController.cs
[Range(0,2)]
public float easeAmount;

float Ease(float x){
	float a = easeAmount +1;
	return Mathf.Pow(x,a) / (Mathf.Pow(x,a) + Mathf.Pow(1-x, a));
}

Vector3 CalculatePlatformMovement(){
	fromWaypointIndex %= globalWaypoints.Length;
	int toWaypointIndex = (fromWaypointIndex + 1) % globalWaypoints.Length;
	float distanceBetweenWaypoints = Vector3.Distance(globalWaypoints[fromWaypointIndex], globalWaypoints[toWaypointIndex]);
	percentBetweenWaypoints += Time.deltaTime * speed / distanceBetweenWaypoints;
	Vector3 newPos = Vector3.Lerp(globalWaypoints[fromWaypointIndex], globalWaypoints[toWaypointIndex], percentBetweenWaypoints);

	percentBetweenWaypoints = Mathf.Clamp01(percentBetweenWaypoints);
	float easedPercentBetweenWaypoints = Ease(percentBetweenWaypoints);  
	Vector3 newPos = Vector3.Lerp(globalWaypoints[fromWaypointIndex], globalWaypoints[toWaypointIndex], easedPercentBetweenWaypoints);

Wall jumping

Caer derrapando contra una pared vertical

Player.cs
bool wallSliding;
public float wallSlideSpeedMax = 3;

void Update(){
	...
	HandleWallSliding();
}

void HandleWallSliding(){
	wallSliding = false;
	if ((controller.collisions.left || controller.collisions.right) && !controller.collisions.below && velocity.y < 0){
		wallSliding = true;
		if (velocity.y < -wallSlideSpeedMax){
			velocity.y = -wallSlideSpeedMax;
		}
	}
}

Solucionar que las colisiones sólo son detectadas si nos movemos contra la pared

Controller2D.cs
public override void Start(){
	base.Start();
	collisions.faceDir = 1;
}

public void Move(Vector3 velocity, bool standingOnPlatform = false ){
	...
	if (velocity.x != 0){
		HorizontalCollisions(ref velocity);
		collisions.faceDir = (int)Mathf.Sign(velocity.x);
	}
	...	
	HorizontalCollisions(ref velocity);
	
public struct CollisionInfo{
	public float faceDir;
	...					
}

void HorizontalCollisions(ref Vector3 velocity){
	float directionX = Mathf.Sign(velocity.x);
	float directionX = collisions.faceDir;
	float rayLength = Mathf.Abs(velocity.x) + skinWidth;
	if (Mathf.Abs(velocity.x) < skinWidth){
		rayLength = 2 * skinWidth;
	}
	...
}

Llegados a este punto es indispensable que no te hayas olvidado de dar este paso:

En Edit -> Project Settings -> Physics 2D …

  • Desmarcamos la check Reuse Collision Callbacks
  • Marcamos la check Auto Sync Transforms
Configuración rayos en Unity

Añadimos la posibilidad de saltar mientras nos apoyamos contra la pared

Tendremos que dar valores a las variables de salto en el inspector:

  • Wall Jump Off(fuerza de salto cuando estamos apoyados en una pared y no tocamos las teclas de dirección): X: 7.5, Y:16
  • Wall Jump Climb (fuerza de salto cuando estamos apoyados en una pared y tocamos la tecla para ir en la dirección de la pared): X: 8.5, Y: 7
  • Wall Leap (fuerza de salto cuando estamos apoyados en una pared y tocamos la tecla para ir en la dirección contraria de la pared): X:18, Y: 17
Player.cs
int wallDirX;

float maxJumpVelocity = 20;
float minJumpVelocity = 10;

public Vector2 wallJumpOff;
public Vector2 wallJumpClimb;
public Vector2 wallLeap;

public void OnJumpInputDown(){
	if (controller.collisions.below){
		velocity.y = jumpVelocity;
	}
}

public void OnJumpInputDown(){
	if (wallSliding){
		if (wallDirX == directionalInput.x){
			velocity.x = -wallDirX * wallJumpClimb.x;
			velocity.y = wallJumpClimb.y;
		}else if (directionalInput.x == 0){
			velocity.x = -wallDirX * wallJumpOff.x;
			velocity.y = wallJumpOff.y;
		}else{
			velocity.x = -wallDirX * wallLeap.x;
			velocity.y = wallLeap.y;
		}
	}
	if (controller.collisions.below){
		velocity.y = maxJumpVelocity;
	}
}

void HandleWallSliding(){
	wallDirX = (controller.collisions.left) ? -1 : 1;
	...
}

Salto a varias alturas

Player.cs
public float jumpHeight;
public float maxJumpHeight = 4;
public float minJumpHeight = 1;
float jumpVelocity;

void Start(){
	...
	gravity = -(2 * jumpHeight) / Mathf.Pow(timeToJumpApex, 2);
	jumpVelocity = Mathf.Abs(gravity) * timeToJumpApex;
	gravity = -(2 * jumpHeight) / Mathf.Pow (timeToJumpApex, 2);
	gravity = -(2 * maxJumpHeight) / Mathf.Pow(timeToJumpApex, 2);

	maxJumpVelocity = Mathf.Abs(gravity) * timeToJumpApex;
	minJumpVelocity = Mathf.Sqrt(2 * Mathf.Abs(gravity) * minJumpHeight);
}

public void OnJumpInputUp() {
	if (velocity.y > minJumpVelocity) {
		velocity.y = minJumpVelocity;
	}
}
PlayerInput.cs
void Update(){
	...
	if (Input.GetKeyUp (KeyCode.Space)) {
		player.OnJumpInputUp ();
	}
}
}

Saltar a una plataforma desde abajo

Creamos una plataforma con el tag Through y el layer Obstacle.

Controller2D.cs
void VerticalCollisions(ref Vector3 velocity){
	if (hit){
		if (hit.collider.tag == "Through"){
			if (directionY == 1){
				continue;
			}
		}
	...

Corregir el problema de que si no llegamos a la parte superior de la plataforma, igualmente subimos sobre ella

Controller2D.cs
if (directionY == 1 || hit.distance == 0)

Caer de la plataforma al pulsar abajo

Será necesario que la plataforma esté etiquetada como «Through».

Controller2D.cs
public Vector2 playerInput;

public void Move (Vector3 velocity, bool standingOnPlatform){
	Move(velocity, Vector2.zero, standingOnPlatform);
}

public void Move(Vector3 velocity, Vector2 input, bool standingOnPlatform = false){
	UpdateRaycastOrigins();
	collisions.Reset();
	collisions.velocityOld = velocity;
	playerInput = input;
	...
}

void VerticalCollisions(ref Vector3 velocity){aaaa
	...
	if (hit){
		if (hit.collider.tag == "Through"){
			if (directionY == 1 || hit.distance == 0){
				continue;
			}
			if (playerInput.y == -1){
				continue;
			}
Player.cs
void Update(){
	...
	//controller.Move(velocity * Time.deltaTime);
	controller.Move(velocity * Time.deltaTime, directionalInput);
	...
}

La cámara

La focus área sigue al player

El siguiente script estará vinculado a la cámara del juego. A su propiedad target le daremos le asignaremos al player.

Cuando probemos nuestro código, deberemos definir un tamaño para la focus area.

public class CameraFollow : MonoBehaviour{
	public Controller2D target;
	public Vector2 focusAreaSize;

	FocusArea focusArea;

	void Start(){
		focusArea = new FocusArea(target.collider.bounds, focusAreaSize);
	}

	void LateUpdate(){
		focusArea.Update(target.collider.bounds);
	}

	void OnDrawGizmos(){
		Gizmos.color = new Color(1, 0, 0, .5f);
		Gizmos.DrawCube(focusArea.centre, focusAreaSize);
	}
    
	struct FocusArea{
		public Vector2 centre;
		public Vector2 velocity;

		float left, right;
		float top, bottom;

		public FocusArea(Bounds targetBounds, Vector2 size){
			left = targetBounds.center.x - size.x / 2;
			right = targetBounds.center.x + size.x / 2;
			bottom = targetBounds.min.y;
			top = targetBounds.min.y + size.y;

			velocity = Vector2.zero;
			centre = new Vector2((left + right) / 2, (top + bottom) /2); 
		}

		public void Update(Bounds targetBounds){
			float shiftX = 0;
			if(targetBounds.min.x < left){
				shiftX = targetBounds.min.x - left;
			}else if (targetBounds.max.x > right){
				shiftX = targetBounds.max.x - right;
			}
			left += shiftX;
			right += shiftX;

			float shiftY = 0;
			if(targetBounds.min.y < bottom){
				shiftY = targetBounds.min.y - bottom;
			}else if (targetBounds.max.y > top){
				shiftY = targetBounds.max.y - top;
			}
			top += shiftY;
			bottom += shiftY;
			centre = new Vector2((left + right) / 2, (top + bottom) / 2);
			velocity = new Vector2(shiftX, shiftY);
		}
	}
}

Mejorar el efecto de la cámara

Vamos a hacer que el player no siempre esté en el centro de la cámara.

RaycastController.cs
public virtual void Awake(){
	collider = GetComponent<BoxCollider2D>();
}

public virtual void Start(){
	collider = GetComponent<BoxCollider2D>();
	CalculateRaySpacing();
}

Algunas mejoras

CameraFollow.cs
public float verticalOffset;

void LateUpdate(){
	...
	Vector2 focusPosition = focusArea.centre + Vector2.up;
	transform.position = (Vector3)focusPosition + Vector3.forward * -10;
}

Mover la cámara a ambos lados del player

CameraFollow.cs
float currentLookAheadX;
float targetLookAheadX;
float lookAheadDstX;
float lookAheadDirX;
float lookSmothTimeX;
float smoothLookVelocityX;

void LateUpdate(){
	focusArea.Update(target.collider.bounds);
	Vector2 focusPosition = focusArea.centre + Vector2.up;

	if(focusArea.velocity.x != 0){
		lookAheadDirX = Mathf.Sign(focusArea.velocity.x);
	}
	targetLookAheadX = lookAheadDirX * lookAheadDstX;
	currentLookAheadX = Mathf.SmoothDamp(currentLookAheadX, targetLookAheadX, ref smoothLookVelocityX, lookSmothTimeX);
	focusPosition += Vector2.right * currentLookAheadX;

	transform.position = (Vector3)focusPosition + Vector3.forward * -10;
}

Mejorar el posicionamiento de la focus area

Realmente, sólo queremos cambiar la posición de la focus area si el jugador mira y camina en cierta dirección, no sólo si camina en esa dirección.

CameraFollow.cs
public float lookSmoothTimeX;
bool lookAheadStopped;
float smoothVelocityY;
public float verticalSmoothTime;

void LateUpdate(){
	focusArea.Update (target.collider.bounds);
	Vector2 focusPosition = focusArea.centre + Vector2.up * verticalOffset;
	if(focusArea.velocity.x != 0){ 
		lookAheadDirX = Mathf.Sign(focusArea.velocity.x);
		if(Mathf.Sign(target.playerInput.x) == Mathf.Sign(focusArea.velocity.x) && target.playerInput.x !=0){
			lookAheadStopped = false;
			targetLookAheadX = lookAheadDirX * lookAheadDstX;
		}else{
			if (!lookAheadStopped){
				lookAheadStopped = true;
				targetLookAheadX = currentLookAheadX + (lookAheadDirX * lookAheadDstX - currentLookAheadX) / 4f;
			}
		}
	}
	currentLookAheadX = Mathf.SmoothDamp(currentLookAheadX, targetLookAheadX, ref smoothLookVelocityX, lookSmoothTimeX);

	focusPosition.y = Mathf.SmoothDamp(transform.position.y, focusPosition.y, ref smoothVelocityY, verticalSmoothTime);   
	focusPosition += Vector2.right * currentLookAheadX;
}

Mover realmente la cámara

void LateUpdate(){
	transform.position = (Vector3)focusPosition + Vector3.forward * -10;
}

Gestionar las animaciones del personaje

Vamos a sustituir el cubo por las correspondientes animaciones. Para ello, definiremos el método ApplyAnimations dentro de una nueva clase Anims que se encargará de activar los correspondientes parámetros del animator.

Llamaremos al método ApplyAnimations al final del método Update de la clase player.

public class PlayerAnims : MonoBehaviour{
	Player player;
	SpriteRenderer spr;
	bool flipped = false;
	void Start(){
		player = GetComponent();
		spr = GetComponent();
	}

	void Update(){
		FlipSprite();
	}

	void FlipSprite(){
		if(flipped && player.velocity.x > 0) {
			flipped = false;
			spr.flipX = false;
		}else if(!flipped && player.velocity.x < 0){
			flipped = true;
			spr.flipX = true;
		}
	}
}

Por 9.99€ al mes tendrás acceso completo a todos los cursos. Sin matrícula ni permanencia.