Verwenden von Drehmoment und Triebwerken zum Bewegen und Drehen eines von Spielern gestalteten Raumschiffs

Bei der Arbeit an einem Spiel, bei dem die Raumschiffe von Spielern entworfen wurden und teilweise zerstört werden können, bin ich auf ein interessantes Problem gestoßen: Das Bewegen eines Schiffes mit Triebwerken ist keine leichte Aufgabe. Sie können das Schiff einfach wie ein Auto bewegen und drehen, aber wenn Sie möchten, dass Schiffsentwurf und struktureller Schaden die Schiffsbewegungen glaubwürdig beeinflussen, könnte die Simulation von Triebwerken die bessere Lösung sein. In diesem Tutorial zeige ich Ihnen, wie das geht.

Unter der Annahme, dass ein Schiff mehrere Strahlruder in verschiedenen Konfigurationen haben kann und sich die Form und die physikalischen Eigenschaften des Schiffes ändern können (z. B. könnten Teile des Schiffes zerstört werden), muss dies bestimmt werden welche Schubdüsen abzufeuern, um das Schiff zu bewegen und zu drehen. Das ist die Hauptherausforderung, die wir hier angehen müssen.

Die Demo ist in Haxe geschrieben, die Lösung kann jedoch problemlos in jeder Sprache implementiert werden. Eine physikalische Engine, die Box2D oder Nape ähnelt, wird vorausgesetzt, aber jede Engine, die die Mittel zur Verfügung stellt, um Kräfte und Impulse anzuwenden und die physikalischen Eigenschaften von Körpern abzufragen, wird dies tun.

Probieren Sie die Demo aus

Klicken Sie auf die SWF-Datei, um den Fokus zu aktivieren. Verwenden Sie dann die Pfeiltasten und die Tasten Q und W, um verschiedene Triebwerke zu aktivieren. Sie können mit den Zifferntasten 1-4 zu anderen Raumschiff-Designs wechseln und Sie können auf einen beliebigen Block oder Strahlruder klicken, um ihn vom Schiff zu entfernen.


Das Schiff vertreten

Dieses Diagramm zeigt die Klassen, die das Schiff repräsentieren, und wie sie miteinander zusammenhängen:

BodySprite ist eine Klasse, die einen physischen Körper mit einer grafischen Darstellung darstellt. Es ermöglicht das Anbringen von Anzeigeobjekten an Formen und stellt sicher, dass sie sich korrekt mit dem Körper bewegen und drehen.

Das Schiff Klasse ist ein Container von Modulen. Es verwaltet die Struktur des Schiffes und kümmert sich um das Anbringen und Lösen von Modulen. Es enthält eine einzige ModuleManager Beispiel.

Beim Anhängen eines Moduls werden dessen Form und Anzeigeobjekt an den Basiswert angehängt BodySprite, Das Entfernen eines Moduls erfordert jedoch etwas mehr Arbeit. Zuerst werden die Form und das Anzeigeobjekt des Moduls aus dem Modul entfernt BodySprite, und dann wird die Struktur des Schiffes überprüft, sodass alle Module, die nicht mit dem Kern (dem Modul mit dem roten Kreis) verbunden sind, getrennt werden. Dies geschieht mit einem Algorithmus ähnlich dem Flood-Fill, der berücksichtigt, wie jedes Modul mit anderen Modulen verbunden werden kann (z. B. können Triebwerke je nach Ausrichtung nur von einer Seite her angeschlossen werden)..

Das Abnehmen von Modulen ist etwas anders: Ihre Form und ihr Anzeigeobjekt werden immer noch aus dem entfernt BodySprite, sind dann aber an eine Instanz von ShipDebris.

Diese Art der Darstellung des Schiffes ist nicht die einfachste, aber ich finde, dass es sehr gut funktioniert. Die Alternative wäre, jedes Modul als separaten Körper darzustellen und mit einer Schweißverbindung zusammenzukleben. Dies würde zwar das Brechen des Schiffs viel einfacher machen, aber es würde auch dazu führen, dass sich das Schiff bei einer großen Anzahl von Modulen gummiartig und elastisch anfühlt.

Das ModuleManager ist ein Container, der die Module eines Schiffes sowohl in einer Liste (die eine einfache Iteration ermöglicht) als auch einer Hash-Karte (die einen einfachen Zugriff über lokale Koordinaten ermöglicht) enthält..

Das ShipModule Klasse stellt offensichtlich ein Schiffsmodul dar. Es ist eine abstrakte Klasse, die einige praktische Methoden und Attribute definiert, die jedes Modul besitzt. Jede Modulunterklasse ist dafür verantwortlich, ein eigenes Anzeigeobjekt und eine eigene Form zu erstellen und sich bei Bedarf selbst zu aktualisieren. Module werden auch aktualisiert, wenn sie mit ihnen verbunden sind ShipDebris, aber in diesem Fall die attachToShip Flag ist auf gesetzt falsch.

Ein Schiff ist also nur eine Sammlung von Funktionsmodulen: Bausteine, deren Platzierung und Typ das Verhalten des Schiffes bestimmen. Wenn ein hübsches Schiff wie ein Ziegelhaufen herumschwebt, wäre das natürlich ein langweiliges Spiel. Wir müssen also herausfinden, wie wir es auf eine Art und Weise bewegen können, die Spaß macht und dennoch überzeugend realistisch ist.


Vereinfachung des Problems

Das Drehen und Bewegen eines Schiffes durch selektives Abfeuern von Triebwerken, dessen Schub entweder durch Einstellen der Drosselklappe oder durch kurzes Ein- und Ausschalten variiert, ist ein schwieriges Problem. Zum Glück ist es auch unnötig.

Wenn Sie ein Schiff beispielsweise genau um einen Punkt drehen möchten, können Sie dies einfach tun, indem Sie der Physik-Engine sagen, dass sie den gesamten Körper drehen soll. In diesem Fall habe ich jedoch nach einer einfachen Lösung gesucht, die nicht perfekt ist, aber Spaß macht. Um das Problem zu vereinfachen, füge ich eine Einschränkung ein:

Triebwerke können nur ein- oder ausgeschaltet sein und ihren Schub nicht variieren.

Nachdem wir auf Perfektion und Komplexität verzichtet haben, ist das Problem viel einfacher. Wir müssen für jedes Triebwerk festlegen, ob es ein- oder ausgeschaltet sein soll, abhängig von seiner Position auf dem Schiff und der Eingabe des Spielers. Wir könnten jedem Triebwerk eine andere Taste zuweisen, aber wir würden am Ende mit einem interstellaren QWOP enden. Deshalb verwenden wir die Pfeiltasten zum Drehen und Bewegen und Q und W zum Spannen.


Der einfache Fall: Das Schiff vorwärts und rückwärts bewegen

Die erste Aufgabe besteht darin, das Schiff vorwärts und rückwärts zu bewegen, da dies der einfachste mögliche Fall ist. Um das Schiff zu bewegen, feuern wir einfach die Triebwerke in die entgegengesetzte Richtung zu der, die wir gehen wollen. Wenn wir zum Beispiel vorgehen wollten, würden wir alle Triebwerke abfeuern, die nach hinten zeigen.

 // Aktualisiert das Thruster einmal pro Frame und überschreibt die öffentliche Funktion update (): Void if (attachedToShip) // Vorwärts und rückwärts bewegen if ((Input.check (Key.UP) && orientation == ShipModule.SOUTH) || (Input.check (Key.DOWN) && direction == ShipModule.NORTH)) fire (thrustImpulse);  // Strafing else if ((Input.check (Key.Q) && orientation == ShipModule.EAST) || (Input.check (Key.W) && orientation == ShipModule.WEST)) fire (thrustImpulse); 

Offensichtlich wird dies nicht immer den gewünschten Effekt erzielen. Wenn die Triebwerke nicht gleichmäßig platziert sind, kann das Schiff aufgrund der oben genannten Einschränkung drehen. Darüber hinaus ist es nicht immer möglich, die richtige Kombination von Triebwerken auszuwählen, um ein Schiff nach Bedarf zu bewegen. Manchmal bewegt keine Kombination von Triebwerken das Schiff so, wie wir es wünschen. Dies ist ein wünschenswerter Effekt in meinem Spiel, da Schiffsschäden und schlechtes Schiffsdesign sehr offensichtlich werden.


Eine Schiffskonfiguration, die sich nicht rückwärts bewegen kann

Das Schiff drehen

In diesem Beispiel ist es offensichtlich, dass die Triebwerke A, D und E dazu führen, dass sich das Schiff im Uhrzeigersinn dreht (und auch etwas driftet, aber das ist ein völlig anderes Problem). Wenn Sie das Schiff drehen, müssen Sie wissen, auf welche Weise ein Triebwerk zur Drehung des Schiffes beiträgt.

Es stellt sich heraus, dass das, was wir hier suchen, die Gleichung von ist Drehmoment - insbesondere das Vorzeichen und die Größe des Drehmoments.

Schauen wir uns das Drehmoment an. Drehmoment ist definiert als Maß dafür, wie stark eine auf ein Objekt einwirkende Kraft dieses Objekt in Drehung versetzt:

Da wir das Schiff um seinen Massenschwerpunkt drehen wollen, ist unser [Latex] r [/ Latex] der Abstandsvektor von der Position unseres Triebwerks zum Massenmittelpunkt des gesamten Schiffes. Das Rotationszentrum könnte ein beliebiger Punkt sein, aber der Massenmittelpunkt ist wahrscheinlich das, was ein Spieler erwarten würde.

Der Kraftvektor [Latex] F [/ Latex] ist ein Einheitsrichtungsvektor, der die Orientierung unseres Triebwerks beschreibt. In diesem Fall ist uns das tatsächliche Drehmoment nicht wichtig, nur das Vorzeichen. Daher ist es in Ordnung, nur den Richtungsvektor zu verwenden.

Da das Kreuzprodukt nicht für zweidimensionale Vektoren definiert ist, arbeiten wir einfach mit dreidimensionalen Vektoren und setzen die Komponente [latex] z [/ latex] auf 0, die mathematik wunderbar vereinfachen:

[Latex]
\ tau = r \ times F \\
\ tau = (r_x, \ quad r_y, \ quad 0) \ times (F_x, \ quad F_y, \ quad 0) \\
\ tau = (-0 \ cdot F_y + r_y \ cdot 0, \ quad 0 \ cdot F_x - r_x \ cdot 0, \ quad -r_y \ cdot F_x + r_x \ cdot F_y) \\
\ tau = (0, \ quad 0, \ quad -r_y \ cdot F_x + r_x \ cdot F_y) \\
\ tau_z = r_x \ cdot F_y - r_y \ cdot F_x \\
[/Latex]


Die farbigen Kreise beschreiben, wie das Triebwerk das Schiff beeinflusst: Grün bedeutet, dass sich das Schiff im Uhrzeigersinn dreht, Rot zeigt an, dass sich das Schiff gegen den Uhrzeigersinn dreht. Die Größe jedes Kreises gibt an, wie stark das Triebwerk die Rotation des Schiffes beeinflusst.

Damit können wir berechnen, wie die einzelnen Triebwerke das Schiff individuell beeinflussen. Ein positiver Rückgabewert zeigt an, dass das Schiff das Schiff im Uhrzeigersinn drehen lässt und umgekehrt. Die Implementierung dieses Codes ist sehr einfach:

 // Berechnet das Drehmoment nicht ganz mit der obigen Gleichung privateTabelle berechnenTorque (): Float var distToCOM = shape.localCOM.mul (-1.0); return distToCOM.x * thrustDir.y - distToCOM.y * thrustDir.x;  // Thruster-Update überschreibt public function update (): Void if (attachedToShip) // Wenn das Thruster an ein Schiff angeschlossen ist, verarbeiten wir die // // Eingabe des Players und schalten das Thruster bei Bedarf aus. var drehmoment = Torque () berechnen; if ((Input.check (Key.UP) && orientation == ShipModule.SOUTH) || (Input.check (Key.DOWN) && orientation == ShipModule.NORTH)) fire (thrustImpulse);  else if ((Input.check (Key.Q) && orientation == ShipModule.EAST) || (Input.check (Key.W) && orientation == ShipModule.WEST)) fire (thrustImpulse);  else if ((Input.check (Key.LEFT) && Drehmoment < -torqueThreshold) || (Input.check(Key.RIGHT) && torque > TorqueThreshold)) fire (thrustImpulse);  else thrusterOn = false;  else // Wenn das Triebwerk nicht an einem Schiff befestigt ist, dann wird es // an einem Bruchstück befestigt. Wenn das Triebwerk ausgelöst wurde, als es abgenommen wurde, wird es noch eine Weile weiter geschossen. // detachedThrustTimer ist eine Variable, die als einfacher Zeitgeber verwendet wird, // und wird gesetzt, wenn sich das Triebwerk von einem Schiff löst. if (detachedThrustTimer> 0) detachedThrustTimer - = NapeWorld.currentWorld.deltaTime; Feuer (Schubstoß);  else thrusterOn = false;  animate ();  // Zündet das Triebwerk durch Anlegen eines Impulses an den übergeordneten Körper, wobei die Richtung entgegen der Triebwerksrichtung und der Betrag als Parameter übergeben wird. // Das ThrusterOn-Flag wird für die Animation verwendet. öffentliches Funktionsfeuer (Betrag: Float): Void var thrustVec = thrustDir.mul (- Betrag); var impulseVec = thrustVec.rotate (parent.body.rotation); parent.body.applyWorldImpulse (impulseVec, getWorldPos ()); thrusterOn = true; 

Fazit

Die vorgestellte Lösung ist einfach zu implementieren und funktioniert gut für ein Spiel dieses Typs. Natürlich gibt es Raum für Verbesserungen: Dieses Tutorial und die Demo berücksichtigen nicht, dass ein Schiff möglicherweise von einem anderen Spieler als einem menschlichen Spieler gesteuert wird, und die Implementierung eines KI-Piloten, der tatsächlich ein halb zerstörtes Schiff fliegen kann, wäre eine sehr interessante Herausforderung (eine, der ich mich irgendwann stellen muss).