So erstellen Sie eine benutzerdefinierte 2D-Physik-Engine Reibung, Szene und Sprungtabelle

In den ersten beiden Tutorials dieser Serie behandelte ich die Themen Impulse Resolution und Core Architecture. Nun ist es an der Zeit, einige der letzten Feinheiten unserer impulsbasierten 2D-Physik-Engine hinzuzufügen.

Die Themen, die wir in diesem Artikel behandeln werden, sind:

  • Reibung
  • Szene
  • Kollisionssprung-Tabelle

Ich habe dringend empfohlen, in den beiden letzten Artikeln der Serie nachzulesen, bevor ich mich mit diesem beschäftige. In diesem Artikel sind einige wichtige Informationen in den vorherigen Artikeln enthalten.

Hinweis: Obwohl dieses Tutorial mit C ++ geschrieben wurde, sollten Sie in der Lage sein, in fast jeder Spieleentwicklungsumgebung dieselben Techniken und Konzepte anzuwenden.


Video-Demo

Hier ist eine kurze Demonstration dessen, worauf wir in diesem Teil hinarbeiten:


Reibung

Reibung ist ein Teil der Kollisionsauflösung. Reibung übt immer eine Kraft auf Objekte in der Richtung aus, die der Bewegung entgegengesetzt ist, in der sie sich bewegen sollen.

Im wirklichen Leben ist Reibung eine unglaublich komplexe Wechselwirkung zwischen verschiedenen Substanzen, und um sie zu modellieren, werden weitreichende Annahmen und Annäherungen gemacht. Diese Annahmen werden in der Mathematik impliziert und sind in der Regel so etwas wie "die Reibung kann durch einen einzelnen Vektor angenähert werden" - ähnlich wie bei der Dynamik starrer Körper reale Interaktionen simuliert werden, indem angenommen wird, dass Körper mit gleichmäßiger Dichte nicht deformiert werden können.

Schauen Sie sich die Video-Demo aus dem ersten Artikel dieser Serie kurz an:

Die Wechselwirkungen zwischen den Körpern sind sehr interessant und das Springen bei Kollisionen fühlt sich realistisch an. Sobald die Objekte auf der festen Plattform gelandet sind, werden sie einfach weggedrückt und driften von den Bildschirmrändern ab. Dies ist auf mangelnde Reibungssimulation zurückzuführen.

Wieder Impulse?

Wie Sie sich aus dem ersten Artikel dieser Serie erinnern sollten, einen bestimmten Wert, j, repräsentierte die Größe eines Impulses, der erforderlich ist, um das Eindringen zweier Objekte während einer Kollision zu trennen. Diese Größe kann als bezeichnet werden normal oder jN wie es verwendet wird, um die Geschwindigkeit entlang der Kollisionsnormalen zu verändern.

Das Einbeziehen einer Reibungsantwort beinhaltet das Berechnen einer anderen Größe, die als bezeichnet wird jtangent oder jT. Reibung wird als Impuls modelliert. Diese Größe ändert die Geschwindigkeit eines Objekts entlang des negativen Tangentenvektors der Kollision oder anders ausgedrückt entlang des Reibungsvektors. In zwei Dimensionen ist das Lösen dieses Reibungsvektors ein lösbares Problem, in 3D wird das Problem jedoch viel komplexer.

Reibung ist ziemlich einfach, und wir können unsere vorherige Gleichung für verwenden j, außer wir werden alle Fälle des Normalen ersetzen n mit einem Tangentenvektor t.

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

Ersetzen n mit t:

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

Obwohl nur eine einzige Instanz von n wurde durch ersetzt t In dieser Gleichung müssen nach der Einführung von Rotationen einige weitere Instanzen außer der einzigen im Zähler von Gleichung 2 ersetzt werden.

Nun geht es darum zu berechnen t entsteht. Der Tangentenvektor ist ein Vektor senkrecht zur Kollisionsnormalen, der mehr der Normalen zugewandt ist. Das hört sich vielleicht verwirrend an - keine Sorge, ich habe ein Diagramm!

Unten sehen Sie den Tangentenvektor senkrecht zur Normalen. Der Tangentenvektor kann entweder nach links oder nach rechts zeigen. Links wäre von der Relativgeschwindigkeit "mehr weg". Es ist jedoch definiert als das Senkrechte zu der Normalen, die auf die Relativgeschwindigkeit gerichtet ist.


Vektoren verschiedener Art im Zeitrahmen einer Kollision starrer Körper.

Wie bereits erwähnt, ist die Reibung ein Vektor, der dem Tangentenvektor entgegengesetzt ist. Dies bedeutet, dass die Richtung, in der Reibung angewendet werden soll, direkt berechnet werden kann, da der Normalenvektor während der Kollisionserkennung gefunden wurde.

Wenn Sie dies wissen, ist der Tangentenvektor (wo n ist die Kollision normal):

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

All das bleibt noch zu lösen jt, Die Größe der Reibung besteht darin, den Wert direkt unter Verwendung der obigen Gleichungen zu berechnen. Nachdem dieser Wert berechnet wurde, gibt es einige sehr knifflige Teile, die in Kürze behandelt werden. Daher ist dies nicht das letzte, was in unserem Kollisionsauflöser benötigt wird:

 // Relative Geschwindigkeit nach Anlegen des normalen Impulses // neu berechnen (Impuls aus dem ersten Artikel, dieser Code kommt // direkt in derselben Auflösungsfunktion). Vec2 rv = VB - VA // Lösung für den Tangentenvektor Vec2 Tangente = rv - Punkt (rv, normal) * normaler Tangens.Normalize () // Auflösungsbetrag auflösen, der entlang des Reibungsvektors Float angewendet werden soll jt = -Dot (rv, t) jt = jt / (1 / MassA + 1 / MassB)

Der obige Code folgt direkt auf Gleichung 2. Es ist wiederum wichtig zu erkennen, dass der Reibungsvektor in die entgegengesetzte Richtung unseres Tangentenvektors zeigt, und als solches müssen wir ein negatives Vorzeichen anwenden, wenn wir die relative Geschwindigkeit entlang der Tangente punktieren, um die relative Geschwindigkeit entlang des Tangentenvektors zu lösen. Dieses negative Vorzeichen kehrt die Tangentengeschwindigkeit um und zeigt plötzlich in die Richtung, in der die Reibung angenähert werden sollte.

Coulomb-Gesetz

Das Coulombsche Gesetz ist der Teil der Reibungssimulation, mit dem die meisten Programmierer Probleme haben. Ich selbst musste ziemlich viel studieren, um herauszufinden, wie man es richtig modelliert. Der Trick ist, dass das Coulombsche Gesetz eine Ungleichheit ist.

Coulomb-Reibungszustände:

\ [Gleichung 3: \\
F_f <= \mu F_n \]

Mit anderen Worten ist die Reibungskraft immer kleiner oder gleich der Normalkraft multipliziert mit einer Konstanten μ (deren Wert von den Materialien der Objekte abhängt).

Die normale Kraft ist nur unsere alte j Betrag multipliziert mit der Kollisionsnormalen. Also wenn unser gelöst ist jt (repräsentiert die Reibungskraft) ist kleiner als μ mal die normale Kraft, dann können wir unsere nutzen jt Größe als Reibung. Wenn nicht, müssen wir unsere normalen Kraftzeiten verwenden μ stattdessen. Dieser "andere" Fall ist eine Form des Klemmens unserer Reibung unter einem maximalen Wert, wobei das Maximum die normalen Kraftzeiten ist μ.

Der Kernpunkt des Coulomb-Gesetzes ist die Durchführung dieses Spannvorgangs. Diese Klemmung erweist sich als der schwierigste Teil der Reibungssimulation für die impulsbasierte Auflösung, um Dokumentation überall zu finden - bis jetzt zumindest! Die meisten White Papers, die ich zu diesem Thema finden konnte, haben entweder die Reibung völlig übersprungen oder wurden abgebrochen und unsachgemäße (oder nicht vorhandene) Spannvorgänge implementiert. Hoffentlich haben Sie inzwischen Verständnis dafür, dass es wichtig ist, diesen Teil richtig zu machen.

Lassen Sie uns einfach die Klemmung in einem Zug austeilen, bevor Sie alles erklären. Dieser nächste Codeblock ist das vorherige Codebeispiel mit dem vollständigen Spannvorgang und der Anwendung des Reibungsimpulses:

 // Relative Geschwindigkeit nach Anlegen des normalen Impulses // neu berechnen (Impuls aus dem ersten Artikel, dieser Code kommt // direkt in derselben Auflösungsfunktion). Vec2 rv = VB - VA // Lösung für den Tangentenvektor Vec2 Tangente = rv - Punkt (rv, normal) * normaler Tangens.Normalize () // Lösung für die Stärke, die entlang des Reibungsvektors Float angewendet werden soll jt = -Dot (rv, t) jt = jt / (1 / MassA + 1 / MassB) // PythagoreanSolve = A ^ 2 + B ^ 2 = C ^ 2, Lösung für C bei A und B // Zur Annäherung an mu gegebene Reibungskoeffizienten jedes Körperschwimmers mu = PythagoreanSolve (A-> staticFriction, B-> staticFriction) Reibungsgröße klemmen und Impulsvektor Vec2 Reibungsimpuls erstellen, wenn (abs (jt) < j * mu) frictionImpulse = jt * t else  dynamicFriction = PythagoreanSolve( A->dynamicFriction, B-> dynamicFriction) Reibungsimpuls = -j * t * DynamikFriction // Übernehmen A-> Geschwindigkeit - = (1 / A-> Masse) * Reibungsimpuls B-> Geschwindigkeit + = (1 / B-> Masse) * Reibungsimpuls

Ich entschied mich für diese Formel, um die Reibungskoeffizienten zwischen zwei Körpern zu ermitteln, wobei für jeden Körper ein Koeffizient gegeben wurde:

\ [Gleichung 4: \\
Reibung = \ sqrt [] Reibung ^ 2_A + Reibung ^ 2_B \]

Ich habe tatsächlich gesehen, dass jemand anderes dies in seiner eigenen Physik-Engine getan hat, und das Ergebnis hat mir gefallen. Ein Durchschnitt der beiden Werte würde perfekt funktionieren, um die Verwendung der Quadratwurzel zu beseitigen. Wirklich, jede Form der Auswahl des Reibungskoeffizienten wird funktionieren; das ist mir gerade lieber. Eine andere Option wäre die Verwendung einer Nachschlagetabelle, bei der der Typ jedes Körpers als Index für eine 2D-Tabelle verwendet wird.

Es ist wichtig, dass der absolute Wert von jt wird im Vergleich verwendet, da der Vergleich theoretisch rohe Größen unter einen bestimmten Schwellenwert einklemmt. Schon seit j ist immer positiv, es muss umgedreht werden, um einen korrekten Reibungsvektor darzustellen, falls dynamische Reibung verwendet wird.

Statische und dynamische Reibung

Im letzten Code-Snippet wurden statische und dynamische Reibung ohne Erklärung eingeführt! In diesem ganzen Abschnitt werde ich den Unterschied zwischen diesen beiden Arten von Werten und deren Notwendigkeit erklären.

Mit Reibung geschieht etwas Interessantes: Es erfordert eine "Aktivierungsenergie", damit sich Objekte bei vollständiger Ruhe in Bewegung setzen können. Wenn zwei Objekte im wirklichen Leben aufeinander ruhen, ist eine ziemlich große Menge Energie erforderlich, um ein Objekt voranzutreiben und in Bewegung zu setzen. Sobald Sie jedoch etwas rutschen, ist es oft einfacher, es von da an zu rutschen.

Dies liegt daran, wie die Reibung auf mikroskopischer Ebene funktioniert. Ein anderes Bild hilft hier:


Mikroskopische Ansicht der Ursachen der Aktivierungsenergie durch Reibung.

Wie Sie sehen, sind die kleinen Verformungen zwischen den Oberflächen tatsächlich der Hauptverursacher, der überhaupt Reibung erzeugt. Wenn ein Objekt auf einem anderen ruht, ruhen mikroskopische Verformungen zwischen den Objekten und greifen ineinander. Diese müssen gebrochen oder getrennt werden, damit die Objekte gegeneinander gleiten können.

Wir brauchen eine Möglichkeit, dies in unserem Motor zu modellieren. Eine einfache Lösung besteht darin, jede Art von Material mit zwei Reibungswerten zu versehen: einen für statische und einen für dynamischen.

Die Haftreibung wird verwendet, um unsere Kraft zu klemmen jt Größe. Wenn das gelöst ist jt Ist die Größe niedrig genug (unter unserer Schwelle), können wir davon ausgehen, dass sich das Objekt im Ruhezustand befindet oder nahezu als Ruhezustand dient und das gesamte Objekt verwendet jt als Impuls.

Auf der anderen Seite, wenn unser gelöst ist jt über der Schwelle liegt, kann davon ausgegangen werden, dass das Objekt bereits die "Aktivierungsenergie" gebrochen hat, und in einer solchen Situation wird ein niedrigerer Reibungsimpuls verwendet, der durch einen kleineren Reibungskoeffizienten und eine etwas andere Impulsberechnung dargestellt wird.


Szene

Angenommen, Sie haben keinen Teil des Friction-Abschnitts übersprungen, gut gemacht! Sie haben den schwierigsten Teil dieser gesamten Serie (meiner Meinung nach) abgeschlossen..

Das Szene Die Klasse dient als Container für alles, was ein physikalisches Simulationsszenario beinhaltet. Es ruft die Ergebnisse einer breiten Phase auf und verwendet sie, enthält alle starren Körper, führt Kollisionsprüfungen durch und ruft eine Auflösung auf. Es integriert auch alle Live-Objekte. Die Szene ist auch mit dem Benutzer verbunden (wie im Programmierer, der die Physik-Engine verwendet)..

Hier ein Beispiel, wie eine Szenenstruktur aussehen kann:

 Klasse Szene public: Szene (Vec2 Schwerkraft, Real dt); ~ Szene (); void SetGravity (Vec2-Schwerkraft) void SetDT (real dt) Body * CreateBody (ShapeInterface * Shape, BodyDef def) // Fügt einen Körper in die Szene ein und initialisiert den Körper (berechnet die Masse). // void InsertBody (body * body) // Löscht einen Body aus der Szene. void RemoveBody (body * body) // Aktualisiert die Szene mit einem einzelnen Zeitschritt. void Schritt (void) float GetDT (void) LinkedList * GetBodyList (void) Vec2 GetGravity (void) void QueryAABB (CallBackQuery cb, const AABB und aabb) void QueryPoint (CallBackQuery cb, const Point2 & point) privat: float dt // Zeitschritt in Sekunden float inv_dt // Inverse Zeitschritt in Sekunden LinkedList Breitphase;

Es gibt nichts besonders Komplexes an der Szene Klasse. Die Idee ist, dem Benutzer das einfache Hinzufügen und Entfernen starrer Körper zu ermöglichen. Das BodyDef ist eine Struktur, die alle Informationen zu einem starren Körper enthält und verwendet werden kann, um dem Benutzer das Einfügen von Werten als eine Art Konfigurationsstruktur zu ermöglichen.

Die andere wichtige Funktion ist Schritt(). Diese Funktion führt eine einzige Runde von Kollisionsprüfungen, Auflösung und Integration durch. Dies sollte aus der Zeitvorbereitungsschleife des zweiten Artikels dieser Serie heraus aufgerufen werden.

Beim Abfragen eines Punkts oder einer AABB wird geprüft, welche Objekte tatsächlich mit einem Zeiger oder einer AABB innerhalb der Szene kollidieren. Dies macht es der Gameplay-Logik leicht, zu sehen, wie sich die Dinge in der Welt befinden.


Sprungtisch

Wir brauchen eine einfache Möglichkeit, anhand des Typs zweier verschiedener Objekte herauszufinden, welche Kollisionsfunktion aufgerufen werden soll.

In C ++ gibt es zwei Hauptmöglichkeiten, die mir bekannt sind: Double Dispatch und eine 2D-Sprungtabelle. In meinen persönlichen Tests fand ich die 2D-Sprungtabelle überlegen, so dass ich ausführlich auf die Implementierung eingehen werde. Wenn Sie planen, eine andere Sprache als C oder C ++ zu verwenden, bin ich mir sicher, dass ein Array von Funktionen oder Funktionsobjekten ähnlich wie eine Tabelle von Funktionszeigern erstellt werden kann (ein anderer Grund, weshalb ich mich eher für Sprungtabellen als für andere Optionen entschieden habe das sind spezifischer für C ++).

Eine Sprungtabelle in C oder C ++ ist eine Tabelle von Funktionszeigern. Indizes, die beliebige Namen oder Konstanten darstellen, werden verwendet, um in die Tabelle zu indexieren und eine bestimmte Funktion aufzurufen. Die Verwendung könnte für eine 1D-Sprungtabelle so aussehen:

 enum Animal Rabbit Duck Lion; const void (* talk) (void) [] = RabbitTalk, DuckTalk, LionTalk,; // Aufruf einer Funktion aus der Tabelle mit 1D Virtual Dispatch Talk [Rabbit] () // Ruft die RabbitTalk-Funktion auf

Der obige Code ahmt tatsächlich nach, was die C ++ - Sprache selbst implementiert virtuelle Funktionsaufrufe und Vererbung. C ++ implementiert jedoch nur eindimensionale virtuelle Aufrufe. Eine 2D-Tabelle kann von Hand erstellt werden.

Hier ist ein Pseudo-Code für eine 2D-Sprungtabelle, um Kollisionsroutinen aufzurufen:

 collisionCallbackArray = AABBvsAABB AABBvsCircle CirclevsAABB CirclevsCircle // Ruft eine Collsionsroutine für die Kollisionserkennung zwischen A und B auf. // zwei Collider, ohne zu wissen, ob der genaue Collider-Typ // Typ AABB oder Circle CollisionCallbackArray [A-> Typ] ist -> Typ] (A, B)

Und da haben wir es! Die tatsächlichen Typen jedes Colliders können verwendet werden, um in ein 2D-Array zu indizieren und eine Funktion auszuwählen, um die Kollision aufzulösen.

Beachten Sie jedoch das AABBvsCircle und CirclevsAABB sind fast Duplikate. Das ist notwendig! Das Normal muss für eine dieser beiden Funktionen umgedreht werden, und das ist der einzige Unterschied zwischen ihnen. Dies ermöglicht eine konsistente Kollisionsauflösung, unabhängig von der zu lösenden Objektkombination.


Fazit

Inzwischen haben wir eine Vielzahl von Themen behandelt, um eine benutzerdefinierte Physik-Engine für starre Körper völlig neu zu erstellen. Kollisionslösung, Reibung und Motorarchitektur sind alle Themen, die bisher behandelt wurden. Eine völlig erfolgreiche Physik-Engine, die für viele zweidimensionale Spiele auf Produktionsniveau geeignet ist, kann mit den bisherigen Kenntnissen dieser Serie erstellt werden.

Wenn ich in die Zukunft blicken möchte, plane ich, einen weiteren Artikel zu schreiben, der sich ganz einem wünschenswerten Feature widmet: Rotation und Orientierung. Orientierte Objekte sind äußerst attraktiv, um zu sehen, wie sie miteinander interagieren, und sie sind das letzte Stück, das unsere benutzerdefinierte Physik-Engine benötigt.

Die Auflösung der Rotation erweist sich als ziemlich einfach, obwohl die Kollisionserkennung einen Komplexitätsverlust mit sich bringt. Viel Glück bis zum nächsten Mal, und bitte stellen Sie Fragen oder schreiben Sie Kommentare!