In diesem Teil meiner Serie zur Erstellung einer benutzerdefinierten 2D-Physik-Engine für Ihre Spiele werden wir der Impulsauflösung, die wir im ersten Teil erarbeitet haben, weitere Funktionen hinzufügen. Wir werden uns insbesondere mit der Integration, der Zeitplanung, dem modularen Design unseres Codes und der Erkennung von Kollisionen mit breiter Phase befassen.
Im letzten Beitrag dieser Serie habe ich das Thema Impulsauflösung behandelt. Lesen Sie das zuerst, wenn Sie es noch nicht getan haben!
Lassen Sie uns direkt auf die in diesem Artikel behandelten Themen eingehen. Diese Themen sind alles Notwendige einer halb anständigen Physik-Engine. Daher ist jetzt der geeignete Zeitpunkt, um weitere Funktionen über die Kernauflösung des letzten Artikels hinaus aufzubauen.
Die Integration ist ganz einfach zu implementieren, und es gibt sehr viele Bereiche im Internet, die gute Informationen für die iterative Integration liefern. In diesem Abschnitt wird meistens beschrieben, wie eine ordnungsgemäße Integrationsfunktion implementiert wird, und wenn gewünscht auf ein paar andere Stellen zum Lesen hingewiesen.
Zuerst sollte man wissen, was Beschleunigung eigentlich ist. Newtons zweites Gesetz besagt:
\ [Gleichung 1: \\
F = ma \]
Dies besagt, dass die Summe aller auf ein Objekt einwirkenden Kräfte gleich der Masse dieses Objekts ist m
multipliziert mit seiner Beschleunigung ein
. m
ist in Kilogramm, ein
ist in Metern / Sekunde und F
ist in Newton.
Diese Gleichung ein wenig umstellen, um sie zu lösen ein
ergibt:
\ [Gleichung 2: \\
a = \ frac F m \\
\deshalb\\
a = F * \ frac 1 m \]
Der nächste Schritt umfasst die Verwendung der Beschleunigung, um ein Objekt von einem Ort zu einem anderen zu verschieben. Da ein Spiel in diskreten separaten Frames in einer illusionenartigen Animation angezeigt wird, müssen die Positionen jeder Position in diesen diskreten Schritten berechnet werden. Weitere Informationen zu diesen Gleichungen finden Sie in: Erin Cattos Integrationsdemo von GDC 2009 und Hannus Ergänzung zu symplectic Euler für mehr Stabilität in Umgebungen mit niedrigem FPS.
Die explizite Euler-Integration (ausgesprochen "Öler") wird im folgenden Snippet gezeigt x
ist Position und v
ist Geschwindigkeit. Bitte beachte, dass 1 / m * F
ist Beschleunigung, wie oben erklärt:
// Expliziter Euler x + = v * dt v + = (1 / m * F) * dt
dt
hier bezieht sich auf Deltazeit. Δ ist das Symbol für Delta und kann buchstäblich als "change in" oder als Δ gelesen werdent
. Also wann immer du siehst dt
kann als "zeitliche Änderung" gelesen werden. dv
wäre "Geschwindigkeitsänderung". Dies wird funktionieren und wird üblicherweise als Ausgangspunkt verwendet. Es hat jedoch numerische Ungenauigkeiten, die wir ohne zusätzlichen Aufwand beseitigen können. Hier ist, was als Symplectic Euler bekannt ist:
// Symplectic Euler v + = (1 / m * F) * dt x + = v * dt
Beachten Sie, dass ich nur die Reihenfolge der beiden Codezeilen geändert habe - siehe den oben genannten Artikel von Hannu.
Dieser Beitrag erklärt die numerischen Ungenauigkeiten von Explicit Euler, aber seien Sie gewarnt, dass er RK4 abdeckt, was ich nicht persönlich empfehle: gafferongames.com: Euler Inaccuracy.
Diese einfachen Gleichungen sind alles, was wir brauchen, um alle Objekte mit linearer Geschwindigkeit und Beschleunigung zu bewegen.
Da Spiele in diskreten Zeitintervallen angezeigt werden, muss es möglich sein, die Zeit zwischen diesen Schritten kontrolliert zu steuern. Haben Sie jemals ein Spiel gesehen, das mit unterschiedlichen Geschwindigkeiten abläuft, je nachdem auf welchem Computer es gespielt wird? Dies ist ein Beispiel für ein Spiel, dessen Geschwindigkeit von der Fähigkeit des Computers abhängt, das Spiel auszuführen.
Wir brauchen einen Weg, um sicherzustellen, dass unsere Physik-Engine erst läuft, wenn eine bestimmte Zeitspanne vergangen ist. Auf diese Weise die dt
Bei Berechnungen wird immer die gleiche Anzahl verwendet. Genau das gleiche verwenden dt
Wert in Ihrem Code überall wird tatsächlich Ihre Physik-Engine machen deterministisch, und ist bekannt als a fester Zeitschritt. Das ist eine gute Sache.
Eine deterministische Physik-Engine ist eine Engine, die jedes Mal genau dasselbe tut, wenn sie ausgeführt wird, vorausgesetzt, dass die gleichen Eingaben gemacht werden. Dies ist für viele Arten von Spielen unerlässlich, bei denen das Gameplay sehr genau auf das Verhalten der Physik-Engine abgestimmt sein muss. Dies ist auch für das Debugging Ihrer Physik-Engine unerlässlich, da das Verhalten Ihrer Engine zur Feststellung von Fehlern konsistent sein muss.
Lassen Sie uns zunächst eine einfache Version eines festen Zeitschritts behandeln. Hier ist ein Beispiel:
const float fps = 100 const float dt = 1 / fps float-akkumulator = 0 // in sekundeneinheiten float frameStart = GetCurrentTime () // hauptschleife while (true) const float currentTime = GetCurrentTime () // Die abgelaufene Zeit speichern der letzte Frame begann akkumulator + = currentTime - frameStart () // Zeichne den Start dieses Frames auf frameStart = currentTime while (akkumulator> dt) UpdatePhysics (dt) akkumulator - = dt RenderGame ()
Dies wartet, um das Spiel zu rendern, bis genügend Zeit vergangen ist, um die Physik zu aktualisieren. Die abgelaufene Zeit wird aufgezeichnet und diskret dt
-Zeitgroße Stücke werden aus dem Akkumulator entnommen und von der Physik verarbeitet. Dadurch wird sichergestellt, dass derselbe Wert an die Physik weitergegeben wird, und dass der an die Physik übergebene Wert eine genaue Darstellung der tatsächlichen Zeit ist, die im wirklichen Leben vergeht. Brocken von dt
werden aus dem entfernt Akkumulator
bis zum Akkumulator
ist kleiner als ein dt
Brocken.
Es gibt einige Probleme, die hier behoben werden können. Die erste betrifft, wie lange es dauert, das Physikupdate tatsächlich durchzuführen: Was ist, wenn das Physikupdate zu lange dauert und die Akkumulator
geht jede Spielschleife höher und höher? Dies nennt man die Spirale des Todes. Wenn dies nicht behoben ist, wird Ihr Motor schnell zum Stillstand kommen, wenn Ihre Physik nicht schnell genug ausgeführt werden kann.
Um dieses Problem zu lösen, muss die Engine wirklich nur weniger Physikupdates ausführen, wenn die Akkumulator
wird zu hoch Ein einfacher Weg, dies zu tun, wäre das Festklemmen Akkumulator
unter einem beliebigen Wert.
const float fps = 100 const float dt = 1 / fps float-akkumulator = 0 // in Einheiten Sekunden float frameStart = GetCurrentTime () // main loop while (true) const float currentTime = GetCurrentTime () // Die seit dem verstrichene Zeit speichern letzter Frame begann akkumulator + = currentTime - frameStart () // Zeichne den Start dieses Frames auf. frameStart = currentTime // Vermeide die Todesspirale und klemme dt, wodurch // festgehalten wird, wie oft UpdatePhysics in einem einzigen Spiel aufgerufen werden kann Schleife. if (akkumulator> 0.2f) akkumulator = 0.2f while (akkumulator> dt) UpdatePhysics (dt) akkumulator - = dt RenderGame ()
Wenn nun ein Spiel, das diese Schleife ausführt, aus irgendeinem Grund auf irgendeine Art von Stalling stößt, wird die Physik nicht in einer Todesspirale ertrinken. Das Spiel wird einfach etwas langsamer laufen, wenn es angebracht ist.
Das nächste Problem ist im Vergleich zur Todesspirale relativ gering. Diese Schleife dauert dt
Brocken aus dem Akkumulator
bis zum Akkumulator
ist kleiner als dt
. Das macht Spaß, aber es bleibt immer noch etwas Zeit im Internet Akkumulator
. Dies wirft ein Problem auf.
Angenommen, das Akkumulator
bleibt mit 1/5 a dt
Chunk jeden Frame. Im sechsten Frame der Akkumulator
hat genug verbleibende Zeit, um ein weiteres Physik-Update durchzuführen, als alle anderen Frames. Dies führt dazu, dass etwa jede Sekunde ein Bild einen etwas größeren diskreten Zeitsprung ausführt, und dies kann in Ihrem Spiel sehr auffällig sein.
Um dies zu lösen, ist die Verwendung von lineare Interpolation Wird benötigt. Wenn dies beängstigend klingt, machen Sie sich keine Sorgen - die Implementierung wird gezeigt. Wenn Sie die Implementierung verstehen möchten, stehen online viele Ressourcen für die lineare Interpolation zur Verfügung.
// lineare Interpolation für a von 0 bis 1 // von t1 bis t2 t1 * a + t2 (1.0f - a)
Damit können wir interpolieren (ungefähr), wo wir uns zwischen zwei verschiedenen Zeitintervallen befinden können. Dies kann verwendet werden, um den Status eines Spiels zwischen zwei verschiedenen Physikupdates darzustellen.
Bei linearer Interpolation kann das Rendern einer Engine mit einer anderen Geschwindigkeit als die Physik-Engine ausgeführt werden. Dies ermöglicht einen zarten Umgang mit dem Rest Akkumulator
aus den Physik-Updates.
Hier ist ein vollständiges Beispiel:
const float fps = 100 const float dt = 1 / fps float-akkumulator = 0 // in Einheiten Sekunden float frameStart = GetCurrentTime () // main loop while (true) const float currentTime = GetCurrentTime () // Die seit dem verstrichene Zeit speichern letzter Frame begann akkumulator + = currentTime - frameStart () // Zeichne den Start dieses Frames auf. frameStart = currentTime // Vermeide die Todesspirale und klemme dt, wodurch // festgehalten wird, wie oft UpdatePhysics in einem einzigen Spiel aufgerufen werden kann Schleife. if (akkumulator> 0.2f) akkumulator = 0.2f while (akkumulator> dt) UpdatePhysics (dt) akkumulator - = dt const float alpha = akkumulator / dt; RenderGame (Alpha) void RenderGame (Float-Alpha) für Shape im Spiel // Berechne eine interpolierte Transformation für das Rendern .Render (i)
Hier können alle Objekte im Spiel zu unterschiedlichen Zeitpunkten zwischen diskreten physischen Zeitstempeln gezeichnet werden. Dadurch werden alle Fehler- und Restlaufzeiten ordnungsgemäß verarbeitet. Dies ist tatsächlich etwas hinter dem zurück, was die Physik derzeit gelöst hat, aber wenn man den Lauf des Spiels betrachtet, werden alle Bewegungen durch die Interpolation perfekt geglättet.
Der Spieler wird nie wissen, dass das Rendering immer etwas hinter der Physik liegt, denn der Spieler wird nur wissen, was er sieht, und was er sieht, sind vollkommen glatte Übergänge von einem Frame zu einem anderen.
Sie fragen sich vielleicht: "Warum interpolieren wir nicht von der aktuellen Position zur nächsten?". Ich habe es ausprobiert und es muss das Rendering "erraten" werden, wo sich Objekte in der Zukunft befinden werden. Häufig nehmen Objekte in einer Physik-Engine plötzliche Bewegungsänderungen vor, beispielsweise während einer Kollision. Wenn eine solche plötzliche Bewegungsänderung vorgenommen wird, werden Objekte aufgrund ungenauer Interpolationen in die Zukunft teleportiert.
Es gibt ein paar Dinge, die jedes Physikobjekt benötigen wird. Die spezifischen Dinge, die jedes Physikobjekt benötigt, können sich jedoch von Objekt zu Objekt geringfügig ändern. Eine kluge Methode zum Organisieren all dieser Daten ist erforderlich, und es wird angenommen, dass die geringere Menge an Code zum Erstellen einer solchen Organisation gewünscht wird. In diesem Fall wäre ein modularer Aufbau sinnvoll.
Modulares Design hört sich vielleicht etwas anmaßend oder überkompliziert an, aber es macht Sinn und ist recht einfach. In diesem Zusammenhang bedeutet "modulares Design" nur, dass wir ein Physikobjekt in einzelne Teile aufteilen möchten, damit wir sie nach Belieben verbinden oder trennen können.
Ein Physikkörper ist ein Objekt, das alle Informationen zu einem bestimmten Physikobjekt enthält. Es werden die Form (en) gespeichert, durch die das Objekt dargestellt wird, Massendaten, Transformation (Position, Rotation), Geschwindigkeit, Drehmoment usw. Hier ist was unser Karosserie
sollte aussehen:
Strukturkörper Shape * Shape; Tx umwandeln; Material Material; MassData Mass_Data; Vec2 Geschwindigkeit; Vec2 Kraft; echte gravityScale; ;
Dies ist ein guter Ausgangspunkt für die Gestaltung einer physischen Körperstruktur. Hier werden einige intelligente Entscheidungen getroffen, die zu einer starken Code-Organisation neigen.
Das erste, was zu bemerken ist, ist, dass eine Form mittels eines Zeigers im Körper enthalten ist. Dies stellt eine lose Beziehung zwischen dem Körper und seiner Form dar. Ein Körper kann eine beliebige Form enthalten, und die Form eines Körpers kann nach Belieben ausgetauscht werden. Tatsächlich kann ein Körper durch mehrere Formen dargestellt werden, und ein solcher Körper würde als "Verbund" bezeichnet, da er aus mehreren Formen bestehen würde. (Ich werde in diesem Tutorial keine Composites behandeln.)
Körper- und Formschnittstelle.Das gestalten
selbst ist für das Berechnen von Begrenzungsformen, das Berechnen der Masse basierend auf der Dichte und das Rendern verantwortlich.
Das Massendaten
ist eine kleine Datenstruktur, die massenbezogene Informationen enthält:
struct MassData Float-Masse; float inv_mass; // für Rotationen (in diesem Artikel nicht behandelt) Schwimmkörperträgheit; float inverse_inertia; ;
Es ist schön, alle Massen- und Intertienwerte in einer einzigen Struktur zu speichern. Die Masse sollte niemals von Hand eingestellt werden - Die Masse sollte immer anhand der Form selbst berechnet werden. Masse ist ein eher unintuitiver Wert, und das Einstellen von Hand erfordert viel Zeit. Es ist definiert als:
\ [Gleichung 3: \\ Masse = Dichte * Volumen \]
Wann immer ein Designer eine Form "massiver" oder "schwerer" haben möchte, sollte er die Dichte einer Form ändern. Diese Dichte kann verwendet werden, um die Masse einer Form unter Berücksichtigung ihres Volumens zu berechnen. Dies ist der richtige Weg, um die Situation zu umgehen, da die Dichte nicht durch das Volumen beeinflusst wird und sich während der Laufzeit des Spiels niemals ändert (es sei denn, dies wird speziell mit speziellem Code unterstützt)..
Einige Beispiele für Formen wie AABBs und Kreise finden Sie im vorherigen Tutorial dieser Serie.
All dieses Gerede von Masse und Dichte führt zu der Frage: Wo liegt der Dichtewert? Es liegt innerhalb der Material
Struktur:
struct Material Schwebedichte; Float-Restitution; ;
Sobald die Werte des Materials eingestellt sind, kann dieses Material an die Form eines Körpers übergeben werden, sodass der Körper die Masse berechnen kann.
Das letzte erwähnenswerte ist das gravity_scale
. Das Skalieren der Schwerkraft für verschiedene Objekte ist für das Optimieren des Spiels so oft erforderlich, dass es am besten ist, in jedem Körper einen bestimmten Wert für diese Aufgabe anzugeben.
Einige nützliche Materialeinstellungen für gängige Materialtypen können verwendet werden, um a zu erstellen Material
Objekt aus einem Aufzählungswert:
Rock-Dichte: 0,6 Restitution: 0,1 Holz-Dichte: 0,3 Restitution: 0,2 Metall-Dichte: 1,2 Restitution: 0,05 BouncyBall-Dichte: 0,3 Restitution: 0,8 SuperBall-Dichte: 0,3 Restitution: 0,95 Kissendichte: 0,1 Restitution: 0,2 Statische Dichte: 0,0 Restitution: 0,4
Es gibt noch eine Sache, über die man im sprechen kann Karosserie
Struktur. Es gibt ein Datenelement namens Macht
. Dieser Wert beginnt zu Beginn jedes Physik-Updates bei Null. Andere Einflüsse in der Physik-Engine (wie die Schwerkraft) werden hinzugefügt Vec2
Vektoren in diese Macht
Datenmitglied. Kurz vor der Integration wird die gesamte Kraft zur Berechnung der Beschleunigung des Körpers verwendet und während der Integration verwendet. Nach der Integration dieses Macht
Datenmember wird auf Null gesetzt.
Dies ermöglicht, dass eine beliebige Anzahl von Kräften auf ein Objekt einwirkt, wann immer sie dies für richtig halten, und es muss kein zusätzlicher Code geschrieben werden, wenn neue Arten von Kräften auf Objekte angewendet werden sollen.
Nehmen wir ein Beispiel. Angenommen, wir haben einen kleinen Kreis, der ein sehr schweres Objekt darstellt. Dieser kleine Kreis fliegt im Spiel herum und ist so schwer, dass er andere Objekte nur leicht in die Richtung zieht. Hier ist ein grober Pseudocode, um dies zu demonstrieren:
HeavyObject-Objekt für Körper im Spiel do if (object.CloseEnoughTo (body) object.ApplyForcePullOn (body)
Die Funktion ApplyForcePullOn ()
könnte vielleicht eine kleine Kraft anwenden, um das zu ziehen Karosserie
in Richtung der HeavyObject
, nur wenn der Karosserie
ist nah genug.
Es spielt keine Rolle, wie viele Kräfte dem hinzugefügt werden Macht
eines Körpers, da sie sich zu einem einzigen summierten Kraftvektor für diesen Körper summieren. Dies bedeutet, dass zwei auf denselben Körper wirkende Kräfte sich möglicherweise gegenseitig aufheben können.
Im vorherigen Artikel dieser Serie wurden Kollisionserkennungsroutinen eingeführt. Diese Routinen waren eigentlich etwas anderes als die "schmale Phase". Die Unterschiede zwischen der breiten Phase und der engen Phase lassen sich mit einer Google-Suche relativ leicht untersuchen.
(Kurz gesagt: Wir verwenden eine Kollisionserkennung mit breiter Phase, um herauszufinden, welche Objektpaare verwendet werden könnte kollidieren und dann die Kollisionserkennung in engen Phasen überprüfen, um zu prüfen, ob sie tatsächlich sind kollidieren.)
Ich möchte einen Beispielcode zusammen mit einer Erläuterung der Implementierung einer breiten Phase von \ (O (n ^ 2) \) - Zeit-Komplexitäts-Paar-Berechnungen bereitstellen.
\ (O (n ^ 2) \) bedeutet im Wesentlichen, dass die Zeit, die benötigt wird, um jedes Paar möglicher Kollisionen zu prüfen, vom Quadrat der Anzahl der Objekte abhängt. Es verwendet die Big-O-Notation.Da wir mit Objektpaaren arbeiten, ist es hilfreich, eine Struktur wie folgt zu erstellen:
Strukturpaar Körper * A; Körper * B; ;
In einer breiten Phase sollten eine Reihe möglicher Kollisionen gesammelt und alle darin gespeichert werden Paar
Strukturen. Diese Paare können dann an einen anderen Teil des Motors (die schmale Phase) weitergeleitet und anschließend aufgelöst werden.
Beispiel breite Phase:
// Erzeugt die Paarliste. // Alle vorherigen Paare werden gelöscht, wenn diese Funktion aufgerufen wird. void BroadPhase :: GeneratePairs (void) pairs.clear () // Cache-Speicherplatz für AABBs, die bei der Berechnung // der Begrenzungsbox der einzelnen Form verwendet werden sollen AABB A_aabb AABB B_aabb für (i = bodies.begin (); i! = body .end (); i = i-> next) für (j = bodies.begin (); j! = bodies.end (); j = j-> next) Body * A = & i-> GetData () Body * B = & j-> GetData () // Überspringe die Prüfung mit self, wenn (A == B) weiter A-> ComputeAABB (& A_aabb) B-> ComputeAABB (& B_aabb) if (AABBtoAABB (A_aabb, B_aabb)) pairs.push_back (A, B)
Der obige Code ist ziemlich einfach: Überprüfen Sie jeden Körper gegen jeden Körper und überspringen Sie die Selbsttests.
Es gibt ein Problem aus dem letzten Abschnitt: Viele doppelte Paare werden zurückgegeben! Diese Duplikate müssen aus den Ergebnissen entnommen werden. Wenn Sie keine Sortierbibliothek zur Verfügung haben, müssen Sie sich mit den Sortieralgorithmen vertraut machen. Wenn Sie C ++ verwenden, haben Sie Glück:
// Paare sortieren, um Duplikate freizulegen sort (Paare, Paare.end (), SortPairs); // Warteschlangen-Mannigfaltigkeiten zum Lösen von int i = 0; während ich < pairs.size( )) Pair *pair = pairs.begin( ) + i; uniquePairs.push_front( pair ); ++i; // Skip duplicate pairs by iterating i until we find a unique pair while(i < pairs.size( )) Pair *potential_dup = pairs + i; if(pair->A! = Potential_dup-> B || Paar-> B! = Potential_dup-> A) Pause; ++ i;
Nach dem Sortieren aller Paare in einer bestimmten Reihenfolge kann davon ausgegangen werden, dass alle Paare in der Paare
Container wird alle Duplikate nebeneinander haben. Platzieren Sie alle eindeutigen Paare in einem neuen Container namens uniquePairs
, und die Arbeit mit dem Aussortieren von Duplikaten ist beendet.
Das letzte, was zu erwähnen ist, ist das Prädikat SortPairs ()
. Diese SortPairs ()
Funktion ist das, was tatsächlich zum Sortieren verwendet wird, und es könnte so aussehen:
bool SortPairs (Paar lhs, Paar rhs) if (lhs.A < rhs.A) return true; if(lhs.A == rhs.A) return lhs.B < rhs.B; return false;Die Begriffe
lhs
und rhs
kann als "linke Seite" und "rechte Seite" gelesen werden. Diese Ausdrücke werden häufig verwendet, um sich auf Parameter von Funktionen zu beziehen, bei denen Dinge logisch als linke und rechte Seite einer Gleichung oder eines Algorithmus betrachtet werden können. Schichtung bezieht sich darauf, dass unterschiedliche Objekte niemals miteinander kollidieren. Dies ist der Schlüssel dafür, dass Kugeln von bestimmten Objekten abgefeuert werden und bestimmte andere Objekte nicht beeinflussen. Zum Beispiel möchten Spieler in einem Team, dass ihre Raketen den Feinden schaden, sich aber nicht gegenseitig.
Layering wird am besten mit implementiert Bitmasken - Eine kurze Einführung finden Sie unter Ein kurzes Bitmask-How-To für Programmierer und auf der Wikipedia-Seite. Im Abschnitt Filtering des Box2D-Handbuchs erfahren Sie, wie die Engine Bitmasken verwendet.
Die Schichtung sollte innerhalb der breiten Phase erfolgen. Hier füge ich nur ein fertiges Beispiel aus der breiten Phase ein:
// Erzeugt die Paarliste. // Alle vorherigen Paare werden gelöscht, wenn diese Funktion aufgerufen wird. void BroadPhase :: GeneratePairs (void) pairs.clear () // Cache-Speicherplatz für AABBs, die bei der Berechnung // der Bounding-Box der jeweiligen Form verwendet werden sollen. AABB A_aabb AABB B_aabb für (i = bodies.begin (); i! =odies .end (); i = i-> next) für (j = bodies.begin (); j! = bodies.end (); j = j-> next) Body * A = & i-> GetData () Body * B = & j-> GetData () // Skip-Prüfung mit self, wenn (A == B) continue // Nur übereinstimmende Layer werden berücksichtigt, wenn (! (A-> Layers & B-> Layers) fortgesetzt werden; A-> ComputeAABB (& A_aabb) B-> ComputeAABB (& B_aabb) if (AABBtoAABB (A_aabb, B_aabb)) pairs.push_back (A, B)
Das Layering erweist sich als sehr effizient und sehr einfach.
EIN halber Platz kann als eine Seite einer Linie in 2D betrachtet werden. Das Erkennen, ob sich ein Punkt auf der einen oder anderen Seite einer Linie befindet, ist eine ziemlich gewöhnliche Aufgabe und sollte von jedem, der seine eigene Physik-Engine erstellt, gründlich verstanden werden. Es ist schade, dass dieses Thema nirgendwo im Internet wirklich sinnvoll behandelt wird, zumindest was ich bisher gesehen habe - natürlich bis jetzt!
Die allgemeine Gleichung einer Linie in 2D lautet:
\ [Gleichung 4: \\
General \: form: ax + by + c = 0 \\
Normal \: bis \: Zeile: \ begin bmatrix
ein \\
b \\
\ end bmatrix \]
Beachten Sie, dass der Normalvektor trotz seines Namens nicht notwendigerweise normalisiert ist (dh er muss nicht unbedingt eine Länge von 1 haben)..
Um zu sehen, ob sich ein Punkt auf einer bestimmten Seite dieser Linie befindet, müssen wir ihn nur mit dem Punkt verbinden x
und y
Variablen in der Gleichung und überprüfen Sie das Vorzeichen des Ergebnisses. Ein Ergebnis von 0 bedeutet, dass sich der Punkt auf der Linie befindet und die positiven / negativen mittleren Seiten der Linie.
Das ist alles was dazu gehört! Wenn man weiß, dass der Abstand von einem Punkt zur Linie das Ergebnis des vorherigen Tests ist. Wenn der Normalenvektor nicht normalisiert wird, wird das Ergebnis um die Größe des Normalvektors skaliert.
Inzwischen kann eine vollständige, wenn auch einfache Physik-Engine völlig neu erstellt werden. Weiterführende Themen wie Reibung, Orientierung und dynamischer AABB-Baum werden möglicherweise in zukünftigen Tutorials behandelt. Bitte stellen Sie Fragen oder Kommentare, ich genieße es, sie zu lesen und zu beantworten!