Comment créer un moteur physique 2D personnalisé friction, scène et table de saut

Dans les deux premiers tutoriels de cette série, j'ai abordé les thèmes de la résolution d'impulsion et de l'architecture principale. Il est maintenant temps d'ajouter quelques éléments finals à notre moteur physique 2D à base d'impulsions.

Les sujets abordés dans cet article sont les suivants:

  • Friction
  • Scène
  • Table de saut de collision

J'ai fortement recommandé de lire les deux articles précédents de la série avant de tenter de s'attaquer à celui-ci. Certaines informations clés dans les articles précédents sont développées dans cet article.

Remarque: Bien que ce tutoriel soit écrit en C ++, vous devriez pouvoir utiliser les mêmes techniques et concepts dans presque tous les environnements de développement de jeux..


Démo vidéo

Voici une brève démonstration de ce à quoi nous travaillons dans cette partie:


Friction

La friction fait partie de la résolution des collisions. La friction exerce toujours une force sur les objets dans la direction opposée au mouvement dans lequel ils doivent se déplacer.

Dans la vie réelle, le frottement est une interaction incroyablement complexe entre différentes substances et, pour le modéliser, de vastes hypothèses et approximations sont établies. Ces hypothèses sont implicites dans les calculs, et sont généralement quelque chose comme "le frottement peut être approché par un seul vecteur" - de la même manière que la dynamique de corps rigide simule des interactions réelles en supposant des corps de densité uniforme qui ne peuvent pas se déformer..

Jetez un coup d’œil à la démo vidéo du premier article de cette série:

Les interactions entre les corps sont très intéressantes et le rebond lors des collisions est réaliste. Cependant, une fois que les objets ont atterri sur la plate-forme solide, ils se sont tout simplement effacés et se sont éloignés des bords de l'écran. Ceci est dû à un manque de simulation de frottement.

Des impulsions, encore?

Comme vous devez le rappeler dans le premier article de cette série, une valeur particulière, j, représentait la magnitude d'une impulsion nécessaire pour séparer la pénétration de deux objets lors d'une collision. Cette grandeur peut être appelée normal ou Jn comme il est utilisé pour modifier la vitesse le long de la normale de collision.

L’incorporation d’une réponse par frottement implique le calcul d’une autre grandeur, appelée jtangent ou jT. La friction sera modélisée comme une impulsion. Cette magnitude modifiera la vitesse d'un objet le long du vecteur tangent négatif de la collision ou, en d'autres termes, le long du vecteur de frottement. En deux dimensions, la résolution de ce vecteur de frottement est un problème qui peut être résolu, mais en 3D, le problème devient beaucoup plus complexe..

La friction est assez simple, et nous pouvons utiliser notre équation précédente pour j, sauf que nous allons remplacer toutes les instances de la normale n avec un vecteur tangent t.

\ [Équation 1: \\
j = \ frac - (1 + e) ​​(V ^ B -V ^ A) \ cdot n)
\ frac 1 masse ^ A + \ frac 1 masse ^ B \]

Remplacer n avec t:

\ [Équation 2: \\
j = \ frac - (1 + e) ​​((V ^ B -V ^ A) \ cdot t)
\ frac 1 masse ^ A + \ frac 1 masse ^ B \]

Bien qu’une seule instance de n a été remplacé par t dans cette équation, une fois que les rotations sont introduites, quelques instances supplémentaires doivent être remplacées en plus de celle unique du numérateur de l'équation 2.

Maintenant, comment calculer t se pose. Le vecteur tangent est un vecteur perpendiculaire à la normale de collision qui fait face plus à la normale. Cela peut paraître déroutant - ne vous inquiétez pas, j'ai un diagramme!

Ci-dessous, vous pouvez voir le vecteur tangent perpendiculaire à la normale. Le vecteur tangent peut pointer à gauche ou à droite. Pour la gauche serait "plus loin" de la vitesse relative. Cependant, il est défini comme la perpendiculaire à la normale qui pointe plus vers la vitesse relative.


Vecteurs de différents types dans le délai d'une collision de corps rigides.

Comme indiqué brièvement ci-dessus, le frottement sera un vecteur opposé au vecteur tangent. Cela signifie que la direction dans laquelle appliquer le frottement peut être directement calculée, puisque le vecteur normal a été trouvé lors de la détection de collision..

Sachant cela, le vecteur tangent est (où n la collision est-elle normale):

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

Tout ce qui reste à résoudre pour jt, la magnitude du frottement, est de calculer la valeur directement en utilisant les équations ci-dessus. Une fois que cette valeur est calculée, il y a des éléments très délicats qui seront couverts sous peu. Ce n'est donc pas la dernière chose nécessaire dans notre résolveur de collision:

 // Recalcule la vitesse relative après l'application de l'impulsion normale // (impulsion du premier article, ce code vient // immédiatement après dans la même fonction de résolution) Vec2 rv = VB - VA // Résolution pour le vecteur de tangente Vec2 tangent = rv - Dot (va, normale) * tangente normale.Normalize () // Résoudre pour que la magnitude s'applique le long du vecteur de frottement float jt = -Dot (va, t) jt = jt / (1 / MassA + 1 / MassB)

Le code ci-dessus suit l'équation 2 directement. Encore une fois, il est important de réaliser que le vecteur de frottement pointe dans la direction opposée à notre vecteur tangent et, en tant que tel, nous devons appliquer un signe négatif lorsque nous ponctions la vitesse relative le long de la tangente pour résoudre la vitesse relative le long du vecteur tangent. Ce signe négatif renverse la vitesse tangente et pointe soudainement dans la direction dans laquelle le frottement doit être approché comme suit..

La loi de coulomb

La loi de Coulomb est la partie de la simulation de friction qui pose problème à la plupart des programmeurs. J'ai moi-même dû faire pas mal d'études pour trouver la bonne façon de le modéliser. L'astuce est que la loi de Coulomb est une inégalité.

États de friction de Coulomb:

\ [Équation 3: \\
F_f <= \mu F_n \]

En d’autres termes, la force de frottement est toujours inférieure ou égale à la force normale multipliée par une constante μ (dont la valeur dépend des matériaux des objets).

La force normale est juste notre vieux j magnitude multipliée par la normale de collision. Donc, si notre résolu jt (représentant la force de frottement) est inférieur à μ fois la force normale, alors nous pouvons utiliser notre jt magnitude comme frottement. Sinon, nous devons utiliser nos temps de force normaux μ au lieu. Ce cas "else" est une forme de blocage de notre frottement en dessous d'une valeur maximale, le maximum étant le temps de force normal μ.

La loi de Coulomb a pour but de réaliser cette procédure de serrage. Ce bridage s’avère être la partie la plus difficile de la simulation de frottement pour une résolution basée sur des impulsions afin de trouver de la documentation n’importe où - jusqu’à présent, au moins! La plupart des livres blancs que j'ai pu trouver sur le sujet ignoraient totalement le frottement ou s'arrêtaient et mettaient en œuvre des procédures de serrage inappropriées (ou inexistantes). Espérons que vous comprenez maintenant qu'il est important de bien comprendre cette pièce..

Laissons juste sortir le serrage en un tour avant d’expliquer quoi que ce soit. Ce bloc de code suivant est l'exemple de code précédent avec la procédure de serrage terminée et l'application d'une impulsion de friction:

 // Recalcule la vitesse relative après l'application de l'impulsion normale // (impulsion du premier article, ce code vient // immédiatement après dans la même fonction de résolution) Vec2 rv = VB - VA // Résolution pour le vecteur de tangente Vec2 tangent = rv - Point (va, normale) * tangente normale.Normalisez () // Résoudre pour que la magnitude s'applique le long du vecteur de frottement float jt = -Dot (va, t) jt = jt / (1 / MassA + 1 / MassB) // PythagoreanSolve = A ^ 2 + B ^ 2 = C ^ 2, résoudre pour C donné A et B // Utilisez pour approximer mu les coefficients de frottement donnés de chaque flotteur corporel mu = PythagoreanSolve (A-> staticFriction, B-> staticFriction) // Pincez la magnitude du frottement et créez le vecteur d’impulsion Vec2 frictionImpulse if (abs (jt) < j * mu) frictionImpulse = jt * t else  dynamicFriction = PythagoreanSolve( A->dynamicFriction, B-> dynamicFriction) frictionImpulse = -j * t * dynamiqueFriction // Appliquer A-> vitesse - = (1 / A-> masse) * frictionImpulse B-> vitesse + = (1 / B-> masse) * frictionImpulsion

J'ai décidé d'utiliser cette formule pour résoudre les coefficients de frottement entre deux corps, étant donné un coefficient pour chaque corps:

\ [Équation 4: \\
Friction = \ sqrt [] Friction ^ 2_A + Friction ^ 2_B \]

En fait, j'ai vu quelqu'un d'autre faire cela dans son propre moteur physique et le résultat m'a plu. Une moyenne des deux valeurs fonctionnerait parfaitement pour éliminer l'utilisation de la racine carrée. En réalité, toute forme de sélection du coefficient de frottement fonctionnera; c'est ce que je préfère. Une autre option consisterait à utiliser une table de correspondance dans laquelle le type de chaque corps est utilisé comme index dans une table 2D..

Il est important que la valeur absolue de jt est utilisé dans la comparaison, puisque la comparaison bloque théoriquement des magnitudes brutes inférieures à un seuil. Puisque j est toujours positif, il doit être retourné afin de représenter un bon vecteur de frottement, dans le cas où le frottement dynamique est utilisé.

Friction statique et dynamique

Dans le dernier extrait de code, les frictions statiques et dynamiques ont été introduites sans aucune explication! Je consacrerai toute cette section à expliquer la différence et la nécessité de ces deux types de valeurs..

Quelque chose d'intéressant se produit avec le frottement: cela nécessite une "énergie d'activation" pour que les objets se mettent à bouger au repos complet. Lorsque deux objets reposent l'un sur l'autre dans la vie réelle, il faut beaucoup d'énergie pour le pousser et le faire bouger. Cependant, une fois que vous obtenez quelque chose qui glisse, il est souvent plus facile de le garder à partir de là..

Cela est dû au fonctionnement du frottement au niveau microscopique. Une autre image aide ici:


Vue microscopique de ce qui cause l'énergie d'activation due au frottement.

Comme vous pouvez le constater, les petites déformations entre les surfaces sont en réalité le principal responsable de la friction. Lorsqu'un objet est au repos sur un autre, des déformations microscopiques reposent entre les objets, s'emboîtant les unes dans les autres. Ceux-ci doivent être cassés ou séparés pour que les objets glissent les uns contre les autres.

Nous avons besoin d'un moyen de modéliser cela dans notre moteur. Une solution simple consiste à fournir à chaque type de matériau deux valeurs de frottement: une pour le statique et une pour le dynamique..

Le frottement statique est utilisé pour serrer notre jt ordre de grandeur. Si le résolu jt la magnitude est suffisamment basse (en dessous de notre seuil), alors nous pouvons supposer que l’objet est au repos, ou presque, au repos et utilise la totalité jt comme une impulsion.

Sur le revers, si notre résolu jt au-dessus du seuil, on peut supposer que l'objet a déjà cassé "l'énergie d'activation", et dans une telle situation, une impulsion de frottement inférieure est utilisée, qui est représentée par un coefficient de frottement inférieur et un calcul d'impulsion légèrement différent.


Scène

En supposant que vous n’ayez omis aucune partie de la section Friction, bravo! Vous avez terminé la partie la plus difficile de toute cette série (à mon avis).

le Scène class agit comme un conteneur pour tout ce qui concerne un scénario de simulation physique. Il appelle et utilise les résultats de toutes les phases, contient tous les corps rigides, exécute des contrôles de collision et résout les appels. Il intègre également tous les objets vivants. La scène s'interface également avec l'utilisateur (comme dans le programmeur utilisant le moteur physique).

Voici un exemple de ce à quoi une structure de scène peut ressembler:

 classe Scene public: Scene (gravité Vec2, dt réel); ~ Scène (); void SetGravity (gravité Vec2) void SetDT (real dt) Body * CreateBody (forme ShapeInterface *, BodyDef def) // Insère un corps dans la scène et initialise le corps (calcule la masse). // void InsertBody (Body * body) // Supprime un corps de la scène void RemoveBody (Body * body) // Met à jour la scène avec un seul timestep void Étape (void) float GetDT (void) LinkedList * GetBodyList (void) Vec2 GetGravity (void) void QueryAABB (CallBackQuery cb, const AABB et aabb) void QueryPoint (CallBackQuery cb, const Point2 & point) private: float dt // durée en seconde float inv_dt // inversion de temps dans les années reliées Broadphase;

Il n’ya rien de particulièrement complexe dans la Scène classe. L'idée est de permettre à l'utilisateur d'ajouter et de supprimer facilement des corps rigides. le BodyDef est une structure qui contient toutes les informations sur un corps rigide et peut être utilisée pour permettre à l'utilisateur d'insérer des valeurs sous forme d'une sorte de structure de configuration.

L’autre fonction importante est Étape(). Cette fonction effectue une série de contrôles de collision, de résolution et d'intégration. Cela doit être appelé depuis la boucle timestepping décrite dans le deuxième article de cette série..

Pour interroger un point ou un AABB, vous devez vérifier quels objets se heurtent réellement à un pointeur ou à un AABB dans la scène. Cela permet à la logique liée au gameplay de voir comment les choses sont placées dans le monde..


Table de saut

Nous avons besoin d’un moyen simple de choisir la fonction de collision à appeler, en fonction du type de deux objets différents..

À ma connaissance, en C ++, il y a deux manières principales: une double répartition et une table de saut 2D. Lors de mes propres tests, j’ai trouvé la table de saut 2D supérieure, alors j’entrerai dans les détails sur la façon de la mettre en œuvre. Si vous envisagez d’utiliser un langage autre que le C ou le C ++, je suis sûr qu’un tableau de fonctions ou d’objets foncteur peut être construit de la même manière qu’une table de pointeurs de fonctions (c’est une autre raison pour laquelle j’ai choisi de parler de tables de sauts plutôt que d’autres options. qui sont plus spécifiques à C ++).

Une table de saut en C ou C ++ est une table de pointeurs de fonction. Des index représentant des noms arbitraires ou des constantes sont utilisés pour indexer dans la table et appeler une fonction spécifique. L'utilisation pourrait ressembler à ceci pour une table de saut 1D:

 enum Animal Lapin Canard Lion; const void (* talk) (void) [] = RabbitTalk, DuckTalk, LionTalk,; // Appelle une fonction de la table avec 1D Virtual Dispatch Talk [Rabbit] () // appelle la fonction RabbitTalk

Le code ci-dessus imite en réalité ce que le langage C ++ lui-même implémente avec appels de fonction virtuels et héritage. Cependant, C ++ implémente uniquement les appels virtuels unidimensionnels. Un tableau 2D peut être construit à la main.

Voici quelques psuedocodes pour une table de saut 2D pour appeler des routines de collision:

 collisionCallbackArray = AABBvsAABB AABBvsCircle CirclevsAABB CirclevsCircle // Appelez une routine de collsion pour la détection de collision entre A et B // deux collisionneurs sans connaître leur type exact de collisionneur // le type peut être de type AABB ou Circle collisionCallbackArray [type A ->] -> type] (A, B)

Et là nous l'avons! Les types réels de chaque collisionneur peuvent être utilisés pour indexer dans un tableau 2D et choisir une fonction pour résoudre les collisions.

Notez cependant que AABBvsCircle et CirclevsAABB sont presque des doublons. Ceci est nécessaire! La normale doit être inversée pour l'une de ces deux fonctions, et c'est la seule différence entre elles. Cela permet une résolution cohérente des collisions, quelle que soit la combinaison d'objets à résoudre..


Conclusion

À ce jour, nous avons couvert une quantité énorme de sujets lors de la mise en place d’un moteur de physique des corps rigides entièrement à partir de rien! La résolution des collisions, la friction et l'architecture du moteur sont tous les sujets abordés jusqu'à présent. Un moteur physique parfaitement adapté à de nombreux jeux bidimensionnels de niveau de production peut être construit avec les connaissances présentées jusqu'à présent dans cette série..

Dans l’avenir, j’ai l’intention d’écrire un autre article entièrement consacré à une fonction très désirable: la rotation et l’orientation. Les objets orientés sont extrêmement attrayants à regarder interagir les uns avec les autres, et sont la dernière pièce que notre moteur physique personnalisé nécessite.

La résolution de rotation s'avère être assez simple, bien que la détection de collision prenne un coup en complexité. Bonne chance jusqu'à la prochaine fois et posez des questions ou postez des commentaires ci-dessous.!