¿Hay alguna manera de aumentar la eficiencia de verificación de colisión de un sistema de n objetos?

9

Estoy haciendo un juego que consta de muchos objetos en pantalla, uno de los cuales es el jugador. Necesito saber qué objetos están colisionando en cada iteración.

Hice algo como esto:

for (o in objects)
{
   o.stuff();
   for (other in objects)
      if (collision(o, other))
          doStuff();

   bla.draw();
}

Esto tiene O (n ^ 2), que me han dicho que es malo. ¿Cómo hago esto de manera más eficiente? ¿Es posible? Estoy haciendo esto en Javascript, y n generalmente será inferior a 30, ¿será un problema si esto sigue igual?

jcora
fuente
3
¿Has intentado ejecutar el código para ver cómo funciona?
thedaian
No, no lo he hecho, solo estoy asumiendo que es malo debido a la O (n ^ 2).
jcora
1
¿Solo 30 objetos? Hubiera recomendado la partición espacial, pero sería inútil con solo 30 objetos. Hay algunas optimizaciones menores que otros han señalado, pero todas son optimizaciones menores en la escala de la que estás hablando.
John McDonald

Respuestas:

16

Con solo 30 objetos como máximo, no debería necesitar mucha optimización para no comparar los mismos dos pares entre sí más de una vez por cuadro. Que cubrirá el siguiente ejemplo de código. Pero si le interesan las diferentes optimizaciones que usaría un motor de física, continúe leyendo el resto de esta publicación.

Lo que necesitará es una implementación de partición espacial , como un Octree (para juegos 3D) o Quadtree (para juegos 2D). Estos dividen el mundo en subsecciones, y luego cada subsección se divide más en la misma mansión, hasta que se hayan subdividido en un tamaño mínimo. Esto le permite verificar rápidamente qué otros objetos están en la misma región del mundo que otro, lo que limita la cantidad de colisiones con las que debe verificar.

Además de la partición espacial, recomendaría crear un AABB ( cuadro delimitador alineado con el eje ) para cada uno de sus objetos de física. Esto le permite verificar el AABB de un objeto contra otro, lo cual es mucho más rápido que una verificación detallada por poli entre objetos.

Esto puede llevarse un paso más allá para objetos físicos complicados o grandes, donde puede subdividir la malla física en sí, dando a cada subforma su propio AABB con el que puede verificar solo si los AABB de dos objetos se superponen.

La mayoría de los motores de física desactivarán la simulación física activa en los cuerpos físicos una vez que se detengan. Cuando un cuerpo de física está desactivado, solo necesita verificar la colisión contra su AABB en cada cuadro, y si algo choca con el AABB, entonces se reactivará y realizará una verificación de colisión más granular. Esto mantiene bajos los tiempos de simulación.

Además, muchos motores de física usan 'islas de simulación', que es donde se agrupa un grupo de cuerpos de física que están muy juntos. Si todo en la isla de simulación está en reposo, entonces la misma isla de simulación está desactivada. El beneficio de la isla de simulación es que todos los cuerpos dentro de ella pueden dejar de verificar colisiones una vez que la isla está inactiva, y la única verificación de cada cuadro es ver si algo ingresó al AABB de la isla. Solo una vez que algo ingrese al AABB de la isla, cada uno de los cuerpos dentro de la isla deberá verificar las colisiones. La isla de simulación también se reactiva si algún cuerpo dentro de ella comienza a moverse nuevamente por sí solo. Si un cuerpo se mueve lo suficientemente lejos del centro del grupo, se elimina de la isla.

Al final te queda algo como esto (en pseudocódigo):

// Go through each leaf node in the octree. This could be more efficient
// by keeping a list of leaf nodes with objects in it.
for ( node in octreeLeafNodes )
{
    // We only need to check for collision if more than one object
    // or island is in the bounds of this octree node.
    if ( node.numAABBsInBounds > 1)
    {
        for ( int i = 0; i < AABBNodes.size(); ++i )
        {
           // Using i+1 here allows us to skip duplicate checks between AABBS
           // e.g (If there are 5 bodies, and i = 0, we only check i against
           //      indexes 1,2,3,4. Once i = 1, we only check i against indexes
           //      2,3,4)
           for ( int j = i + 1; j < AABBNodes.size(); ++j )
           {
               if ( AABBOverlaps( AABBNodes[i], AABBNodes[j] ) )
               {
                   // If the AABB we checked against was a simulation island
                   // then we now check against the nodes in the simulation island

                   // Once you find overlaps between two actual object AABBs
                   // you can now check sub-nodes with each object, if you went
                   // that far in optimizing physics meshes.
               {
           }
        }
    }
}

También recomendaría no tener tantos bucles dentro de bucles como este, la muestra anterior fue solo para que usted tenga la idea, lo dividiría en múltiples funciones que le brindan la misma funcionalidad que algo como lo que se muestra arriba.

Además, asegúrese de no alterar el contenedor AABBNodes mientras lo recorre, ya que eso podría significar verificaciones de colisión perdidas. Esto puede sonar a sentido común, pero se sorprendería de lo fácil que es hacer que las cosas reaccionen a colisiones que causan cambios que no anticiparía. Por ejemplo, si una colisión provoca que uno de los objetos en colisión cambie de posición lo suficiente como para eliminarlos del AABB del nodo Octree que estaba comprobando, podría alterar ese contenedor. Para resolver esto, recomiendo mantener una lista de todos los eventos de colisión que ocurren durante las verificaciones, y luego, después de completar todas las verificaciones, revise la lista y envíe cualquier evento de colisión.

Nic Foster
fuente
44
Respuesta muy consistente con precisiones técnicas agradables y útiles para abrir la mente del lector a los métodos existentes. +1
Valkea
¿Qué sucede si necesito eliminar el objeto que colisiona? ¿Puedo alterar el contenedor? Me refiero a eliminarlo del contenedor ya que ya no necesito el objeto porque está "destruido". Necesito un bucle más para ejecutar los eventos de colisión si no lo elimino durante la detección de colisión.
newguy
Quitar el objeto colisionador está bien, pero recomendaría esperar hasta después de que se haya realizado el paso de colisión durante toda la simulación. Por lo general, solo marca los objetos que deben eliminarse, o genera una lista de los objetos que deben eliminarse, y luego, una vez realizada la simulación de colisión, aplica esos cambios.
Nic Foster
4

Su ejemplo prueba cada par de objetos varias veces.

Tomemos un ejemplo muy simple con una matriz que contiene 0,1,2,3

Con tu código obtienes esto:

  • En el bucle 0 prueba contra 1, 2 y 3
  • En el bucle 1, prueba contra 0, 2 y 3 ===> (0-1 ya probado)
  • En el bucle 2, prueba contra 0, 1 y 3 ===> (0-2 / 1-2 ya probado)
  • En el bucle 3, prueba contra 0, 1 y 2 ===> (0-3 / 1-3 / 2-3 ya probado)

Ahora veamos el siguiente código:

for(i=0;i<=objects.length;i++)
{
    objects[i].stuff();

    for(j=i+1;j<=objects.length;j++)
    {
        if (collision(objects[i], objects[j]))
        doStuff();
    }

    bla.draw();
}

Si usamos la matriz que contiene 0,1,2,3 una vez más, tenemos el siguiente comportamiento:

  • En el bucle 0 prueba contra 1, 2, 3
  • En el bucle 1 prueba contra 2, 3
  • En el bucle 2, prueba contra 3
  • En el bucle 3 no pruebas nada

Con el segundo algoritmo tenemos 6 pruebas de colisión, mientras que el anterior solicitó 12 pruebas de colisión.

Valkea
fuente
Este algoritmo hace N(N-1)/2comparaciones que todavía es el rendimiento O (N ^ 2).
Kai
1
Bueno, con 30 objetos según lo solicitado, eso significa 465 pruebas de colisión contra 870 ... probablemente sea similar desde su punto de vista, pero no desde el mío. Además, la solución ofrecida en la otra respuesta es exactamente el mismo algoritmo :)
Valkea
1
@ Valkea: Bueno, parte de eso es. :)
Nic Foster
@NicFoster: sí, tienes razón;) Estaba hablando estrictamente sobre la prueba de colisión entre los objetos seleccionados, no sobre la parte de partición del algoritmo, que obviamente es una adición muy valiosa que ni siquiera pensé agregar en mi ejemplo cuando Lo estaba escribiendo
Valkea
¿Esto se llama amortización? ¡Gracias de todos modos!
jcora
3

Diseñe su algoritmo según sus necesidades, pero mantenga los detalles de implementación encapsulados. Incluso en Javascript, se aplican los conceptos básicos de OOP.

Porque N =~ 30, O(N*N)no es una preocupación, y es probable que su búsqueda lineal sea tan rápida como cualquier otra alternativa. Pero no desea codificar suposiciones en su código. En pseudocódigo, tendrías una interfaz

interface itemContainer { 
    add(BoundingBox);
    remove(BoundingBox);
    BoundingBox[] getIntersections();
}

Eso describe lo que puede hacer su lista de artículos. Luego puede escribir una clase ArrayContainer que implemente esta interfaz. En Javascript, el código se vería así:

function ArrayContainer() { ... } // this uses an array to store my objects
ArrayContainer.prototype.add = function(box) { ... };
ArrayContainer.prototype.remove = function(box) { ... };
ArrayContainer.prototype.getIntersections = function() { ... };

function QuadTreeContainer { ... } // this uses a quadtree to store my objects
... and implement in the add/remove/getIntersections for QuadTreeContainer too

Y aquí hay un código de ejemplo que crea 300 cuadros de límite y obtiene todas las intersecciones. Si ha implementado ArrayContainer y QuadTreeContainer correctamente, lo único que necesitaría cambiar en su código es cambiarvar allMyObjects = new ArrayContainer() a var allMyObjects = QuadTreeContainer().

var r = Math.random;
var allMyObjects = new ArrayContainer();
for(var i=0; i<300; i++)
    allMyObjects.add(new BoundingBox(r(), r()));
var intersections = allMyObjects.getIntersections();

Seguí adelante y agité la implementación del ArrayContainer estándar aquí:

http://jsfiddle.net/SKkN5/1/

Palanqueta
fuente
Nota: Esta respuesta fue motivada por la queja de Bane de que su base de código se estaba volviendo demasiado grande, desordenada y difícil de administrar. Aunque no agrega mucho a la discusión sobre el uso de una matriz frente a un árbol, espero que sea una respuesta relevante sobre cómo podría específicamente organizar mejor su código.
Jimmy
2

También debe considerar los tipos de objetos que pueden chocar sensiblemente.

Por ejemplo, es probable que el jugador deba verificarse por colisión con todo excepto sus propias balas. Sin embargo, los enemigos solo pueden necesitar comprobar contra las balas del jugador. Las balas casi seguramente no necesitan chocar entre sí.

Para implementar esto de manera eficiente, probablemente desee mantener listas separadas de objetos, una por tipo de objeto.

Adán
fuente