En la anterior entrada vimos un poquito de las matemáticas que usaremos en nuestros juegos. Hoy vamos a aplicar un poco de esa teoría y echar un vistazo muy básico a las colisiones, algo que se usa en casi todos los juegos (por no decir en todos). Concretamente vamos a ver 4 casos: punto contra círculo, círculo contra círculo, punto contra caja y caja contra caja. El código que se muestra es orientativo, no significa que lo vayamos a usar en los juegos sino que está a modo ayuda de la explicación.
El caso del punto contra círculo puede sernos útil por ejemplo en un juego de vista cenital (visto desde arriba hacia abajo) donde el punto podría ser el protagonista y el círculo sea el área donde se activa una mina. Si el jugador entra dentro del área podríamos hacer explotar la mina y así dañarlo.
Veamos la siguiente imagen:
Tenemos el punto P1, que está fuera del círculo C y el punto P2 que sí esta dentro del círculo. Los puntos P1 y P2 los representamos como vectores, como vimos en la anterior entrada. El punto C representa el centro del círculo y también es un vector, mientras que r es el radio de este circulo y es un número escalar (un número "normal" vamos). Los círculos los definimos mediante estos dos valores (la posición del centro y el radio del círculo).
Bien, ¿Cómo detectamos que un punto está dentro de un círculo? Tenemos la posición del punto y también la posición del círculo así como su radio. Podemos calcular la distancia entre el centro del círculo y el punto, y si es menor o igual a r es que está dentro. ¿Cómo se hace lo de la distancia? Muy fácil, usando el módulo de un vector: calculamos el vector que va desde el punto C al P1, haciendo la resta P1 - C, y calculamos el módulo de este nuevo vector. Luego lo comparamos con r y si es igual o menor estamos dentro, sino no. En C++ quedaría por ejemplo algo así:
bool pointInsideCircle(const Circle& circle, const Vec2& point) { return (circle.center() - point).length() <= circle.radius(); }
Hacemos la resta entre el centro del círculo y el punto, calculamos su módulo (el método "length()") y lo comparamos con el radio del círculo. Si el módulo es menor o igual el punto está dentro.
Con esto hay una pequeña optimización que puede ser útil en casos donde se haga esta comprobación muchísimas veces y queremos que vaya más rápido: en vez de usar la distancia en sí podemos usar el cuadrado de la distancia de forma que nos ahorramos la relativamente costosa operación de la raíz cuadrada. Cada círculo guarda además de su centro y radio el cuadrado de este último. Luego en "pointInsideCircle()" hacemos lo siguiente:
bool pointInsideCircle(const Circle& circle, const Vec2& point) { return (circle.center() - point).lengthSq() <= circle.radiusSq(); }
Exactamente igual que la otra función, sólo que ahora el método del módulo es "lengthSq()" ("Sq" de "square", cuadrado) y en vez de usar "radius()" usamos "radiusSq()". Este último método devuelve el cuadrado del radio del círculo, que podemos tenerlo precalculado cuando creamos el círculo o cambiamos su radio.
El método "lengthSq()" es como "length()" pero omite el paso de calcular la raíz cuadrada. Si tenemos los valores A y B, donde A > B el cuadrado del primero también es mayor que el segundo: A * A > B * B. Para saber si el punto está dentro del círculo no nos hace falta saber la distancia en sí, sólo con saber si es menor que el radio ya es suficiente, por lo que en este caso el "truco" de usar los cuadrados nos sirve perfectamente.
Círculo contra círculo
Comprobar la colisión entre 2 círculos puede sernos útil por ejemplo para saber si el enorme proyectil circular que nos ha disparado un jefe ha colisionado o no con el escudo de nuestra nave. Consideremos la siguiente imagen:
En este caso tenemos 3 círculos: C1, C2 y C3. C2 no colisiona con C1, mientras que C3 sí lo hace. Comprobar este tipo de colisiones es como el caso del punto contra círculo pero con un pequeño cambio: teniendo 2 círculos, calculamos el vector entre los centros de ambos y calculamos el módulo de este nuevo vector. Este módulo lo comparamos con la suma de los radios de ambos círculos, si es menor existe colisión. En C++ tendríamos algo así:
bool circlesIntersect(const Circle& c1, const Circle& c2) { return (c1.center() - c2.center()).length() <= c1.radius() + c2.radius(); }
Obviamente esto también admite el "truco" de los cuadrados, simplemente obtenemos la distancia cuadrada del vector y lo comparamos con la suma de los cuadrados de los radios de cada círculo.
Punto contra caja
Este caso tiene la misma utilidad que el de punto contra círculo, sólo que en algunos casos nos servirá mejor usar una caja que un círculo.
Hay que tener en cuenta que hay 2 tipos de cajas en líneas generales: los alineados con los ejes (en inglés llamadas "Axis Aligned Bounding Box" o "AABB" usando sus iniciales) y los que son "libres" u orientados ("Oriented Bounding Box" o "OBB" en inglés), que pueden tener cualquier orientación. Obviamente los primeros son un caso especial del segundo, ya que serían OBB pero con una orientación fija de forma que los lados de la caja fuesen paralelos al eje X o Y. Sólo vamos a ver los AABB en esta entrada.
Como en los casos anteriores partimos de una imagen:
Una caja AABB la podemos definir con su origen (punto O en la imagen), su anchura y altura. Debido a que usamos este tipo de cajas podemos calcular si el punto está o no dentro de la caja con simples comparaciones. Para saber si un punto está dentro comprobamos si su coordenada X está entre los dos lados verticales de la caja y si su coordenada Y está entre los lados horizontales. Esto en C++ lo podríamos implementar así:
bool pointInsideRect(const Rect& rect, const Vec2& point) { return rect.pos().x() <= point.x() && point.x() <= rect.pos().x() + rect.width() && rect.pos().y() <= point.y() && point.y() <= rect.pos().y() + rect.height(); }
Caja contra caja
Llegamos al último caso, la colisión entre dos cajas. Primero la imagen
En este caso en vez de ver si una caja colisiona con otra podemos mirar que no colisiona. Fijémonos en las cajas B1 y B4: no colisionan porque el lado horizontal de B1 más alto ("alto" en el sentido habitual, ya que el eje Y crece hacia abajo y realmente sería el más bajo respecto al sistema de coordenadas) tiene un valor superior del lado más bajo de B4. Podemos generalizar esto al resto de lados y ya tenemos la función que nos dice si dos AABB colisionan o no:
bool rectsIntersect(const Rect& r1, const Rect& r2) { return !(r1.pos().x() > r2.pos().x() + r2.width() || r1.pos().x() + r1.width() < r2.pos().x() || r1.pos().y() > r2.pos().y() + r2.height() || r1.pos().y() + r1.height() < r2.pos().y()); }
Por cada lado comprobamos si nos hemos "pasado". Si no nos "pasamos" por ningún lado es que los AABB colisionan.
Como punto final comentar que usar puntos, círculos o cajas depende de lo que queramos hacer. En algunas ocasiones nos vendrá mejor usar un círculo, otras veces una caja y otras con un punto es suficiente.
Con esta entrada terminamos las explicaciones preliminares, en la siguiente ya empezaremos a programar Culebrilla. ¡Hasta pronto!