Unlimited WordPress themes, graphics, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Game Development
  2. Game Physics
Gamedevelopment

Física básica de plataformas en 2D, parte 5: Detección de colisiones de objetos contra objetos

by
Difficulty:BeginnerLength:LongLanguages:
This post is part of a series called Basic 2D Platformer Physics .
Basic 2D Platformer Physics, Part 4
Basic 2D Platformer Physics, Part 6: Object vs. Object Collision Response

Spanish (Español) translation by Elías Nicolás (you can also view the original English article)

En esta parte de la serie, empezaremos a trabajar para que los objetos no solo interactúen físicamente solo con el mapa de mosaicos, sino también con cualquier otro objeto, mediante la implementación de un mecanismo de detección de colisiones entre los objetos del juego.

Demo

La demostración muestra el resultado final de este tutorial. Usa WASD para mover el personaje. El botón central del mouse genera una plataforma unidireccional, el botón derecho del mouse engendra un mosaico sólido y la barra espaciadora genera un clon de caracteres. Los controles deslizantes cambian el tamaño del personaje del jugador. Los objetos que detectan una colisión se hacen semitransparentes.

La demostración se publicó bajo Unity 5.4.0f3 y el código fuente también es compatible con esta versión de Unity.

Detección de colisiones

Antes de hablar sobre cualquier tipo de respuesta de colisión, como hacer imposible que los objetos se atraviesen entre sí, primero debemos saber si esos objetos particulares se superponen.

Esta podría ser una operación muy costosa si simplemente verificamos cada objeto contra cualquier otro objeto en el juego, dependiendo de cuántos objetos activos el juego necesite manejar actualmente. Para aliviar un poco el pobre procesador de nuestros jugadores, usaremos...

¡Particionamiento espacial!

Básicamente se trata de dividir el espacio del juego en áreas más pequeñas, lo que nos permite controlar las colisiones entre objetos que pertenecen solo a la misma área. Esta optimización es muy necesaria en juegos como Terraria, donde el mundo y la cantidad de posibles objetos en colisión son enormes y los objetos están escasamente ubicados. En los juegos de una sola pantalla, donde el número de objetos está muy restringido por el tamaño de la pantalla, a menudo no es necesario, pero sigue siendo útil.

El método

El método de partición espacial más popular para el espacio 2D es quad tree; puedes encontrar su descripción en este tutorial. Para mis juegos, estoy usando una estructura plana, lo que básicamente significa que el espacio del juego está dividido en rectángulos de cierto tamaño, y estoy buscando colisiones con objetos que residen en el mismo espacio rectangular.

The Rectangular space for our objects

Hay un matiz en esto: un objeto puede residir en más de un subespacio a la vez. Eso está totalmente bien, solo significa que necesitamos detectar objetos que pertenecen a cualquiera de las particiones con las que se superpone nuestro objeto anterior.

An object residing in more than one sub-space

Datos para el Particionamiento

La base es simple. Necesitamos saber qué tan grande debe ser cada celda y una matriz bidimensional, de la cual cada elemento es una lista de objetos que residen en un área particular. Necesitamos ubicar esta información en la clase Map.

En nuestro caso, decidí expresar el tamaño de la partición en mosaicos, por lo que cada partición es de 16 por 16 fichas grandes.

Para nuestros objetos, querremos una lista de áreas con las que el objeto se superpone actualmente, así como su índice en cada partición. Vamos a agregar esto a la clase MovingObject.

En lugar de dos listas, podríamos usar un solo diccionario, pero desafortunadamente la sobrecarga de rendimiento de usar contenedores complejos en la iteración actual de Unity deja mucho que desear, así que nos quedaremos con las listas para la demostración.

Inicializar particiones

Pasemos a calcular cuántas particiones necesitamos para cubrir toda el área del mapa. La suposición aquí es que ningún objeto puede flotar fuera de los límites del mapa.

Por supuesto, dependiendo del tamaño del mapa, las particiones no necesitan coincidir exactamente con los límites del mapa. Es por eso que estamos utilizando un techo de valor calculado para garantizar que tengamos al menos suficiente para cubrir todo el mapa.

Vamos a iniciar las particiones ahora.

Aquí no ocurre nada sofisticado, solo nos aseguramos de que cada célula tenga una lista de objetos listos para que podamos operar.

Asignar particiones de objetos

Ahora es el momento de hacer una función que actualice las áreas sobre las que un objeto en particular se superpone.

En primer lugar, necesitamos saber con qué mosaicos de mapas se superpone el objeto. Como solo estamos usando AABB, todo lo que necesitamos verificar es a qué azulejo llega cada rincón de la AABB.

Ahora para obtener las coordenadas en el espacio particionado, todo lo que tenemos que hacer es dividir la posición del mosaico por el tamaño de la partición. No necesitamos calcular la partición de la esquina inferior derecha en este momento, porque su coordenada x será igual a la esquina superior derecha, y su coordenada y será igual a la inferior izquierda.

Todo esto debería funcionar basado en la suposición de que ningún objeto se moverá fuera de los límites del mapa. De lo contrario, necesitaríamos tener una verificación adicional aquí para ignorar los objetos que están fuera de los límites.

Ahora, es posible que el objeto resida por completo en una sola partición, puede residir en dos, o puede ocupar el espacio justo donde se encuentran cuatro particiones. Esto es bajo la suposición de que ningún objeto es más grande que el tamaño de la partición, en cuyo caso podría ocupar todo el mapa y todas las particiones si fuera lo suficientemente grande. He estado operando bajo esta suposición, así es como vamos a manejar esto en el tutorial. Las modificaciones para permitir objetos más grandes son bastante triviales, así que las explicaré también.

Comencemos por verificar con qué áreas se solapa el personaje. Si todas las coordenadas de la partición de la esquina son iguales, entonces el objeto ocupa solo un área.

The object occupying a single area

Si ese no es el caso y las coordenadas son las mismas en el eje x, entonces el objeto se superpone con dos particiones diferentes verticalmente.

An object occupying two of the same partitions along the x-axis

Si estuviéramos soportando objetos que son más grandes que las particiones, sería suficiente si simplemente agregamos todas las particiones desde la esquina superior izquierda a la inferior izquierda usando un bucle.

La misma lógica se aplica si solo las coordenadas verticales son iguales.

An object occupying two of the same partitions along the y-axis

Finalmente, si todas las coordenadas son diferentes, necesitamos agregar las cuatro áreas.

An object occupying four quadrants

Antes de continuar con esta función, debemos ser capaces de agregar y eliminar el objeto de una partición en particular. Vamos a crear estas funciones, comenzando con la adición.

Como puede ver, el procedimiento es muy simple: agregamos el índice del área a la lista de áreas superpuestas del objeto, agregamos el índice correspondiente a la lista de ids del objeto y finalmente agregamos el objeto a la partición.

Ahora crearemos la función de eliminación.

Como puede ver, utilizaremos las coordenadas del área con la que el personaje ya no se superpone, su índice en la lista de objetos dentro de esa área y la referencia al objeto que necesitamos eliminar.

Para eliminar el objeto, lo intercambiaremos con el último objeto de la lista. Esto requerirá que también nos aseguremos de que el índice del objeto para esta área particular se actualice al que tenía nuestro objeto eliminado. Si no intercambiáramos el objeto, necesitaríamos actualizar los índices de todos los objetos que van después del que necesitamos eliminar. En cambio, necesitamos actualizar solo el que intercambiamos.

Tener un diccionario aquí ahorraría muchas molestias, pero eliminar el objeto de un área es una operación que se necesita con mucha menos frecuencia que iterar a través del diccionario, lo que debe hacerse en cada cuadro para cada objeto cuando estamos actualizando la superposición del áreas del objeto.

Ahora necesitamos encontrar el área que nos interesa en la lista de áreas del objeto intercambiado, y cambiar el índice en la lista de ids al índice del objeto eliminado.

Finalmente, podemos eliminar el último objeto de la partición, que ahora es una referencia al objeto que necesitábamos eliminar.

Toda la función debería verse así:

Regresemos a la función UpdateAreas.

Sabemos en qué áreas el personaje se superpone a este marco, pero en el último cuadro el objeto ya podría haber sido asignado a la misma o diferentes áreas. Primero, recorramos las áreas antiguas, y si el objeto ya no se superpone con ellas, eliminemos el objeto de ellas.

Ahora revisemos las nuevas áreas, y si el objeto no se le ha asignado previamente, agreguemoslo ahora.

Finalmente, borre la lista de áreas superpuestas para que esté listo para procesar el siguiente objeto.

¡Eso es! La función final debería verse así:

Detectar colisión entre objetos

En primer lugar, debemos asegurarnos de llamar a UpdateAreas en todos los objetos del juego.

Antes de crear una función en la que verifiquemos todas las colisiones, creemos una estructura que contenga los datos de la colisión.

Esto será muy útil, porque podremos preservar los datos tal como están en el momento de la colisión, mientras que si solo guardáramos la referencia a un objeto con el que colisionamos, no solo tendríamos demasiado poco para trabajar, pero también la posición y otras variables podrían haber cambiado para ese objeto antes de que lleguemos a procesar la colisión en el ciclo de actualización del objeto.

Los datos que guardamos son la referencia al objeto con el que colisionamos, la superposición, la velocidad de ambos objetos en el momento de la colisión, sus posiciones y también sus posiciones justo antes del momento de la colisión.

Pasemos a la clase MovingObject y creemos un contenedor para los datos de colisión recién creados que necesitamos detectar.

Ahora volvamos a la clase Map y creamos una función CheckCollisions. Esta será nuestra función de trabajo pesado donde detectamos las colisiones entre todos los objetos del juego.

Para detectar las colisiones, vamos a iterar a través de todas las particiones.

Para cada partición, vamos a iterar a través de cada objeto dentro de ella.

Para cada objeto, verificamos todos los demás objetos que están más abajo en la lista de la partición. De esta forma, comprobaremos cada colisión solo una vez.

Ahora podemos verificar si las AABB de los objetos se superponen entre sí.

Esto es lo que sucede en la función OverlapsSigned de AABB.

Como puede ver, si el tamaño de un AABB en cualquier eje es cero, no se puede colisionar. La otra cosa que podría notar es que incluso si la superposición es igual a cero, la función devolverá verdadera, ya que rechazará los casos en que la brecha entre las AABB es mayor que cero. Eso se debe principalmente a que si los objetos se tocan entre sí y no se superponen, aún queremos tener la información de que este es el caso, por lo que necesitamos que esto se realice.

Como última cosa, una vez que se detecta la colisión, calculamos cuánto se superpone AABB con la otra AABB. La superposición está firmada, por lo que en este caso si la AABB superpuesta está en el lado derecho de esta AABB, la superposición en el eje x será negativa, y si la otra AABB está en el lado izquierdo de esta AABB, la superposición en el eje x será positivo. Esto facilitará que más adelante salga de la posición de superposición, ya que sabemos en qué dirección queremos que se mueva el objeto.

Volviendo a nuestra función CheckCollisions, si no hubo solapamiento, eso es todo, podemos pasar al siguiente objeto, pero si se produjo una superposición, entonces necesitamos agregar los datos de colisión a ambos objetos.

Para facilitarnos las cosas, supondremos que los 1 (speed1, pos1, oldPos1) en la estructura CollisionData siempre se refieren al propietario de los datos de colisión, y los 2 son los datos relativos al otro objeto.

La otra cosa es que la superposición se calcula desde la perspectiva del obj1. La superposición del obj2 necesita ser negada, por lo que si obj1 necesita moverse hacia la izquierda para salir de la colisión, obj2 tendrá que moverse hacia la derecha para salir de la misma colisión.

Todavía hay una cosa pequeña de la que ocuparse: porque estamos iterando a través de las particiones del mapa y un objeto puede estar en varias particiones al mismo nivel, hasta cuatro en nuestro caso, es posible que detectemos una superposición para el mismo dos objetos hasta cuatro veces.

Para eliminar esta posibilidad, simplemente verificamos si ya hemos detectado una colisión entre dos objetos. Si ese es el caso, omitimos la iteración.

La función HasCollisionDataFor se implementa de la siguiente manera.

Simplemente itera a través de todas las estructuras de datos de colisión y busca si alguno ya pertenece al objeto por el que estamos a punto de verificar la colisión.

Esto debería estar bien en el caso de uso general ya que no esperamos que un objeto colisione con muchos otros objetos, por lo que mirar a través de la lista será rápido. Sin embargo, en un escenario diferente, podría ser mejor reemplazar la lista de CollisionData con un diccionario, así que en lugar de iterar podríamos decir de inmediato si un elemento ya está o no.

La otra cosa es que esta comprobación nos evita agregar múltiples copias de la misma colisión a la misma lista, pero si los objetos no están colisionando, de todos modos vamos a verificar la superposición varias veces si ambos objetos pertenecen a las mismas particiones.

Esto no debería ser una gran preocupación, ya que el control de colisión es barato y la situación no es tan común, pero si fuera un problema, la solución podría ser simplemente tener una matriz de colisiones comprobadas o un diccionario bidireccional, llenar a medida que se verifican las colisiones, y restablecerlo justo antes de llamar a la función CheckCollisions.

Ahora llamemos a la función que acabamos de terminar en el ciclo principal del juego.

¡Eso es! Ahora todos nuestros objetos deberían tener los datos sobre las colisiones.

Para comprobar si todo funciona correctamente, hagámoslo de modo que si un personaje colisiona con un objeto, el sprite del personaje se vuelva semitransparente.

Reviewing Collisions via Animation

Como puede ver, ¡la detección parece estar funcionando bien!

Resumen

Eso es todo por otra parte de la simple serie de física de plataformas 2D. Logramos implementar un mecanismo de partición espacial muy simple y detectar las colisiones entre cada objeto.

Si tiene una pregunta, un consejo sobre cómo hacer algo mejor, o simplemente tiene una opinión sobre el tutorial, ¡siéntase libre de utilizar la sección de comentarios para avisarme!

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.