DETECCIÓN DE COLISIONES

Como con todas las partes de Unity hay muchas formas de detectar colisiones, lo aclaro para que no se queden únicamente con la forma que muestro acá como la forma definitiva de hacerlo, ya que cada forma de detección cambia acorde a las necesidades que tengamos. Por ejemplo, la física de un bullet hell no es la misma que la física que requeriría un juego de autos, o un space shooter simple, ya que cada juego necesita un nivel de precisión diferente. Igualmente considero que la finalidad debería ser siempre la misma: Consumir la menor cantidad de recursos sin la necesidad de hacer que el código sea ilegible. Esto es más que nada porque a veces los programadores tendemos (y me incluyo), a hacer código completamente ilegible para lograr una performance más alta, pero muchas podemos sacrificar un poco de esa performance con el fin de hacer el código más legible y escalable.

El chequeo de colisión mayormente es un tema de debate al momento de realizarlo, ya que podemos basarlo en física realista, física dedicada o unos simples cálculos matemáticos. Por ejemplo, si voy a hacer un juego basado en física debería incluir varios factores como el rozamiento, gravedad, fuerzas de impacto, colisiones por área, y un sin fin de cálculos que en muchos casos Unity los resuelve con su propio sistema de físicas (basado en el PhysX de nVidia); o podemos también usar algo menos preciso pero de mucho menor consumo para algún juego de naves simple, como un cálculo por distancia, o sea, asumir que hay una colisión si dos objetos están a menos distancia de X. Los juegos con gran cantidad de disparos en pantalla como Touhou suelen usar cálculos que no necesitan tanto detalle como este último, y juegos como Angry Birds suele necesitar algo bastante más complejo. Es importante decidir cuál se ajusta más a nuestras necesidades.

Por raro que les pueda parecer, el sistema de físicas realista de Unity es el más fácil de usar. De hecho la gente que está aprendiendo suele empezar con esto por el simple hecho de que, como comenté arriba, la gran mayoría de los cálculos ya están contemplados y con unos pocos pasos podemos tener un juego con detección de colisiones y demás. El problema es sería que quizás por hacerla fácil a veces tengamos que hacer ajustes, y hacer ajustes, para hacer que el sistema deje de hacer ciertas cosas, no siempre está bueno. Pero como dije, es más fácil, y la gran mayoría empieza así. Pero también quería hacer la aclaración para, de nuevo, que no se queden con una sola idea, sino que siempre busquen maneras diferentes. En la mayoría de los posts voy a poner las maneras que sean más fáciles, o que mejor se ajusten a una solución, o que yo sepa usar, pero siempre tengan en cuenta que hay miles y ver cómo lo resuelve otra persona siempre enriquece el conocimiento, más allá de si terminan o no usando esa solución.

Empecemos entonces!!

Conceptualmente, para que Unity detecte una colisión se tienen que dar ciertas condiciones:

 

CONDICIONES PARA QUE UNITY DETECTE UNA COLISIÓN:

Los dos objetos incluidos en una colisión DEBEN tener un componente “Collider” (BoxCollider, CircleCollider, etc), y AL MENOS UNO de los dos debe, también, tener un componente “Rigidbody“.

 

Pero… ¿Qué son estos componentes? Vamos a ver sus características principales:

  • Collider: Cualquier componente como BoxCollider, CircleCollider, es un collider. Y se encargan básicamente de decirle al motor de física que el objeto que lo tiene puede producir una colisión de algún tipo. Por ejemplo, BoxCollider sirve para detectar colisiones con forma de caja. Las diferentes formas sirven para optimizar la tarea en base a la necesidad. Una colisión con una forma más exacta suele consumir más recursos que una menos exacta. Un “CircleCollider” consumirá menos que un “BoxCollider“, ya que sólo detecta colisión dentro de un radio.
  • Rigidbody: Este componente es el que se encarga de decirle a Unity que un objeto físico puede moverse. Si tenemos algo como una pared estática, no sería necesario. Pero para que Unity detecte que un objeto se mueve, debe tener este componente. Es importante tener en cuenta que trasladar un objeto usando transform.position no es lo mismo que moverlo. Esto es un concepto que más que nada van a ir notando ustedes, pero algo clave es que el transform,position le indica a Unity dónde se pinta, y, al cambiarlo, lo estamos “teletransportando”, no moviéndo. La diferencia radica en que si lo “teletransportamos” a una posición que es una pared, Unity lo va a hacer y, en tal caso, el sistema de física lo EXPULSARÁ hacia afuera (literalmente lo expulsa).

Entonces, a modo de una breve optimización, lo primero que deberían pensar es: “¿qué objetos deberían tener un Rigidbody?”. Por empezar, podríamos ponerle un collider a las bullet, otro a las naves enemigas, y dejar el Rigidbody dentro de las bullets. O sea:

  • Bullet: Collider y Rigidbody.
  • Enemy: Collider.

Para esto seleccionen el prefab de las bullets, y, en la ventana “Inspector” hagan click en “Add Component” y elijan el collider que más se ajuste. Tengan en cuenta que, al ser un juego en 2D, el collider que van a tener que agregar debería ser “2D”. O sea, en lugar de “BoxCollider” debería ser “BoxCollider2D“. Elijan el collider con la forma que más se adapte a la necesidad, más que al objeto. O sea… ¿La bala NECESITA un collider con forma de cápsula, o podría tener uno con forma de caja? Respondan esta pregunta y agreguen el collider que mejor se adapte.

 

 

Una vez seleccionado el collider, vamos a ponerle un Rigidbody.

 

 

Y así nos quedarían nuestros componentes físicos para la bullet.

 

 

Ahora es el turno de ponerle las cosas necesarias a nuestro héroe. Nuestro héroe va a necesitar un Collider y un Rigidbody. De nuevo, podemos sentarnos a pensar si realmente lo necesita o no, que sería lo ideal. Pero para este ejemplo vamos a ponerselo y a medida que vayamos aprendiendo nos vamos preocupando más por las optimizaciones. Entonces, nuestro personaje quedaría de la siguiente forma:

 

 

Noten que le puse 2 BoxCollider2D. Esto es algo completamente opcional y ya depende de lo TOC que cada uno sea. Pueden ponerle un BoxCollider2D, o pueden ponerle 2 como en este caso para hacer una caja de colisión para el cuerpo de la nave y para las alas por separado, o también pueden usar otro tipo de collider que se adapte mejor. Elegí, en este caso, ponerle 2 BoxCollider2D por un intermedio entre una colisión precisa y consumo de recursos, pero principalmente para mostrar que se pueden poner varios colliders también.

Además también vean el botón que tiene, como la mayoría de los colliders, para editarlo. Por supuesto, cada collider puede ser editado basado en el tipo de collider. O sea… Si elijo un BoxCollider2D no puedo hacerlo de una forma que no sea cuadrada/rectangular. Vamos a usar ese botón para darle la forma correspondiente con los puntos que aparecerán en el collider.

 

 

Una vez que haya quedado de su agrado, nuestro héroe estaría listo para empezar a procesar física. Pueden ver qué es lo que va a pasar iniciando el juego, con el botón “Play“. Vean qué es lo que pasa con la nave.

 

 

La nave empieza a caer porque el componente Rigidbody2D le está aplicando física “real“. Tomen ese “real” con muchas comillas. Ya que depende de cómo lo vean. La nave se empieza a comportar como un objeto dentro de un universo sin viento, donde no hay ningún tipo de resistencia en el aire, y todos los objetos tienen, de manera predeterminada, una elasticidad y una masa con valores ya establecidos (por supuesto, los mismos se pueden editar). Además, la gravedad es un valor bastante mentido. O sea, es una fuerza constante que se aplica en la misma dirección (lo que nosotros veríamos como “abajo“). Los objetos no tienen peso, sólo utilizan la masa para moverse.

 

NOTA:

Recuerden que hay una diferencia MUY IMPORTANTE entre PESO y MASA. Si no la conocen, tómense unos minutos para ver este video:

 

Teniendo en cuenta esto de la física mentida, vamos a hacerle (todo el tiempo…), los ajustes necesarios para que se adapte a lo que queramos lograr. El pequeño ajuste que le podemos hacer es cambiar la opción “Gravity Scale” a 0.

 

 

Esta opción funciona como un multiplicador de gravedad, aunque sólo está en los Rigidbody2D y no en los Rigidbody. Dependiendo de cómo lo vean, podríamos usar este valor como si fuera el peso de la nave (recuerden ver el video de arriba si no saben la diferencia entre peso y masa). Así que con este pequeño cambio, la nave ahora “floatará” en lugar de caer.

Observen ahora las consecuencias de los ajustes que hicimos. Siempre que hagamos algo van a notar enseguida qué es lo que tienen que arreglar al momento de probarlo:

 

 

Como notarán, la física está haciendo lo suyo. Nosotros le dijimos a la nave que la gravedad no le afecte, pero por supuesto hay un montón de factores físicos que la siguen afectando, por ejemplo, si una bullet la impacta, el sistema de física les dirá a la nave y a la bullet que se separen. Noten también que la física está afectando también a las bullets, las cuales no sólo están cayendo sino también rotando.

 

 

Vamos a aplicar lo mismo a nuestra nave, para que la rotación ya no le afecte. Cabe destacar que lo que estamos haciendo con esto es decirle a Unity que LA FÍSICA no va a afectar la rotación del objeto. Esto no significa que después no podamos rotar el objeto. Esto no bloquea su rotación, sino que impide que el motor de física lo rote. El objeto podría rotar por otros factores, pero no que tengan que ver con la física desde que tildamos esa opción.

Noten ahora cómo se reparó el problema. Vamos a ver también que tenemos otro pequeño problemita para arreglar:

 

 

El resultado parcial que nos queda es el siguiente:

 

 

Ese problema que podemos observar es por lo mismo que dijimos más arriba: el sistema de física detecta una colisión y empuja a ambos objetos. Pero podemos decirle a Unity que no detecte colisión entre ciertos objetos. Esto lo podemos hacer desde código pero también desde el editor, pero desde código tiene un par de cosas a tener en cuenta que quisiera por ahora no presentarles. Pero si tienen ganas de investigarlo, pueden ENTRAR ACÁ.

Lo primero que vamos a hacer es crear una layer. En Unity las layers sirven para que el motor agrupe determinadas acciones y objetos en algo así como categorías. Para crear una layer seleccionamos un objeto cualquiera y, en la ventana Inspector hacemos click en “Layer“:

 

 

Y luego vamos a “Add Layer“:

 

 

A continuación vamos a ver una ventana donde se nos permitirá agregar nuevas layers. Vamos a agregar dos nuevas, “Good” y “Bad“, en los dos primeros lugares donde Unity nos permita hacerlo.

 

 

A continuación seleccionamos a nuestra nave y le ponemos como layer “Good“.

 

 

Ahora… ¿Qué layer le ponemos a nuestra bullet? Lo primero que quizás se les ocurra es ponerle la layer “Good“, pero si esta bullet puede ser disparada por nuestros enemigos, esto haría que nuestros enemigos básicamente se destruyan entre ellos. Así que lo que vamos a hacer es aplicar la siguiente lógica:

 

Cada nave le aplicará a cada bullet que dispara su propia layer.

 

Esto se lo podemos indicar desde el script del que la va a disparar, por ejemplo, desde el Hero, al momento de dispararla:

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Hero : MonoBehaviour
{
    public float speed;
    public GameObject prefab;
    
    // Use this for initialization
    void Start ()
    {
        
    }
    
    // Update is called once per frame
    void Update ()
    {
        if (Input.GetKey(KeyCode.RightArrow))
        {
            transform.position += Vector3.right * speed * Time.deltaTime;
        }else if (Input.GetKey(KeyCode.LeftArrow))
        {
            transform.position += Vector3.left * speed * Time.deltaTime;
        }
        
        if (Input.GetKey(KeyCode.DownArrow))
        {
            transform.position += Vector3.down * speed * Time.deltaTime;
        }else if (Input.GetKey(KeyCode.UpArrow))
        {
            transform.position += Vector3.up * speed * Time.deltaTime;
        }
        
        if (Input.GetKeyDown(KeyCode.Space))
        {
            GameObject myNewBullet = GameObject.Instantiate(prefab);
            myNewBullet.transform.position = transform.position;

            //Indicamos que la bullet a disparar tiene la misma layer que la nave.
            myNewBullet.layer = gameObject.layer;
        }
    }
}

 

Hecho esto, ahora le tenemos que decir a Unity qué es lo que querémos que haga cuando dos objetos con esas layers colisionan. Para esto, vamos a la pestaña “Physics2DSettings” que se encuentra en “Edit” => “Project Settings” => “Physics 2D“. Vamos a ver una ventana como la siguiente:

 

 

Debajo de todo vamos a encontrar una especie de grilla. Esa grilla define básicamente qué cosa detecta colisión con qué cosa. Lo que vamos a hacer es que “Bad” no detecte colisión con “Bad” y “Good” no detecte colisión con “Good“. Fijense que las layers se repiten a la izquierda y arriba, así que podemos controlar con quién sí detecta colisión cada uno y con quien no.

 

 

Ahora… La prueba de fuego. Prueben si funciona. Deberían ver algo como esto:

 

 

 

¿SIGUE COLISIONANDO?

Se fijaron que realmente la nave tenga asignada la layer “Good“? Es muy común olvidarse de asignarle esa layer después de crearla, ya que la creamos a través de la selección del GameObject, pero luego de la creación de la layer Unity no le asigna dicha layer al objeto. ;).

 

Ahora, para que las bullets colisionen contra nuestros enemigos debemos ponerles a los mismos un collider. Vamos a seguir para este proceso la misma lógica. Elijan el collider que mejor se adapte a la necesidad. Pueden ponerle dos colliders, como hicimos con la nave principal, o uno cuadrado que abarque todo. También pueden usar uno que tenga la forma de la nave enemiga, pero recuerden que el consumo de ese collider es bastante más alto.

Entonces, seleccionamos a nuestro enemigo, y hacemos click en “Add Component” (en la ventana “Inspector“). Podemos tirar al enemigo al escenario temporalmente para ir viendo cómo está el o los colliders que le pongamos. O sea:

  1. Tiramos el prefab del enemigo a la escena.
  2. Le agregamos los dos colliders y los editamos a gusto.
  3. Arrastramos el enemigo de la escena ENCIMA del prefab para actualizarlo.

 

 

 

Ahora probamos el juego y vemos los resultados. Que serían algo como esto:

 

 

Vemos que ahora los enemigos y las bullets colisionan correctamente. Aún no se destruyen porque no programamos eso. Pero también podemos ver que los enemigos colisionan contra nuestra nave. Así que por el momento todo va bien. Así que lo siguiente a hacer es destruir las cosas que colisionan, según corresponda.