Cómo crear un motor de física 2D personalizado Friction, Scene y Jump Table

En los dos primeros tutoriales de esta serie, traté los temas de resolución de impulsos y arquitectura central. Ahora es el momento de agregar algunos de los toques finales a nuestro motor de física 2D basado en impulsos.

Los temas que veremos en este artículo son:

  • Fricción
  • Escena
  • Tabla de salto de colisión

Recomendé encarecidamente leer los dos artículos anteriores de la serie antes de intentar abordar este. Parte de la información clave de los artículos anteriores se basa en este artículo..

Nota: Aunque este tutorial está escrito en C ++, debes poder usar las mismas técnicas y conceptos en casi cualquier entorno de desarrollo de juegos.


Video Demo

Aquí hay una rápida demostración de lo que estamos trabajando en esta parte:


Fricción

La fricción es parte de la resolución de colisiones. La fricción siempre aplica una fuerza sobre los objetos en la dirección opuesta al movimiento en el que van a viajar..

En la vida real, la fricción es una interacción increíblemente compleja entre diferentes sustancias, y para modelarla, se hacen vastas suposiciones y aproximaciones. Estas suposiciones están implícitas dentro de las matemáticas, y generalmente son algo así como "la fricción puede ser aproximada por un solo vector", de manera similar a cómo la dinámica del cuerpo rígido simula las interacciones de la vida real al asumir cuerpos con densidad uniforme que no pueden deformarse..

Eche un vistazo rápido al video de demostración del primer artículo de esta serie:

Las interacciones entre los cuerpos son bastante interesantes, y el rebote durante las colisiones se siente realista. Sin embargo, una vez que los objetos aterrizan en la plataforma sólida, simplemente presionan para alejarse de los bordes de la pantalla. Esto se debe a la falta de simulación de fricción..

Impulsos, de nuevo?

Como debe recordar del primer artículo de esta serie, un valor particular, j, representó la magnitud de un impulso requerido para separar la penetración de dos objetos durante una colisión. Esta magnitud puede ser referida como jnormal o jN Como se usa para modificar la velocidad a lo largo de la colisión normal..

La incorporación de una respuesta de fricción implica el cálculo de otra magnitud, denominada jtangent o jT. La fricción será modelada como un impulso. Esta magnitud modificará la velocidad de un objeto a lo largo del vector tangente negativo de la colisión, o en otras palabras a lo largo del vector de fricción. En dos dimensiones, resolver este vector de fricción es un problema solucionable, pero en 3D el problema se vuelve mucho más complejo..

La fricción es bastante simple, y podemos hacer uso de nuestra ecuación anterior para j, Excepto que reemplazaremos todas las instancias de lo normal. norte con un vector tangente t.

\ [Ecuación 1: \\
j = \ frac - (1 + e) ​​(V ^ B -V ^ A) \ cdot n)
\ frac 1 masa ^ A + \ frac 1 masa ^ B \]

Reemplazar norte con t:

\ [Ecuación 2: \\
j = \ frac - (1 + e) ​​((V ^ B -V ^ A) \ cdot t)
\ frac 1 masa ^ A + \ frac 1 masa ^ B \]

Aunque solo una instancia de norte fue reemplazado por t En esta ecuación, una vez que se introducen las rotaciones, se deben reemplazar algunas instancias adicionales, además del único en el numerador de la Ecuación 2.

Ahora la cuestión de cómo calcular t surge El vector tangente es un vector perpendicular a la colisión normal que está más orientado hacia la normal. Esto puede sonar confuso - no te preocupes, tengo un diagrama!

Abajo puede ver el vector tangente perpendicular a la normal. El vector tangente puede apuntar hacia la izquierda o hacia la derecha. A la izquierda estaría "más alejado" de la velocidad relativa. Sin embargo, se define como la perpendicular a la normal que apunta "más hacia" la velocidad relativa.


Vectores de varios tipos dentro del marco temporal de una colisión de cuerpos rígidos.

Como se mencionó brevemente anteriormente, la fricción será un vector orientado opuesto al vector tangente. Esto significa que la dirección en la que se aplica la fricción se puede calcular directamente, ya que se encontró el vector normal durante la detección de colisión.

Sabiendo esto, el vector tangente es (donde norte es la colisión normal):

\ [V ^ R = V ^ B -V ^ A \\
t = V ^ R - (V ^ R \ cdot n) * n \]

Todo lo que queda por resolver jt, La magnitud de la fricción es calcular el valor directamente usando las ecuaciones anteriores. Hay algunas piezas muy difíciles después de calcular este valor que se cubrirán en breve, por lo que no es lo último que se necesita en nuestro sistema de resolución de colisiones:

 // Volver a calcular la velocidad relativa después de que se aplique el impulso normal // (impulso del primer artículo, este código viene // directamente después en la misma función de resolución) Vec2 rv = VB - VA // Resolver para el vector tangente Vec2 tangent = rv - Punto (rv, normal) * tangente normal. Normalizar () // Resolver por magnitud para aplicar a lo largo del vector de fricción jt = -Dot (rv, t) jt = jt / (1 / MassA + 1 / MassB)

El código anterior sigue directamente la Ecuación 2. De nuevo, es importante darse cuenta de que el vector de fricción apunta en la dirección opuesta a nuestro vector tangente, y como tal debemos aplicar un signo negativo cuando punteamos la velocidad relativa a lo largo de la tangente para resolver la velocidad relativa a lo largo del vector tangente. Este signo negativo cambia la velocidad de la tangente y de repente apunta en la dirección en la que la fricción debe aproximarse como.

Ley de Coulomb

La ley de Coulomb es la parte de la simulación de fricción con la que la mayoría de los programadores tienen problemas. Yo mismo tuve que estudiar un poco para descubrir la forma correcta de modelarlo. El truco es que la ley de Coulomb es una desigualdad..

Estados de fricción de Coulomb:

\ [Ecuación 3: \\
F_f <= \mu F_n \]

En otras palabras, la fuerza de fricción es siempre menor o igual que la fuerza normal multiplicada por alguna constante μ (cuyo valor depende de los materiales de los objetos).

La fuerza normal es solo nuestra vieja. j Magnitud multiplicada por la colisión normal. Así que si nuestro resuelto jt (que representa la fuerza de fricción) es menor que μ veces la fuerza normal, entonces podemos usar nuestra jt La magnitud como fricción. Si no, entonces debemos usar nuestros tiempos de fuerza normales μ en lugar. Este "otro" caso es una forma de sujetar nuestra fricción por debajo de algún valor máximo, siendo el máximo los tiempos de fuerza normales μ.

El punto central de la ley de Coulomb es realizar este procedimiento de sujeción. Esta sujeción resulta ser la parte más difícil de la simulación de fricción para que la resolución basada en impulsos encuentre documentación en cualquier lugar, ¡hasta ahora, al menos! La mayoría de los libros blancos que pude encontrar sobre el tema omitieron la fricción por completo o se detuvieron en seco e implementaron procedimientos de sujeción inadecuados (o inexistentes). Es de esperar que ahora tenga una apreciación por comprender que es importante hacer esta parte correctamente..

Solo repartimos las pinzas de una sola vez antes de explicar algo. Este bloque de código siguiente es el ejemplo de código anterior con el procedimiento de sujeción terminado y la aplicación de impulso de fricción en conjunto:

 // Volver a calcular la velocidad relativa después de que se aplique el impulso normal // (impulso del primer artículo, este código viene // directamente después en la misma función de resolución) Vec2 rv = VB - VA // Resolver para el vector tangente Vec2 tangent = rv - Punto (rv, normal) * tangente normal. Normalizar () // Resolver por magnitud para aplicar a lo largo del vector de fricción jt = -Dot (rv, t) jt = jt / (1 / MassA + 1 / MassB) // PythagoreanSolve = A ^ 2 + B ^ 2 = C ^ 2, resolviendo para C dado A y B // Se usa para aproximar a mu dados los coeficientes de fricción de cada cuerpo flotante mu = PythagoreanSolve (A-> staticFriction, B-> staticFriction) // Sujete la magnitud de la fricción y cree el vector de impulso Vec2 frictionImpulse if (abs (jt) < j * mu) frictionImpulse = jt * t else  dynamicFriction = PythagoreanSolve( A->dynamicFriction, B-> dynamicFriction) frictionImpulse = -j * t * dynamicFriction // Apply A-> speed - = (1 / A-> mass) * frictionImpulse B-> speed + = (1 / B-> mass) * Fricción Impulso

Decidí usar esta fórmula para resolver los coeficientes de fricción entre dos cuerpos, dado un coeficiente para cada cuerpo:

\ [Ecuación 4: \\
Fricción = \ sqrt [] Fricción ^ 2_A + Fricción ^ 2_B \]

De hecho, vi a alguien más hacer esto en su propio motor de física, y me gustó el resultado. Un promedio de los dos valores funcionaría perfectamente bien para deshacerse del uso de la raíz cuadrada. Realmente, cualquier forma de escoger el coeficiente de fricción funcionará; esto es justo lo que prefiero. Otra opción sería usar una tabla de búsqueda donde el tipo de cada cuerpo se use como un índice en una tabla 2D.

Es importante que el valor absoluto de jt se utiliza en la comparación, ya que la comparación teóricamente sujeta magnitudes brutas por debajo de algún umbral. Ya que j siempre es positivo, debe voltearse para representar un vector de fricción adecuado, en el caso de que se use fricción dinámica.

Fricción estática y dinámica

¡En el último fragmento de código se introdujeron fricciones estáticas y dinámicas sin ninguna explicación! Dedicaré toda esta sección a explicar la diferencia y la necesidad de estos dos tipos de valores..

Algo interesante sucede con la fricción: requiere una "energía de activación" para que los objetos comiencen a moverse cuando están completamente descansados. Cuando dos objetos descansan uno sobre el otro en la vida real, se necesita una buena cantidad de energía para empujar uno y hacer que se mueva. Sin embargo, una vez que obtiene algo deslizante, a menudo es más fácil mantenerlo deslizándose a partir de ese momento..

Esto se debe a cómo funciona la fricción a nivel microscópico. Otra foto ayuda aquí:


Vista microscópica de qué causa la energía de activación debido a la fricción..

Como puede ver, las pequeñas deformidades entre las superficies son realmente el principal culpable que crea fricción en primer lugar. Cuando un objeto descansa sobre otro, las deformidades microscópicas descansan entre los objetos, entrelazados. Estos deben romperse o separarse para que los objetos se deslicen uno contra el otro..

Necesitamos una forma de modelar esto dentro de nuestro motor. Una solución simple es proporcionar a cada tipo de material dos valores de fricción: uno para estático y otro para dinámico.

La fricción estática se utiliza para sujetar nuestra jt magnitud. Si el resuelto jt La magnitud es lo suficientemente baja (por debajo de nuestro umbral), entonces podemos asumir que el objeto está en reposo, o casi como en reposo y usar todo el jt como un impulso.

En el otro lado, si nuestro resuelto jt está por encima del umbral, se puede suponer que el objeto ya ha roto la "energía de activación", y en tal situación se usa un impulso de fricción más bajo, que se representa por un coeficiente de fricción más pequeño y un cálculo de impulso ligeramente diferente.


Escena

Suponiendo que no se saltó ninguna parte de la sección de Fricción, ¡bien hecho! Has completado la parte más difícil de esta serie completa (en mi opinión).

los Escena La clase actúa como un contenedor para todo lo que involucra un escenario de simulación física. Llama y usa los resultados de cualquier fase amplia, contiene todos los cuerpos rígidos, ejecuta controles de colisión y resolución de llamadas. También integra todos los objetos vivos. La escena también interactúa con el usuario (como en el programador que usa el motor de física).

Aquí hay un ejemplo de cómo puede verse una estructura de escena:

 clase Escena public: Scene (Vec2 gravity, real dt); ~ Escena (); void SetGravity (Vec2 gravity) void SetDT (real dt) Body * CreateBody (ShapeInterface * shape, BodyDef def) // Inserta un body en la escena e inicializa el body (calcula la masa). // void InsertBody (Cuerpo * cuerpo) // Borra un cuerpo de la escena void RemoveBody (Cuerpo * cuerpo) // Actualiza la escena con un solo paso de tiempo void Paso (void) float GetDT (void) LinkedList * GetBodyList (void) Vec2 GetGravity (void) void QueryAABB (CallBackQuery cb, const AABB & aabb) void QueryPoint (CallBackQuery cb, const Point2 & point) privado: float dt // Timestepeaspelgas de las personas de las fuerzas de las personas de las fuerzas de las fuerzas de las fuerzas de los Estados Unidos de América broadphase;

No hay nada particularmente complejo sobre el Escena clase. La idea es permitir al usuario agregar y eliminar cuerpos rígidos fácilmente. los BodyDef es una estructura que contiene toda la información sobre un cuerpo rígido y se puede usar para permitir al usuario insertar valores como una especie de estructura de configuración.

La otra función importante es Paso(). Esta función realiza una única ronda de comprobaciones de colisión, resolución e integración. Esto debe llamarse desde el bucle de paso de tiempo descrito en el segundo artículo de esta serie..

Consultar un punto o AABB implica verificar qué objetos realmente chocan con un puntero o AABB dentro de la escena. Esto facilita que la lógica relacionada con el juego vea cómo se colocan las cosas en el mundo..


Tabla de salto

Necesitamos una forma fácil de elegir a qué función de colisión debería llamarse, según el tipo de dos objetos diferentes.

En C ++ hay dos formas principales que conozco: doble despacho y una tabla de saltos 2D. En mis propias pruebas personales encontré la tabla de saltos 2D en superior, así que entraré en detalles sobre cómo implementar eso. Si planea usar un lenguaje que no sea C o C ++, estoy seguro de que una serie de funciones u objetos de funciones pueden construirse de manera similar a una tabla de punteros de funciones (que es otra razón por la que elegí hablar sobre tablas de salto en lugar de otras opciones) que son mas especificos a C ++).

Una tabla de salto en C o C ++ es una tabla de punteros de función. Los índices que representan nombres o constantes arbitrarios se utilizan para indexar en la tabla y llamar a una función específica. El uso podría ser algo así para una tabla de salto 1D:

 enumeración Animal Rabbit Duck Lion; const void (* talk) (void) [] = RabbitTalk, DuckTalk, LionTalk,; // Llamar a una función desde la tabla con conversación de despacho virtual 1D [Rabbit] () // llama a la función RabbitTalk

El código anterior en realidad imita lo que el lenguaje C ++ implementa con Llamadas a funciones virtuales y herencia. Sin embargo, C ++ solo implementa llamadas virtuales unidimensionales. Una mesa 2D puede ser construida a mano..

Aquí hay algunos psuedocode para una tabla de salto 2D para llamar a las rutinas de colisión:

 collisionCallbackArray = AABBvsAABB AABBvsCircle CirclevsAABB CirclevsCircle // Llame a una rutina de colisión para la detección de colisiones entre A y B // dos colectores sin saber su tipo de colisionador exacto // el tipo puede ser de AABB o Circle collisionCallbackArray [A-> type] [B -> tipo] (A, B)

¡Y ahí lo tenemos! Los tipos reales de cada colisionador se pueden usar para indexar en una matriz 2D y seleccionar una función para resolver la colisión.

Tenga en cuenta, sin embargo, que AABBvsCircle y CirclevsAABB Son casi duplicados. ¡Esto es necesario! Es necesario cambiar la normalidad para una de estas dos funciones, y esa es la única diferencia entre ellas. Esto permite una resolución de colisión consistente, sin importar la combinación de objetos para resolver.


Conclusión

¡Ya hemos cubierto una gran cantidad de temas en la configuración de un motor de física de cuerpos rígidos personalizados completamente desde cero! La resolución de colisiones, la fricción y la arquitectura del motor son todos los temas que se han tratado hasta ahora. Se puede construir un motor de física completamente exitoso adecuado para muchos juegos bidimensionales de nivel de producción con el conocimiento presentado en esta serie hasta ahora.

De cara al futuro, planeo escribir un artículo más dedicado por completo a una característica muy deseable: la rotación y la orientación. Los objetos orientados son sumamente atractivos para verlos interactuar entre sí, y son la pieza final que requiere nuestro motor de física personalizado..

La resolución de la rotación resulta bastante simple, aunque la detección de colisiones tiene un impacto en la complejidad. Buena suerte hasta la próxima, y ​​por favor haga preguntas o publique comentarios a continuación!