Problemas de momento y orden de actualización en mi motor de física

22

ingrese la descripción de la imagen aquí

Esta pregunta es una pregunta de "seguimiento" de la anterior, con respecto a la detección y resolución de colisiones, que puede encontrar aquí .


Si no desea leer la pregunta anterior, aquí hay una breve descripción de cómo funciona mi motor de física:

Cada entidad física se almacena en una clase llamada SSSPBody.

Solo se admiten AABB.

Cada SSSPBody se almacena en una clase llamada SSSPWorld, que actualiza cada cuerpo y maneja la gravedad.

Cada cuadro, SSSPWorld actualiza cada cuerpo.

Cada cuerpo actualizado busca cuerpos cercanos en un hash espacial, comprueba si necesitan detectar colisiones con ellos. En caso afirmativo, invocan un evento de "colisión" y comprueban si necesitan resolver colisiones con ellos. En caso afirmativo, calculan el vector de penetración y la superposición direccional, luego cambian su posición para resolver la penetración.

Cuando un cuerpo choca con otro, transfiere su velocidad al otro simplemente ajustando la velocidad del cuerpo a la suya.

Un cuerpo con velocidad se establece en 0 cuando no ha cambiado de posición desde el último fotograma. Si también choca con un cuerpo móvil (como un elevador o una plataforma móvil), calcula la diferencia de movimiento del elevador para ver si el cuerpo no se ha movido desde su última posición.

Además, un cuerpo invoca un evento "aplastado" cuando todas sus esquinas AABB se superponen a algo en un marco.

Este es el código fuente COMPLETO de mi juego. Está dividido en tres proyectos. SFMLStart es una biblioteca simple que maneja entradas, dibujos y actualizaciones de entidades. SFMLStartPhysics es el más importante, donde se encuentran las clases SSSPBody y SSSPWorld. PlatformerPhysicsTest es el proyecto del juego, que contiene toda la lógica del juego.

Y este es el método de "actualización" en la clase SSSPBody, comentado y simplificado. Puede ver esto solo si no tiene ganas de mirar todo el proyecto SFMLStartSimplePhysics. (E incluso si lo haces, aún deberías echarle un vistazo a esto ya que está comentado).


El .gif muestra dos problemas.

  1. Si los cuerpos se colocan en un orden diferente, se producen resultados diferentes. Las cajas de la izquierda son idénticas a las cajas de la derecha, solo colocadas en orden inverso (en el editor).
  2. Ambas cajas deben ser impulsadas hacia la parte superior de la pantalla. En la situación de la izquierda, no se impulsan cajas. A la derecha, solo uno de ellos es. Ambas situaciones son involuntarias.

Primer problema: orden de actualización

Esto es bastante simple de entender. En la situación de la izquierda, la caja superior se actualiza antes que la otra. Incluso si la caja en la parte inferior "transfiere" la velocidad a la otra, necesita esperar al siguiente cuadro para moverse. Como no se movió, la velocidad de la caja inferior se establece en 0.

No tengo idea de cómo solucionar esto. Prefiero que la solución no dependa de "ordenar" la lista de actualizaciones, porque siento que estoy haciendo algo mal en todo el diseño del motor de física.

¿Cómo manejan los principales motores de física (Box2D, Bullet, Chipmunk) el orden de actualización?


Segundo problema: solo una caja se impulsa hacia el techo

Todavía no entiendo por qué sucede esto. Lo que hace la entidad de "resorte" es establecer la velocidad del cuerpo en -4000 y reubicarla en la parte superior del resorte. Incluso si desactivo el código de reubicación, el problema aún ocurre.

Mi idea es que cuando la caja inferior choca con la caja superior, su velocidad se establece en 0. No estoy seguro de por qué sucede esto.


A pesar de la posibilidad de parecer alguien que se rinde ante el primer problema, publiqué todo el código fuente del proyecto arriba. No tengo nada que lo demuestre, pero créanme, intenté arreglar esto, pero no pude encontrar una solución y no tengo experiencia previa en física y colisiones. Llevo más de una semana intentando resolver estos dos problemas y ahora estoy desesperado.

No creo que pueda encontrar una solución por mi cuenta sin quitar muchas funciones del juego (transferencia de velocidad y resortes, por ejemplo).

Muchas gracias por el tiempo dedicado a leer esta pregunta, y más aún si intentas encontrar una solución o una sugerencia.

Vittorio Romeo
fuente
Cada vez que apilas cajas, ¿podrías combinar su física para que se consideren un solo objeto?
CiscoIPPhone

Respuestas:

12

En realidad, los problemas de orden de actualización son bastante comunes para los motores de física de impulsos normales, no puede simplemente retrasar la aplicación de la fuerza como sugiere Vigil, terminaría rompiendo la preservación de energía cuando un objeto colisiona simultáneamente con otros 2. Por lo general, logran hacer algo que parece bastante real, a pesar de que un orden diferente de actualización habría tenido un resultado significativamente diferente.

En cualquier caso, para su propósito hay suficientes contratiempos en un sistema de impulsos que le sugiero que, en su lugar, cree un modelo de resorte en masa.

La idea básica es que, en lugar de intentar resolver una colisión en un solo paso, aplica una fuerza a los objetos que colisionan, esta fuerza debería ser equivalente a la cantidad de superposición entre los objetos, esto es comparable a cómo los objetos reales durante una colisión transforman su movimiento de energía en deformación y luego de nuevo en movimiento, lo mejor de este sistema es que permite que la fuerza viaje a través de un objeto sin que ese objeto tenga que rebotar de un lado a otro, y razonablemente se puede hacer completamente por orden de actualización independiente.

Para que los objetos se detengan en lugar de rebotar indefinidamente, tendrá que aplicar algún tipo de amortiguación, puede afectar en gran medida el estilo y la sensación de su juego dependiendo de cómo lo haga, pero un enfoque muy básico sería aplique una fuerza a dos objetos en contacto equivalentes a su movimiento interno, puede optar por aplicarla solo cuando se mueven uno hacia el otro, o también cuando se alejan uno del otro, este último puede usarse para evitar por completo que los objetos reboten cuando golpean el suelo, pero también los harán un poco pegajosos.

También puede hacer un efecto de fricción al frenar un objeto en la dirección perpendicular de una colisión, la cantidad de frenado debe ser equivalente a la cantidad de superposición.

Puede evitar el concepto de masa con bastante facilidad haciendo que todos los objetos tengan la misma masa, y los objetos inmóviles funcionarán como tener una masa infinita si simplemente descuida acelerarlos.

Algunos pseudocódigos, en caso de que lo anterior no sea lo suficientemente claro:

//Presuming that you have done collision checks between two objects and now have  
//numbers for how much they overlap in each direction.
overlapX
overlapY
if(overlapX<overlapY){ //Do collision in direction X
    if(obj1.X>obj2.X){
        swap(obj1,obj2)
    }
    //Spring effect:
    obj1.addXvelocity-=overlapX*0.1 //Constant, the lower this is set the softer the  
                                    //collision will be.
    obj2.addXvelocity+=overlapX*0.1
    //Dampener effect:
    velocityDifference=obj2.Xvelocity-obj1.Xvelocity
    //velocityDifference=min(velocityDifference,0) //Uncomment to only dampen when  
                                                   //objects move towards each other.
    obj1.addXvelocity+=velocityDifference*0.1 //Constant, higher for more dampening.
    obj2.addXvelocity-=velocityDifference*0.1
    //Friction effect:
    if(obj1.Yvelocity>obj2.Yvelocity){
        swap(obj1,obj2)
    }
    friction=overlapX*0.01
    if(2*friction>obj2.Yvelocity-obj1.Yvelocity){
        obj1.addYvelocity+=(obj2.Yvelocity-obj1.Yvelocity)/2
        obj2.addYvelocity-=(obj2.Yvelocity-obj1.Yvelocity)/2
    }
    else{
        obj1.addYvelocity+=friction
        obj2.addYvelocity-=friction
    }
}
else{ //Do collision in direction Y

}

El punto de las propiedades addXvelocity y addYvelocity es que se agregan a la velocidad de su objeto después de que se realiza todo el manejo de colisión.

Editar:
podría hacer cosas en el siguiente orden, donde cada viñeta se debe realizar en todos los elementos antes de realizar la siguiente:

  • Detecte colisiones, pueden resolverse tan pronto como se detecten.
  • Agregue los valores addVelocity a los valores de velocidad, agregue gravedad yvelocidad, restablezca los valores addVelocity a 0, mueva los objetos de acuerdo con su velocidad.
  • Renderiza la escena.

Además, me doy cuenta de que lo siguiente podría no estar completamente claro en mi publicación inicial, bajo la influencia de los objetos de gravedad se superpondrán al descansar uno encima del otro, esto sugiere que su cuadro de colisión debe ser ligeramente más alto que su representación gráfica para evitar la superposición visualmente. Este problema será menor si la física se ejecuta a una velocidad de actualización más alta. Le sugiero que intente ejecutar a 120Hz para un compromiso razonable entre el tiempo de CPU y la precisión física.

Edit2:
flujo de motor de física muy básico:

  • Las colisiones y la gravedad producen fuerza / aceleración. acceleration = [Complicated formulas]
  • La fuerza / aceleración se agrega a la velocidad. velocity += acceleration
  • La velocidad se agrega a la posición. position += velocity
aaaaaaaaaaaa
fuente
Se ve bien, nunca pensé en spring-spring para plataformas. Pulgares arriba para algo esclarecedor :)
EnoughTea
Intentaré implementar esto en unas horas, cuando vuelva a casa. ¿Debo mover (Posición + = Velocidad) los cuerpos simultáneamente y luego verificar las colisiones, o moverme y verificar las colisiones una por una? [Además, ¿tengo que modificar manualmente la posición para resolver las colisiones? ¿O cambiar la velocidad se encargará de eso?]
Vittorio Romeo
No estoy completamente seguro de cómo interpretar su primera pregunta. La resolución de colisión cambiará la velocidad y, por lo tanto, solo influirá indirectamente en la posición.
aaaaaaaaaaaa
El hecho es que muevo entidades estableciendo manualmente su velocidad a un cierto valor. Para resolver las superposiciones, elimino la distancia de superposición de su posición. Si uso su método, ¿tendré que mover entidades usando fuerzas u otra cosa? Nunca he hecho eso antes.
Vittorio Romeo
Técnicamente, sí, tendrá que usar fuerzas, en mi código, sin embargo, se simplifica un poco con todos los objetos que tienen un peso 1 y, por lo tanto, la fuerza es igual a la aceleración.
aaaaaaaaaaaa
14

Bueno, obviamente no eres una persona que se rinde fácilmente, eres un verdadero hombre de hierro, habría lanzado mis manos al aire mucho antes, ya que este proyecto se parece mucho a un bosque de algas :)

En primer lugar, las posiciones y las velocidades se establecen en todo el lugar, desde el punto de vista del subsistema de física es una receta para un desastre. Además, al cambiar cosas integrales por varios subsistemas, cree métodos privados como "ChangeVelocityByPhysicsEngine", "ChangeVelocityBySpring", "LimitVelocity", "TransferVelocity" o algo así. Agregará la capacidad de verificar los cambios realizados por una parte específica de la lógica y proporcionará un significado adicional a estos cambios de velocidad. De esa manera la depuración sería más fácil.

Primer problema

Sobre la pregunta misma. Ahora solo está aplicando correcciones de posición y velocidad "a medida que avanzan" en orden de apariencia y lógica del juego. Eso no funcionará para interacciones complejas sin codificar cuidadosamente la física de cada cosa compleja. Entonces no se necesita un motor de física separado.

Para realizar interacciones complejas sin hacks, debe agregar un paso adicional entre la detección de colisiones en función de las posiciones que fueron cambiadas por las velocidades iniciales y los cambios finales de las posiciones en función de la "velocidad posterior". Me imagino que sería así:

  • integre la velocidad usando todas las fuerzas que actúan sobre los cuerpos (ahora está aplicando correcciones de velocidad directamente, deje los cálculos de velocidad en su motor de física y use las fuerzas para mover cosas) , luego use la nueva velocidad para integrar posiciones.
  • detectar colisiones, luego restaurar la velocidad y las posiciones,
  • luego procese colisiones (usando impulsos sin actualización inmediata de posición, ofc, solo se cambia la velocidad hasta el paso final)
  • integre nuevamente la nueva velocidad y procese todas las colisiones usando impulsos nuevamente, excepto que ahora las colisiones son inelásticas.
  • hacer la integración final de las posiciones usando la velocidad resultante.

Pueden aparecer cosas adicionales, como lidiar con sacudidas, negarse a acumularse cuando el FPS es pequeño u otras cosas así, prepárate :)

Segundo problema

La velocidad vertical de ambas cajas de "peso muerto" nunca cambia de cero. Curiosamente, en el bucle de actualización de PhysSpring se asigna velocidad, pero en el bucle de actualización de PhysCrate ya es cero. Es posible encontrar una línea donde la velocidad va mal, pero dejé de depurar aquí porque es la situación "Recoge lo que cosechaste". Es hora de detener la codificación y comenzar a repensar todo cuando la depuración se vuelve difícil. Pero si llega a un punto donde es imposible incluso para el autor del código entender lo que está sucediendo en el código, entonces su base de código ya está muerta sin que se dé cuenta :)

Tercer problema

Creo que algo está mal cuando necesitas recrear una parte de Farseer para hacer un simple juego de plataformas basado en mosaicos. Personalmente, pensaría en su motor actual como una experiencia tremenda, y luego lo abandonaría por completo para obtener una física más simple y directa basada en mosaicos. Al hacerlo, sería prudente retomar cosas como Debug.Asert y quizás incluso, oh, el horror, pruebas unitarias, ya que sería posible detectar cosas inesperadas antes.

EnoughTea
fuente
Me gustó esa comparación de "bosque de algas".
Den
En realidad, estoy un poco avergonzado de usar esas palabras, pero sentí que si resulta en una refactorización o dos, entonces estaría justificado.
EnoughTea
Con solo una prueba en t, ¿no habrá siempre la posibilidad de que esto suceda? Me imagino que necesitarías integrar las velocidades en t y luego verificar las colisiones en t + 1 antes de establecer cualquier velocidad en 0.
Jonathan Connell
Sí, estamos detectando colisiones adelante después de integrar el estado inicial adelante de t a t + dt usando Runge-Kutta o algo así.
EnoughTea
"integrar la velocidad utilizando todas las fuerzas que actúan sobre los cuerpos" "dejar los cálculos de velocidad a su motor de física" - Entiendo lo que está tratando de decir, pero no tengo idea de cómo hacerlo. ¿Hay algún ejemplo / artículo que me puedan mostrar?
Vittorio Romeo
7

Cuando un cuerpo choca con otro, transfiere su velocidad al otro simplemente ajustando la velocidad del cuerpo a la suya.

Su problema es que estos son supuestos fundamentalmente erróneos sobre el movimiento, por lo que lo que obtiene no se parece al movimiento ya que está familiarizado con él.

Cuando un cuerpo choca con otro, se conserva el impulso. Pensar en esto como "A golpea a B" versus "B golpea a A" es aplicar un verbo transitivo a una situación intransitiva. A y B chocan; El impulso resultante debe ser igual al impulso inicial. Es decir, si A y B tienen la misma masa, ahora ambos viajan con la media de sus velocidades originales.

También es probable que necesite un poco de colisión y un solucionador iterativo, o se encontrará con problemas de estabilidad. Probablemente deberías leer algunas de las presentaciones de GDC de Erin Catto.


fuente
2
Solo obtendrían la media de la velocidad original si la colisión es completamente inelástica, por ejemplo, A y B son trozos de masa.
Mikael Öhman
"simplemente ajustando la velocidad del cuerpo a la suya". Son declaraciones como esta las que iluminan por qué no funciona. En general, siempre he descubierto que las personas sin experiencia escriben sistemas de física sin comprender los principios subyacentes involucrados. Nunca 'simplemente establece la velocidad' o 'simplemente ...'. Toda modificación de las propiedades de un cuerpo debe ser una aplicación directa de las leyes de la dinámica; incluida la conservación de la cantidad de movimiento, la energía, etc. Sí, siempre habrá factores falsos para compensar las inestabilidades, pero en ningún momento puede cambiar mágicamente la velocidad de un cuerpo.
MrCranky
En primer lugar, es más fácil asumir cuerpos inelásticos cuando se trata de hacer funcionar un motor, cuanto menos complicado, mejor para la resolución de problemas.
Patrick Hughes
4

Creo que ha hecho un esfuerzo realmente noble, pero parece que hay problemas fundamentales con la estructura del código. Como otros han sugerido, puede ayudar separar las operaciones en partes discretas, por ejemplo:

  1. Fase amplia : recorra todos los objetos; realice una prueba rápida (por ejemplo, AABB) para determinar qué objetos pueden estar colisionando; descarte los que no lo estén.
  2. Fase estrecha : recorra todos los objetos en colisión; calcule un vector de penetración para la colisión (por ejemplo, utilizando SAT).
  3. Respuesta de colisión : recorra la lista de vectores de colisión: calcule un vector de fuerza basado en la masa, luego use esto para calcular un vector de aceleración.
  4. Integración : recorra todos los vectores de aceleración e integre la posición (y la rotación si es necesario).
  5. Renderizado : recorre todas las posiciones calculadas y renderiza cada objeto.

Al separar las fases, todos los objetos se actualizan progresivamente en sincronización y no tendrá las dependencias de orden con las que está luchando actualmente. El código también suele ser más simple y más fácil de cambiar. Cada una de estas fases es bastante genérica, y a menudo es posible sustituir mejores algoritmos después de tener un sistema en funcionamiento.

Dicho esto, cada una de estas partes es una ciencia en sí misma y puede ocupar una gran cantidad de tiempo tratando de encontrar la solución óptima. Puede ser mejor comenzar con algunos de los algoritmos más utilizados:

  • Detección de colisión de fase amplia : hash espacial .
  • Detección de colisión de fase estrecha : para una física de mosaico simple, solo puede aplicar las pruebas de intersección de la caja delimitada por ejes alineados (AABB). Para formas más complicadas puede usar el Teorema del eje de separación . Cualquiera que sea el algoritmo que utilice, debería devolver la dirección y la profundidad de una intersección entre dos objetos (llamado vector de penetración).
  • Respuesta de colisión : use la proyección para resolver la interpenetración.
  • Integración : El integrador es el mayor determinante de la estabilidad y velocidad del motor. Dos opciones populares son la integración Verlet (rápida pero simple) o RK4 (precisa pero lenta). El uso de la integración verlet puede conducir a un diseño extremadamente simple ya que la mayoría de los comportamientos físicos (rebote, rotación) simplemente funcionan sin demasiado esfuerzo. Una de las mejores referencias que he visto para aprender la integración de RK4 es la serie de Glen Fiedler sobre física para juegos .

Un buen (y obvio) lugar para comenzar es con las leyes del movimiento de Newton .

lukevanin
fuente
Gracias por la respuesta. Sin embargo, ¿cómo transfiero la velocidad entre los cuerpos? ¿Ocurre durante la fase de integración?
Vittorio Romeo
En cierto sentido, sí. La transferencia de velocidad comienza con la fase de respuesta de colisión. Es entonces cuando calculas las fuerzas que actúan sobre los cuerpos. La fuerza se traduce en aceleración usando la fórmula aceleración = fuerza / masa. La aceleración se usa en la fase de integración para calcular la velocidad, que luego se usa para calcular la posición. La precisión de la fase de integración determina con qué precisión cambia la velocidad (y posteriormente la posición) con el tiempo.
Luke Van en