¿Cómo hacer un juego simple en Unity?

Uno puede pensar que comenzar a implementar un juego es un proceso muy complejo y hay que tener conocimientos altos de programación. Lo cierto es que no estarías equivocado, sin embargo voy a demostrar con un ejemplo que también es posible hacer cosas sencillas y con resultados decentes en muy poco tiempo usando Unity.

 

Movimiento: flechas de dirección.

Disparar: tecla espaciadora.

Comenzar de nuevo: tecla escape.

Paso 1 – La escena

Como no queremos empezar la casa por el tejado, lo primero que debemos hacer es copiar al directorio Assets del nuevo proyecto de Unity los modelos que vamos a necesitar: la sala, el jugador, etc. A continuación arrastramos dichos modelos que tenemos en la ventana Project a la ventana Hierarchy para añadirlos a la escena. Creamos también las luces y colocamos en su sitio todos los objetos. Ya tenemos nuestro escenario, así de fácil!

post

Paso 2 – Movimiento del jugador

Mover un objeto en Unity es también sencillo gracias a la función Translate.

transform.Translate(Vector3.forward * trasSpeed * Time.deltaTime);

Con esta instrucción podemos indicar al objeto que tiene asignado el script (en este caso el jugador) que se mueva en la dirección frontal Vector3.forward una cantidad indicada en la variable trasSpeed. Además multiplicamos por Time.deltaTime para transformar de metros/frame a metros/segundo, ya que el tiempo en renderizar un frame puede ser variable y el movimiento no sería uniforme.

Además queremos girar el personaje, y para ello usamos Rotate.

transform.Rotate(Vector3.up, rotSpeed * Time.deltaTime);

Esta función rota alrededor del vector dado como primer parámetro (la vertical en este caso) una cantidad indicada en rotSpeed y una vez más multiplicada por el paso de tiempo.

Al capturar la tecla deseada trasladamos o rotamos al personaje en el sentido deseado. Por ejemplo:

if(Input.GetKey(KeyCode.UpArrow))
{
	transform.Translate(Vector3.forward * trasSpeed * Time.deltaTime);
}

Ya podemos dar agradables paseos por nuestra escena 🙂

Paso 3 – Físicas

Simplemente añadiendo al personaje el componente Rigidbody podemos hacer que se comporte como un objeto físico, y por lo tanto caiga por acción de la gravedad y rebote contra otros objetos. Sin embargo solo detectará un choque con otro objeto si ambos tienen asignado un componente Collider, así que debemos asignar un Collider al personaje, suelo y paredes.

Nuestro personaje es un tanto especial ya que nos interesa que otros objetos reboten cuando se choquen contra él, pero si ocurre lo contrario y el jugador choca contra un objeto se caería al suelo y rodaría como un ladrillo. Para evitar ese comportamiento activamos la opción de isKinematic en el rigidbody del personaje.

Paso 4 – Tratamiento de colisiones

¿Qué pasa cuando el personaje choca contra la pared? Al activar isKinematic ya no le afectan las físicas así que debemos implementar nosotros la reacción que queremos que se produzca en el choque.

Para ello activamos la opción isTrigger en los Colliders de las paredes de forma que lancen una señal cuando se produzca una colisión sobre ellas, y además debemos asignarles un script para implementar la respuesta a dicha señal. La respuesta se especifica en la función definida como OnTriggerEnter(other:Collider).

Si el objeto contra el que choca (dado en la variable other) es kinematic entonces no podemos aplicar físicas sobre él así que simplemente lo trasladaremos con la función Translate que ya vimos antes. Si queremos ir más allá y hacer el rebote más realista podemos suavizar el movimiento utilizando la función Vector3.Lerp(ini,fin,t) que interpola linealmente el movimiento entre la posición ini y la posición fin. Si t=0 la función devuelve ini donde se encuentra en el momento del choque, si t=1 devuelve fin donde queremos que acabe el movimiento alejándose de la pared, si t=0.5 nos devuelve la posición intermedia y así con cualquier valor entre 0 y 1.

En el caso de que el objeto que ha chocado contra la pared no sea kinematic entonces sí podemos aplicar físicas y por tanto usaremos la función AddForce.

other.rigidbody.AddForce(-transform.up*force, ForceMode.Acceleration);

Esta función aplica una aceleración en este caso con la dirección y magnitud indicada en el primer parámetro. También podríamos utilizar ForceMode.Force si nos interesa tener en cuenta la masa del objeto. En mi caso el vector –transform.up apunta hacia dentro de la sala pero eso depende en cada caso según cómo tengamos rotado el objeto.

Además, para que el rebote sea más realista podemos reflejar la velocidad que tenía el objeto no kinematic antes de chocar contra la pared respecto a la normal de la pared.

Vector3.Reflect(other.rigidbody.velocity, -transform.up);
Vector3.Reflect(other.rigidbody.angularVelocity, -transform.up);

Paso 5 – Instanciar

Ahora que nuestro personaje ya se comporta correctamente podemos centrarnos en lo divertido, ¡disparar pelotas!

Para ello debemos primero crear una esfera y asignarle los componentes Collider y Rigidbody no kinematic. Como no queremos tenerla en la escena inicialmente sino que aparezca al disparar, arrastramos la esfera a la ventana Project para convertirla en lo que se llama un objeto Prefab, y a continuación asignamos el Prefab al script del jugador que va a ser quien dispare.

En el script simplemente debemos añadir que cuando se pulse la tecla deseada cree una instancia de esa esfera usando la función Instantiate, y además aplicamos una fuerza hacia delante como vimos con la función AddForce.

Instantiate(pelota, transform.position, Quaternion.identity );

Esta función solo necesita el prefab de la pelota así como posición y rotación donde se va a crear en la escena.

Paso 6 – Destruir

Por último si hoy tenemos un día malo y queremos romper algo bonito, podemos hacer que algunos objetos se destruyan cuando se les dispare.

Solo necesitamos asignar a esos objetos un Collider activando su opción isTrigger, y asignarles un nuevo script con la función OnTriggerEnter(other:Collider) tal y como hicimos anteriormente. La implementación de esa función se basa en comprobar si el objeto contra el que se ha producido la colisión es una pelota, y si es así destruirlo. Para identificar un objeto se utiliza la propiedad other.tag que antes debemos asignar en la ventana Inspector del objeto, y para destruirlo se utiliza Destroy.

Destroy(gameObject);

Para dar más emoción al juego podemos crear efectos especiales importando el paquete de assets llamado Particles. De esta forma podemos instanciar un objeto fuego igual que hicimos con la pelota.

Conclusión

Unity es un motor de videojuegos con una curva de aprendizaje muy suave. Es muy sencillo comenzar a crear un juego básico como el que hemos visto, pero también nos da la posibilidad de crear aplicaciones complejas y con un buen acabado una vez conocemos a fondo la herramienta.

Player

#pragma strict

var trasSpeed:float = 10;
var rotSpeed:float = 2;

var pelota:GameObject;
var disparoSpeed:float = 200;

function Start () {

}

function Update () {

	if(Input.GetKey(KeyCode.UpArrow))
	{
		transform.Translate(Vector3.forward * trasSpeed * Time.deltaTime);
	}
	else if(Input.GetKey(KeyCode.DownArrow))
	{
		transform.Translate(-Vector3.forward * trasSpeed * Time.deltaTime);
	}

	if(Input.GetKey(KeyCode.LeftArrow))
	{
		transform.Rotate(Vector3.up, -rotSpeed * Time.deltaTime);
	}
	else if(Input.GetKey(KeyCode.RightArrow))
	{
		transform.Rotate(Vector3.up, rotSpeed * Time.deltaTime);
	}

	if(Input.GetKey(KeyCode.Space))
	{
		var clon:GameObject = Instantiate(pelota, transform.position + transform.forward - transform.right*0.1, Quaternion.identity );
		clon.rigidbody.AddForce(transform.forward*disparoSpeed, ForceMode.Acceleration);
		Destroy(clon,60);
	}

	if(Input.GetKey(KeyCode.Escape))
	{
		Application.LoadLevel(0);
	}
}

Pared

#pragma strict

var force:float = 100;
var speed:float = 0.5;

private var lerp:boolean=false;
private var ini:Vector3=Vector3.zero;
private var fin:Vector3=Vector3.zero;
private var t:float=0;//0..1
private var obj:Transform;

function OnTriggerEnter(other:Collider)
{
	if(other.rigidbody.isKinematic)
	{
		//other.transform.Translate(-Vector3.up*speed);
		if(!lerp)
		{
			lerp=true;
			t=0;
			obj=other.transform;
			ini=obj.position;
			fin=obj.position - transform.up*speed;
		}
	}
	else
	{
		other.rigidbody.velocity = Vector3.Reflect(other.rigidbody.velocity, -transform.up);
		other.rigidbody.angularVelocity = Vector3.Reflect(other.rigidbody.angularVelocity, -transform.up);
		other.rigidbody.AddForce(-transform.up*force, ForceMode.Acceleration);
	}
}

function Update()
{
	if(lerp)
	{
		obj.transform.position = Vector3.Lerp(ini,fin,t);
		t+=0.1;
		if(t>=1) lerp=false;
	}
}

Objetivo

#pragma strict

var fuego:GameObject;
var fuego2:GameObject;

function OnTriggerEnter(other:Collider)
{
	if(other.tag=="Pelota")
	{
		Destroy(gameObject);

		var clon:GameObject = Instantiate(fuego,transform.position, Quaternion.identity);
		Destroy(clon, 5);

		var clon2:GameObject = Instantiate(fuego2,transform.position, Quaternion.identity);
		Destroy(clon2, 30);
	}
}