Erstellen Sie eine mechanische Schlange mit inverser Kinematik
German (Deutsch) translation by Alex Grigorovich (you can also view the original English article)
Stellen Sie sich eine Kette von Partikeln vor, die zusammen in Symphonie animiert werden: Ein Zug, der sich bewegt, während alle angeschlossenen Abteile nachziehen; eine Puppe, die tanzt, während ihr Meister an der Schnur zieht; sogar deine Arme, wenn deine Eltern deine Hände halten, während sie dich zu einem Abendspaziergang führen. Die Bewegung bewegt sich vom letzten Knoten zum Ursprung und hält sich dabei an die Einschränkungen. Dies ist inverse Kinematik (IK), ein mathematischer Algorithmus, der notwendige Bewegungen berechnet. Hier verwenden wir es, um eine Schlange zu erstellen, die etwas fortgeschrittener ist als die aus Nokia-Spielen.
Endergebnisvorschau
Werfen wir einen Blick auf das Endergebnis, auf das wir hinarbeiten werden. Drücken und halten Sie die Tasten AUF, LINKS und RECHTS, um es zu bewegen.
Schritt 1: Beziehungen in einer Kette
Eine Kette ist aus Knoten aufgebaut. Jeder Knoten stellt einen Punkt in der Kette dar, an dem Translation und Rotation auftreten können. In der IK-Kette verläuft die Bewegung rückwärts vom letzten Knoten (letztes Kind) zum ersten Knoten (Wurzelknoten), im Gegensatz zu Vorwärtskinematik (FK), wo die Kinematik vom Wurzelknoten zum letzten Kind verläuft.
Alle Ketten beginnen mit dem Wurzelknoten. Dieser Wurzelknoten ist der handelnde Elternknoten, an den ein neuer Kindknoten angehängt wird. Dieses erste Kind wiederum wird dem zweiten Kind in der Kette übergeordnet, und dies wird wiederholt, bis das letzte Kind hinzugefügt wird. Die folgende Animation zeigt eine solche Beziehung.
Schritt 2: Erinnern an Beziehungen
Die Klasse IKshape
implementiert den Begriff eines Knotens in unserer Kette. Instanzen der IKshape-Klasse erinnern sich an ihre Eltern- und Kindknoten, mit Ausnahme des Wurzelknotens, der keinen Elternknoten hat, und des letzten Knotens, der keinen Kindknoten hat. Unten sind die privaten Eigenschaften von IKshape.
private var childNode:IKshape; private var parentNode:IKshape; private var vec2Parent:Vector2D;
Accessoren dieser Eigenschaften werden wie folgt gezeigt:
public function set IKchild(childSprite:IKshape):void { childNode = childSprite; } public function get IKchild ():IKshape { return childNode } public function set IKparent(parentSprite:IKshape):void { parentNode = parentSprite; } public function get IKparent():IKshape { return parentNode; }
Schritt 3: Vektor vom Kind zum Elternteil
Sie werden vielleicht feststellen, dass diese Klasse ein Vector2D speichert, das vom Kindknoten zum Elternknoten zeigt. Der Grund für diese Richtung liegt in der Bewegung, die vom Kind zu den Eltern fließt. Vector2D wird verwendet, weil die Größe und Richtung des Vektors, der vom Kind zum Eltern zeigt, häufig manipuliert wird, während das Verhalten einer IK-Kette implementiert wird. Daher ist es notwendig, solche Daten zu verfolgen. Nachfolgend finden Sie Methoden zum Manipulieren von Vektorgrößen für IKshape.
public function calcVec2Parent():void { var xlength:Number = parentNode.x - this.x; var ylength:Number = parentNode.y - this.y; vec2Parent = new Vector2D(xlength, ylength); } public function setVec2Parent(vec:Vector2D):void { vec2Parent = vec.duplicate(); } public function getVec2Parent():Vector2D { return vec2Parent.duplicate(); } public function getAng2Parent():Number { return vec2Parent.getAngle(); }
Schritt 4: Knoten zeichnen
Zu guter Letzt brauchen wir eine Methode, um unsere Form zu zeichnen. Wir werden ein Rechteck zeichnen, um jeden Knoten darzustellen. Sie können jedoch alle anderen Einstellungen vornehmen, indem Sie die Zeichenmethode hier überschreiben. Iv fügte ein Beispiel für eine Klasse hinzu, die die Standard-Draw-Methode, die Ball-Klasse, überschreibt. (Ein schneller Wechsel zwischen Formen wird am Ende dieses Tutorials gezeigt.) Damit schließen wir die Erstellung der Ikshape-Klasse ab.
protected function draw():void { var col:Number = 0x00FF00; var w:Number = 50; var h:Number = 10; graphics.beginFill(col); graphics.drawRect(-w/2, -h/2, w, h); graphics.endFill(); }
Schritt 5: Die IK-Kette
Die Klasse IKine implementiert das Verhalten einer IK-Kette. Die Erklärung zu dieser Klasse folgt dieser Reihenfolge
- Einführung in private Variablen in dieser Klasse.
- In dieser Klasse verwendete grundlegende Methoden.
- Mathematische Erläuterung der Funktionsweise bestimmter Funktionen.
- Implementierung dieser spezifischen Funktionen.
Schritt 6: Die Daten in einer Kette
Der folgende Code zeigt die privaten Variablen der IKine-Klasse.
private var IKineChain:Vector.<IKshape>; //members of chain //Data structure for constraints private var constraintDistance:Vector.<Number>; //distance between nodes private var constraintRangeStart:Vector.<Number>; //start of rotational freedom private var constraintRangeEnd:Vector.<Number>; //end of rotational freedom
Schritt 7: Instanziieren Sie die Kette
Die IKine-Kette speichert einen Sprite-Datentyp, der sich an die Beziehung zwischen Eltern und Kind erinnert. Diese Sprites sind Instanzen von IKshape. Die resultierende Kette sieht den Wurzelknoten bei Index 0, das nächste Kind bei Index 1, ... bis zum letzten Kind in sequentieller Weise. Der Aufbau der Kette geht jedoch nicht von der Wurzel bis zum letzten Kind; es ist vom letzten Kind bis zur Wurzel.
Unter der Annahme, dass die Kette die Länge n hat, folgt die Konstruktion dieser Reihenfolge: n-ter Knoten, (n-1)-ter Knoten, (n-2)-ter Knoten ... 0-ter Knoten. Die folgende Animation zeigt diesen Ablauf.
Bei der Instanziierung der IK-Kette wird der letzte Knoten eingefügt. Übergeordnete Knoten werden später angehängt. Der letzte angehängte Knoten ist die Wurzel. Der folgende Code enthält Methoden zum Aufbau von IK-Ketten, wobei Knoten an die Kette angehängt und entfernt werden.
public function IKine (lastChild:IKshape, distance:Number) { //initiate all private variables IKineChain = new Vector.<IKshape>(); constraintDistance = new Vector.<Number>(); constraintRangeStart = new Vector.<Number>(); constraintRangeEnd = new Vector.<Number>(); //Set constraints this.IKineChain[0] = lastChild; this.constraintDistance[0] = distance; this.constraintRangeStart[0] = 0; this.constraintRangeEnd[0] = 0; } /*Methods to manipulate IK chain */ public function appendNode(nodeNext:IKshape, distance:Number = 60, angleStart:Number = -1*Math.PI, angleEnd:Number = Math.PI):void { this.IKineChain.unshift(nodeNext); this.constraintDistance.unshift(distance); this.constraintRangeStart.unshift(angleStart); this.constraintRangeEnd.unshift(angleEnd); } public function removeNode(node:Number):void { this.IKineChain.splice(node, 1); this.constraintDistance.splice(node, 1); this.constraintRangeStart.splice(node, 1); this.constraintRangeEnd.splice(node, 1); }
Schritt 8: Kettenknoten erhalten
Diese folgenden Methoden werden verwendet, um bei Bedarf Knoten aus der Kette abzurufen.
public function getRootNode():IKshape { return this.IKineChain[0]; } public function getLastNode():IKshape { return this.IKineChain[IKineChain.length - 1]; } public function getNode(node:Number):IKshape { return this.IKineChain[node]; }
Schritt 9: Einschränkungen
Wir haben gesehen, wie die Knotenkette in einem Array dargestellt wird: Wurzelknoten bei Index 0, ... (n-1)-ter Knoten bei Index (n-2), n-ter Knoten bei Index (n-1 ), wobei n die Länge der Kette ist. Wir können unsere Einschränkungen auch bequem in einer solchen Reihenfolge anordnen. Abhängigkeiten gibt es in zwei Formen: Abstand zwischen Knoten und Biegefreiheitsgrad zwischen Knoten.
Die zwischen Knoten einzuhaltende Distanz wird als Einschränkung eines Kindknotens gegenüber seinem Elternknoten erkannt. Der Einfachheit halber können wir diesen Wert als constraintDistance
-Array mit einem ähnlichen Index wie dem des Kindknotens speichern. Beachten Sie, dass der Wurzelknoten kein übergeordnetes Element hat. Die Abstandsbeschränkung sollte jedoch beim Anhängen des Wurzelknotens registriert werden, damit, wenn die Kette später verlängert wird, der neu angehängte "Elternteil" dieses Wurzelknotens seine Daten verwenden kann.
Als nächstes wird der Biegewinkel für einen Elternknoten auf einen Bereich beschränkt. Wir werden den Start- und Endpunkt für den Bereich in den Arrays constraintRangeStart
und ConstraintRangeEnd
speichern. Die folgende Abbildung zeigt einen untergeordneten Knoten in Grün und zwei übergeordnete Knoten in Blau. Nur der mit "OK" markierte Knoten ist zulässig, da er innerhalb der Winkelbeschränkung liegt. Wir können einen ähnlichen Ansatz verwenden, um Werte in diesen Arrays zu referenzieren. Beachten Sie erneut, dass die Winkelbeschränkungen des Wurzelknotens registriert werden sollten, auch wenn sie aus ähnlichen Gründen wie zuvor nicht verwendet werden. Außerdem gelten Winkelbeschränkungen nicht für das letzte untergeordnete Element, da wir Flexibilität bei der Steuerung wünschen.



Schritt 10: Einschränkungen: Abrufen und Einstellen
Die folgenden Methoden können sich als nützlich erweisen, wenn Sie Einschränkungen für einen Knoten eingeleitet haben, den Wert jedoch in Zukunft ändern möchten.
/*Manipulating corresponding constraints */ public function getDistance(node:Number):Number { return this.constraintDistance[node]; } public function setDistance(newDistance:Number, node:Number):void { this.constraintDistance[node] = newDistance; } public function getAngleStart(node:Number):Number { return this.constraintRangeStart[node]; } public function setAngleStart(newAngleStart:Number, node:Number):void { this.constraintRangeStart[node] = newAngleStart; } public function getAngleRange(node:Number):Number { return this.constraintRangeEnd[node]; } public function setAngleRange(newAngleRange:Number, node:Number):void { this.constraintRangeEnd[node] = newAngleRange; }
Schritt 11: Längenbeschränkung, Konzept
Die folgende Animation zeigt die Berechnung der Längenbeschränkung.
Schritt 12: Längenbeschränkung, Formel
In diesem Schritt sehen wir uns Befehle in einer Methode an, die dabei helfen, den Abstand zwischen Knoten einzuschränken. Beachten Sie die hervorgehobenen Zeilen. Möglicherweise stellen Sie fest, dass diese Einschränkung nur auf das letzte untergeordnete Element angewendet wird. Soweit der Befehl geht, ist dies wahr. Elternknoten müssen nicht nur Längen-, sondern auch Winkelbeschränkungen erfüllen. All dies wird mit der Implementierung der Methode vecWithinRange() behandelt. Das letzte Kind muss nicht im Winkel eingeschränkt sein, da wir maximale Biegeflexibilität benötigen.
private function updateParentPosition():void { for (var i:uint = IKineChain.length - 1; i > 0; i--) { IKineChain[i].calcVec2Parent(); var vec:Vector2D; //handling the last child if ( i == IKineChain.length - 1) { var ang:Number = IKineChain[i].getAng2Parent(); vec = new Vector2D(0, 0); vec.redefine(this.constraintDistance[IKineChain.length - 1], ang); } else { vec = this.vecWithinRange(i); } IKineChain[i].setVec2Parent(vec); IKineChain[i].IKparent.x = IKineChain[i].x + IKineChain[i].getVec2Parent().x; IKineChain[i].IKparent.y = IKineChain[i].y + IKineChain[i].getVec2Parent().y; } }
Schritt 13: Winkelbeschränkung, Konzept
Zuerst berechnen wir den aktuellen Winkel zwischen den beiden Vektoren vec1 und vec2. Wenn der Winkel nicht innerhalb des eingeschränkten Bereichs liegt, weisen Sie dem Winkel die minimale oder maximale Grenze zu. Sobald ein Winkel definiert ist, können wir einen Vektor berechnen, der von vec1 zusammen mit der Einschränkung des Abstands (Größe) gedreht wird.



Die folgende Animation bietet eine weitere Alternative zur Visualisierung der Idee.
Schritt 14: Winkelbeschränkung, Formel
Die Implementierung der Winkelbeschränkungen ist wie folgt.
private function vecWithinRange(currentNode:Number):Vector2D { //getting the appropriate vectors var child2Me:Vector2D = IKineChain[currentNode].IKchild.getVec2Parent(); var me2Parent:Vector2D = IKineChain[currentNode].getVec2Parent(); //Implement angle bounds limitation var currentAng:Number = child2Me.angleBetween(me2Parent); var currentStart:Number = this.constraintRangeStart[currentNode]; var currentEnd:Number = this.constraintRangeEnd[currentNode]; var limitedAng:Number = Math2.implementBound(currentStart, currentEnd, currentAng); //Implement distance limitation child2Me.setMagnitude(this.constraintDistance[currentNode]); child2Me.rotate(limitedAng); return child2Me }
Schritt 15: Winkel mit Richtungen
Vielleicht lohnt es sich, hier die Idee durchzugehen, einen Winkel zu erhalten, der die Richtung im Uhrzeigersinn und gegen den Uhrzeigersinn interpretiert. Der Winkel zwischen zwei Vektoren, sagen wir vec1 und vec2, kann leicht aus dem Skalarprodukt dieser beiden Vektoren erhalten werden. Die Ausgabe ist der kürzeste Winkel, um vec1 zu vec2 zu drehen. Es gibt jedoch keine Richtungsvorstellung, da die Antwort immer positiv ist. Daher sollten Änderungen an der regulären Ausgabe vorgenommen werden. Vor der Ausgabe des Winkels habe ich das Vektorprodukt zwischen vec1 und vec2 verwendet, um zu bestimmen, ob die aktuelle Sequenz eine positive oder negative Drehung ist, und das Vorzeichen in den Winkel integriert. Ich habe die Richtungsfunktion unten in Codezeilen hervorgehoben.
public function vectorProduct(vec2:Vector2D):Number { return this.vec_x * vec2.y - this.vec_y * vec2.x; } public function angleBetween(vec2:Vector2D):Number { var angle:Number = Math.acos(this.normalise().dotProduct(vec2.normalise())); var vec1:Vector2D = this.duplicate(); if (vec1.vectorProduct(vec2) < 0) { angle *= -1; } return angle; }
Schritt 16: Orientierungsknoten
Knoten, die Boxen sind, müssen an der Richtung ihrer Vektoren ausgerichtet sein, damit sie schön aussehen. Andernfalls sehen Sie eine Kette wie unten. (Benutze die Pfeiltasten zur Bewegung.)
Die folgende Funktion implementiert die richtige Ausrichtung der Knoten.
private function updateOrientation():void { for (var i:uint = 0; i < IKineChain.length - 1; i++) { var orientation:Number = IKineChain[i].IKchild.getVec2Parent().getAngle(); IKineChain[i].rotation = Math2.degreeOf(orientation); } }
Schritt 17: Letztes Bit
Nachdem alles eingestellt ist, können wir unsere Kette mit animate()
animieren. Dies ist eine zusammengesetzte Funktion, die updateParentPosition()
und updateOrientation()
aufruft. Bevor dies jedoch erreicht werden kann, müssen wir die Beziehungen auf allen Knoten aktualisieren. Wir rufen updateRelationships()
auf. updateRelationships()
ist wiederum eine zusammengesetzte Funktion, die defineParent()
und defineChild()
aufruft. Dies geschieht einmalig und immer dann, wenn sich die Kettenstruktur ändert, zB werden Knoten zur Laufzeit hinzugefügt oder gelöscht.
Schritt 18: Wesentliche Methoden in IKine
Damit die IKine-Klasse für Sie funktioniert, sind dies die wenigen Methoden, die Sie sich ansehen sollten. Ich habe sie in Tabellenform dokumentiert.
Methode | Eingabeparameter | Rolle |
IKine() | letztes Kind: IKshape, Abstand:Anzahl | Konstrukteur. |
appendNode() | nodeNext:IKshape, [distance:Number, angleStart:Number, angleEnd:Number] | Knoten zur Kette hinzufügen, durch Knoten implementierte Einschränkungen definieren. |
updateRelationships() | Keiner | Aktualisieren Sie die Eltern-Kind-Beziehungen für alle Knoten. |
animate() | Keiner | Neuberechnung der Position aller Knoten in der Kette. Muss in jedem Frame aufgerufen werden. |
Beachten Sie, dass Winkeleingaben im Bogenmaß und nicht in Grad erfolgen.
Schritt 19: Erstellen einer Schlange
Lassen Sie uns nun ein Projekt in FlashDevelop erstellen. Im src-Ordner sehen Sie Main.as. Dies ist die Reihenfolge der Aufgaben, die Sie ausführen sollten:
- Initiieren Sie Kopien von IKshape oder Klassen, die sich von IKshape auf der Bühne erstrecken.
- Starten Sie IKine und verwenden Sie es, um Kopien von IKshape auf der Bühne zu verketten.
- Aktualisieren Sie die Beziehungen auf allen Knoten in der Kette.
- Implementieren Sie Benutzerkontrollen.
- Animieren!
Schritt 20: Objekte zeichnen
Das Objekt wird gezeichnet, während wir IKshape konstruieren. Dies geschieht in einer Schleife. Beachten Sie, wenn Sie das Aussehen der Zeichnung in einen Kreis ändern möchten, aktivieren Sie den Kommentar in Zeile 56 und deaktivieren Sie den Kommentar in Zeile 57. (Sie müssen meine Quelldateien herunterladen, damit dies funktioniert.)
private function drawObjects():void { for (var i:uint = 0; i < totalNodes; i++) { var currentObj:IKshape = new IKshape(); //var currentObj:Ball = new Ball(); currentObj.name = "b" + i; addChild(currentObj); } }
Schritt 21: Kette initialisieren
Vor der Initialisierung der IKine-Klasse zum Konstruieren der Kette werden private Variablen von Main.as erstellt.
private var currentChain:IKine; private var lastNode:IKshape; private var totalNodes:uint = 10;
Für den Fall hier sind alle Knoten auf einen Abstand von 40 zwischen den Knoten beschränkt.
private function initChain():void { this.lastNode = this.getChildByName("b" + (totalNodes - 1)) as IKshape; currentChain = new IKine(lastNode, 40); for (var i:uint = 2; i <= totalNodes; i++) { currentChain.appendNode(this.getChildByName("b" + (totalNodes - i)) as IKshape, 40, Math2.radianOf(-30), Math2.radianOf(30)); } currentChain.updateRelationships(); //center snake on the stage. currentChain.getLastNode().x = stage.stageWidth / 2; currentChain.getLastNode().y = stage.stageHeight /2 }
Schritt 22: Tastatursteuerelemente hinzufügen
Als nächstes deklarieren wir Variablen, die von unserer Tastatursteuerung verwendet werden sollen.
private var leadingVec:Vector2D; private var currentMagnitude:Number = 0; private var currentAngle:Number = 0; private var increaseAng:Number = 5; private var increaseMag:Number = 1; private var decreaseMag:Number = 0.8; private var capMag:Number = 10; private var pressedUp:Boolean = false; private var pressedLeft:Boolean = false; private var pressedRight:Boolean = false;
Bringen Sie die Hauptschleife und die Keyboard-Hörer auf der Bühne an. Ich habe sie hervorgehoben.
private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); // entry point this.drawObjects(); this.initChain(); leadingVec = new Vector2D(0, 0); stage.addEventListener(Event.ENTER_FRAME, handleEnterFrame); stage.addEventListener(KeyboardEvent.KEY_DOWN, handleKeyDown); stage.addEventListener(KeyboardEvent.KEY_UP, handleKeyUp); }
Schreiben Sie die Zuhörer.
private function handleEnterFrame(e:Event):void { if (pressedUp == true) { currentMagnitude += increaseMag; currentMagnitude = Math.min(currentMagnitude, capMag); } else { currentMagnitude *= decreaseMag; } if (pressedLeft == true) { currentAngle -= Math2.radianOf(increaseAng); } if (pressedRight == true) { currentAngle += Math2.radianOf(increaseAng); } leadingVec.redefine(currentMagnitude, currentAngle); var futureX:Number = leadingVec.x + lastNode.x; var futureY:Number = leadingVec.y + lastNode.y; futureX = Math2.implementBound(0, stage.stageWidth, futureX); futureY = Math2.implementBound(0, stage.stageHeight, futureY); lastNode.x = futureX; lastNode.y = futureY; lastNode.rotation = Math2.degreeOf(leadingVec.getAngle()); currentChain.animate(); } private function handleKeyDown(e:KeyboardEvent):void { if (e.keyCode == Keyboard.UP) { pressedUp = true; } if (e.keyCode == Keyboard.LEFT) { pressedLeft = true; } else if (e.keyCode == Keyboard.RIGHT) { pressedRight = true; } } private function handleKeyUp(e:KeyboardEvent):void { if (e.keyCode == Keyboard.UP) { pressedUp = false; } if (e.keyCode == Keyboard.LEFT) { pressedLeft = false; } else if (e.keyCode == Keyboard.RIGHT) { pressedRight = false; } }
Beachten Sie, dass ich eine Vector2D-Instanz verwendet habe, um die Schlange auf der Bühne zu bewegen. Ich habe diesen Vektor auch innerhalb der Bühnenbegrenzung eingeschränkt, damit er sich nicht nach außen bewegt. Das Actionscript, das diese Einschränkung ausführt, wird hervorgehoben.
Schritt 23: Animieren!
Drücken Sie Strg+Eingabe, um Ihre Schlange animieren zu sehen!. Steuern Sie seine Bewegung mit den Pfeiltasten.
Abschluss
Dieses Tutorial erfordert einige Kenntnisse in der Vektoranalyse. Für Leser, die einen vertrauten Blick auf Vektoren werfen möchten, lesen Sie den Beitrag von Daniel Sidhon. Ich hoffe, dies hilft Ihnen, die inverse Kinematik zu verstehen und zu implementieren. Danke fürs Lesen. Lassen Sie Vorschläge und Kommentare fallen, da ich immer gespannt darauf bin, vom Publikum zu hören. Terima Kasih.