Si esta es su primera vez en esta pregunta, sugiero leer primero la parte de pre-actualización a continuación, luego esta parte. Sin embargo, aquí hay una síntesis del problema:
Básicamente, tengo un motor de detección y resolución de colisiones con un sistema de partición espacial de cuadrícula donde importa el orden de colisión y los grupos de colisión. Un cuerpo a la vez debe moverse, luego detectar la colisión, luego resolver las colisiones. Si muevo todos los cuerpos a la vez y luego genero posibles pares de colisión, obviamente es más rápido, pero la resolución se rompe porque no se respeta el orden de colisión. Si muevo un cuerpo a la vez, me veo obligado a hacer que los cuerpos revisen las colisiones, y se convierte en un problema ^ 2. Coloque grupos en la mezcla, y puede imaginar por qué se vuelve muy lento muy rápido con muchos cuerpos.
Actualización: He trabajado muy duro en esto, pero no pude optimizar nada.
También descubrí un gran problema: mi motor depende del orden de colisión.
Intenté una implementación de generación de pares de colisión única , que definitivamente aceleró mucho todo, pero rompió el orden de colisión .
Dejame explicar:
En mi diseño original (sin generar pares), esto sucede:
- un solo cuerpo se mueve
- Después de moverse, refresca sus células y obtiene los cuerpos contra los que choca.
- Si se superpone a un cuerpo que necesita resolver, resuelva la colisión.
Esto significa que si un cuerpo se mueve y golpea una pared (o cualquier otro cuerpo), solo el cuerpo que se ha movido resolverá su colisión y el otro cuerpo no se verá afectado.
Este es el comportamiento que deseo .
Entiendo que no es común para los motores de física, pero tiene muchas ventajas para los juegos de estilo retro .
En el diseño de cuadrícula habitual (que genera pares únicos), esto sucede:
- todos los cuerpos se mueven
- después de que todos los cuerpos se hayan movido, actualice todas las celdas
- generar pares de colisión únicos
- para cada par, maneje la detección y resolución de colisión
en este caso, un movimiento simultáneo podría haber resultado en la superposición de dos cuerpos, y se resolverán al mismo tiempo, lo que efectivamente hace que los cuerpos "se empujen unos a otros" y rompa la estabilidad de colisión con múltiples cuerpos
Este comportamiento es común para los motores de física, pero no es aceptable en mi caso .
También encontré otro problema, que es importante (incluso si no es probable que suceda en una situación del mundo real):
- considerar los cuerpos de los grupos A, B y W
- A choca y resuelve contra W y A
- B choca y resuelve contra W y B
- A no hace nada contra B
- B no hace nada contra A
puede haber una situación en la que muchos cuerpos A y B ocupan la misma celda; en ese caso, hay una gran cantidad de iteraciones innecesarias entre cuerpos que no deben reaccionar entre sí (o solo detectar colisiones pero no resolverlas) .
¡Para 100 cuerpos que ocupan la misma celda, son 100 ^ 100 iteraciones! Esto sucede porque no se generan pares únicos , pero no puedo generar pares únicos , de lo contrario obtendría un comportamiento que no deseo.
¿Hay alguna manera de optimizar este tipo de motor de colisión?
Estas son las pautas que deben respetarse:
¡El orden de colisión es extremadamente importante!
- Los cuerpos deben moverse uno a la vez , luego verificar si hay colisiones, uno a la vez , y resolver después del movimiento, uno a la vez .
Los cuerpos deben tener 3 conjuntos de bits de grupo
- Grupos : grupos a los que pertenece el cuerpo
- GroupsToCheck : agrupa el cuerpo contra el cual debe detectar colisiones
- GroupsNoResolve : agrupa el cuerpo contra el cual no debe resolver la colisión
- Puede haber situaciones en las que solo quiero que se detecte una colisión pero no se resuelva
Pre-actualización:
Prefacio : Soy consciente de que optimizar este cuello de botella no es una necesidad: el motor ya es muy rápido. Sin embargo, por diversión y con fines educativos, me encantaría encontrar una manera de hacer que el motor sea aún más rápido.
Estoy creando un motor de detección / respuesta de colisión 2D C ++ de propósito general, con énfasis en la flexibilidad y la velocidad.
Aquí hay un diagrama muy básico de su arquitectura:
Básicamente, la clase principal es World
, que posee (administra la memoria) de a ResolverBase*
, a SpatialBase*
y a vector<Body*>
.
SpatialBase
es una clase virtual pura que se ocupa de la detección de colisiones de fase amplia.
ResolverBase
es una clase virtual pura que se ocupa de la resolución de colisiones.
Los cuerpos se comunican a la World::SpatialBase*
de SpatialInfo
los objetos, propiedad de los propios cuerpos.
Actualmente hay una clase espacial: Grid : SpatialBase
que es una cuadrícula 2D fija básica. Tiene su propia clase de información, GridInfo : SpatialInfo
.
Así es como se ve su arquitectura:
La Grid
clase posee una matriz 2D de Cell*
. La Cell
clase contiene una colección de (no propiedad) Body*
: una vector<Body*>
que contiene todos los cuerpos que están en la celda.
GridInfo
los objetos también contienen punteros no propietarios a las células en las que se encuentra el cuerpo.
Como dije anteriormente, el motor se basa en grupos.
Body::getGroups()
devuelve unostd::bitset
de todos los grupos de los que forma parte el cuerpo.Body::getGroupsToCheck()
devuelve unostd::bitset
de todos los grupos con los que el cuerpo tiene que verificar la colisión.
Los cuerpos pueden ocupar más de una sola célula. GridInfo siempre almacena punteros no propietarios a las celdas ocupadas.
Después de que un solo cuerpo se mueve, ocurre la detección de colisión. Supongo que todos los cuerpos son cuadros delimitadores alineados con ejes.
Cómo funciona la detección de colisión de fase amplia:
Parte 1: actualización de información espacial
Para cada uno Body
body
:
- Se calculan la celda ocupada superior izquierda y las celdas ocupadas inferior derecha.
- Si difieren de las celdas anteriores,
body.gridInfo.cells
se borra y se llena con todas las celdas que ocupa el cuerpo (2D para el bucle de la celda superior izquierda a la celda inferior derecha).
body
ahora está garantizado para saber qué células ocupa.
Parte 2: controles de colisión reales
Para cada uno Body
body
:
body.gridInfo.handleCollisions
se llama:
void GridInfo::handleCollisions(float mFrameTime)
{
static int paint{-1};
++paint;
for(const auto& c : cells)
for(const auto& b : c->getBodies())
{
if(b->paint == paint) continue;
base.handleCollision(mFrameTime, b);
b->paint = paint;
}
}
void Body::handleCollision(float mFrameTime, Body* mBody)
{
if(mBody == this || !mustCheck(*mBody) || !shape.isOverlapping(mBody->getShape())) return;
auto intersection(getMinIntersection(shape, mBody->getShape()));
onDetection({*mBody, mFrameTime, mBody->getUserData(), intersection});
mBody->onDetection({*this, mFrameTime, userData, -intersection});
if(!resolve || mustIgnoreResolution(*mBody)) return;
bodiesToResolve.push_back(mBody);
}
La colisión se resuelve para cada cuerpo adentro
bodiesToResolve
.Eso es.
Entonces, he estado tratando de optimizar esta detección de colisión de fase amplia durante bastante tiempo. Cada vez que intento algo más que la arquitectura / configuración actual, algo no sale según lo planeado o asumo sobre la simulación que luego se demuestra que es falsa.
Mi pregunta es: ¿cómo puedo optimizar la fase amplia de mi motor de colisión ?
¿Existe algún tipo de optimización mágica de C ++ que se pueda aplicar aquí?
¿Se puede rediseñar la arquitectura para permitir un mayor rendimiento?
- Implementación real: SSVSCollsion
- Body.h , Body.cpp
- World.h , World.cpp
- Grid.h , Grid.cpp
- Cell.h , Cell.cpp
- GridInfo.h , GridInfo.cpp
Salida de Callgrind para la última versión: http://txtup.co/rLJgz
getBodiesToCheck()
se llamó 5462334 veces y tomó el 35,1% de todo el tiempo de creación de perfiles (Tiempo de acceso de lectura de instrucciones)