Creëer een mechanische slang met inverse Kinematica

Stel je een keten van deeltjes voor die samen in de symfonie animeren: een trein die beweegt terwijl alle gehechte compartimenten hetzelfde zijn; een pop die danst terwijl de meester aan zijn touw trekt; zelfs je armen, wanneer je ouders je handen vasthouden terwijl ze je leiden tijdens een avondwandeling. Movevment rimpelingen van het laatste knooppunt naar de oorsprong en blijven zich aan beperkingen houden zoals het gaat. Dit is omgekeerde kinematica (IK), een wiskundig algoritme dat de nodige bewegingen berekent. Hier gebruiken we het om een ​​slang te maken die wat geavanceerder is dan die van Nokia-games.


Eindresultaat voorbeeld

Laten we eens kijken naar het uiteindelijke resultaat waar we naartoe zullen werken. Houd de toetsen OMHOOG, LINKS en RECHTS ingedrukt om te verplaatsen.


Stap 1: Relaties in een keten

Een ketting is opgebouwd uit knooppunten. Elk knooppunt vertegenwoordigt een punt in de keten waar vertaling en rotatie kunnen optreden. In de IK-keten loopt de beweging achteruit achteruit van het laatste knooppunt (laatste kind) naar het eerste knooppunt (wortelknooppunt) in tegenstelling tot Forward Kinematics (FK) waarbij de kinematica doorloopt van het wortelknooppunt naar het laatste kind.

Alle ketens beginnen met het wortelknooppunt. Dit wortelknooppunt is de werkende ouder waaraan een nieuw kindknooppunt is gekoppeld. Op zijn beurt zal dit eerste kind het tweede kind in de keten ouder maken en dit wordt herhaald totdat het laatste kind is toegevoegd. De onderstaande animatie toont een dergelijke relatie.


Stap 2: Relaties onthouden

De IKshape klasse implementeert het concept van een knoop in onze keten. Instanties van de klasse IKshape onthouden hun bovenliggende en onderliggende knooppunten, met uitzondering van het basisknooppunt dat geen bovenliggend knooppunt heeft en het laatste knooppunt dat geen onderliggende knoop heeft. Hieronder staan ​​de privé-eigenschappen van de IKshape.

 private var childNode: IKshape; private var parentNode: IKshape; privé var vec2Parent: Vector2D;

Accessors van deze eigenschappen worden getoond zoals hieronder:

 openbare functieset IKchild (childSprite: IKshape): void childNode = childSprite;  openbare functie krijgt IKchild (): IKshape return childNode openbare functieset IKparent (parentSprite: IKshape): void parentNode = parentSprite;  openbare functie krijgt IKparent (): IKshape return parentNode; 

Stap 3: Vector van kind tot ouder

U merkt misschien dat deze klasse een Vector2D opslaat die van het kind-node naar het bovenliggende knooppunt wijst. De reden voor deze richting is het gevolg van beweging die van kind tot ouder loopt. Vector2D wordt gebruikt omdat de grootte en richting van de vector die van kind naar ouder wijst, vaak zullen worden gemanipuleerd tijdens het uitvoeren van gedrag van een IK-keten. Het bijhouden van dergelijke gegevens is dus noodzakelijk. Hieronder vindt u methoden voor het manipuleren van vectorgrootheden voor IKshape.

 openbare functie 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 ();  openbare functie getVec2Parent (): Vector2D return vec2Parent.duplicate ();  openbare functie getAng2Parent (): Number return vec2Parent.getAngle (); 

Stap 4: Node tekenen

Last but not least hebben we een methode nodig om onze vorm te tekenen. We zullen een rechthoek tekenen om elk knooppunt te vertegenwoordigen. Eventuele andere voorkeuren kunnen echter worden ingevoerd door de tekenmethode te negeren. Iv nam een ​​voorbeeld van een klasse op die de standaard tekenmethode, de klasse Bal, opheft. (Een snelle omschakeling tussen vormen wordt aan het einde van deze tutorial getoond.) Hiermee voltooien we de creatie van de Ikshape-klasse.

 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 (); 

Stap 5: De IK-keten

IKine-klasse implementeert gedrag van een IK-keten. Uitleg over deze klasse volgt deze volgorde

  1. Inleiding tot privévariabelen in deze klasse.
  2. Basismethoden die in deze klasse worden gebruikt.
  3. Wiskundige toelichting op de werking van specifieke functies.
  4. Implementatie van die specifieke functies.

Stap 6: De gegevens in een keten

Code hieronder toont de private variabelen van de IKine-klasse.

 private var IKineChain: Vector.; // ketenleden // Datastructuur voor beperkingen private var constraintDistance: Vector.; // afstand tussen knooppunten private var constraintRangeStart: Vector.; // begin van rotatievrijheid private var constraintRangeEnd: Vector.; // einde van rotatievrijheid

Stap 7: Start de ketting

IKine-keten zal een Sprite-datatype opslaan dat de relatie van zijn ouder en kind onthoudt. Deze sprites zijn voorbeelden van IKshape. De resulterende keten ziet het hoofdknooppunt bij index 0, het volgende kind bij index 1 ,? tot het laatste kind op een sequentiële manier. De constructie van de ketting is echter niet van root tot het laatste kind; het is van het laatste kind om te rooten.

Ervan uitgaande dat de keten van lengte n is, volgt constructie deze reeks: n-de knoop, (n-1) -de knoop, (n-2) -de knoop? 0-ste knoop. De onderstaande animatie toont deze reeks.

Na het instantiëren van de IK-keten wordt het laatste knooppunt ingevoegd. Ouderknooppunten worden later toegevoegd. Het laatste toegevoegde knooppunt is de root. De onderstaande code zijn methoden voor IK-ketenconstructie, het toevoegen en verwijderen van knooppunten om te ketenen.

 openbare functie IKine (lastChild: IKshape, distance: Number) // start alle privévariabelen IKineChain = new Vector.(); constraintDistance = new Vector.(); constraintRangeStart = new Vector.(); constraintRangeEnd = nieuwe Vector.(); // Stel beperkingen in this.IKineChain [0] = lastChild; this.constraintDistance [0] = afstand; this.constraintRangeStart [0] = 0; this.constraintRangeEnd [0] = 0;  / * Methoden om IK-keten te manipuleren * / 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 (afstand); this.constraintRangeStart.unshift (angleStart); this.constraintRangeEnd.unshift (angleEnd);  public function removeNode (node: Number): void this.IKineChain.splice (node, 1); this.constraintDistance.plice (knooppunt, 1); this.constraintRangeStart.plice (knooppunt, 1); this.constraintRangeEnd.splice (knooppunt, 1); 

Stap 8: kettingknopen krijgen

Deze volgende methoden worden gebruikt om knooppunten uit de keten op te halen wanneer dat nodig is.

 openbare functie 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]; 

Stap 9: beperkingen

We hebben gezien hoe de keten van knooppunten wordt weergegeven in een array: Root-knooppunt op index 0 ,? (n-1) -de knoop bij index (n-2), n-de knoop bij index (n-1), waarbij n de lengte van keten is. We kunnen onze beperkingen gemakkelijk in die volgorde ook regelen. Beperkingen zijn er in twee vormen: afstand tussen knooppunten en mate van buigvrijheid tussen knooppunten.

De afstand die moet worden gehandhaafd tussen knooppunten wordt herkend als de beperking van een onderliggende knoop aan het bovenliggende element. Voor het gemak van verwijzingen kunnen we deze waarde opslaan als constraintDistance array met index die lijkt op die van de onderliggende knooppunten. Merk op dat het root-knooppunt geen bovenliggend element heeft. De afstandsbeperking moet echter worden geregistreerd wanneer het basisknooppunt wordt toegevoegd, zodat als de keten later wordt uitgebreid, het nieuw toegevoegde "bovenliggend element" van dit hoofdknooppunt zijn gegevens kan gebruiken..

Vervolgens is de buigingshoek voor een bovenliggend knooppunt beperkt tot een bereik. We zullen het start- en eindpunt voor bereik opslaan constraintRangeStart en ConstraintRangeEnd matrix. Afbeelding hieronder toont een kindknooppunt in groen en twee bovenliggende knooppunten in blauw. Alleen het knooppunt gemarkeerd met "OK" is toegestaan ​​omdat het binnen de hoekbeperking valt. We kunnen een vergelijkbare benadering gebruiken bij het verwijzen naar waarden in deze arrays. Merk nogmaals op dat de hoekbeperkingen van het knooppunt moeten worden geregistreerd, hoewel ze niet in gebruik zijn vanwege een vergelijkbare redenering als eerder. Bovendien zijn hoekbeperkingen niet van toepassing op het laatste kind, omdat we flexibiliteit in controle willen.


Stap 10: Constraints: Getting and Setting

De volgende methoden kunnen nuttig zijn wanneer u beperkingen op een knooppunt hebt geïnitieerd, maar in de toekomst de waarde ervan wilt wijzigen.

 / * Manipuleren van overeenkomstige beperkingen * / publieke functie getDistance (knooppunt: Number): Number return this.constraintDistance [node];  public function setDistance (newDistance: Number, node: Number): void this.constraintDistance [node] = newDistance;  openbare functie getAngleStart (knooppunt: 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; 

Stap 11: Length Constraint, Concept

яDe volgende animatie toont de berekening van de lengtebeperking.


Stap 12: lengtebeperking, formule

In deze stap zullen we de opdrachten in een methode bekijken die helpen de afstand tussen knooppunten te beperken. Let op de gemarkeerde lijnen. U merkt mogelijk dat alleen het laatste kind deze beperking heeft toegepast. Welnu, voor zover het commando gaat, is dit waar. Ouderknooppunten zijn vereist om niet alleen lengte- maar hoekbeperkingen te vervullen. Al deze worden afgehandeld met de implementatie van de methode vecWithinRange (). Het laatste kind hoeft niet beperkt te worden, omdat we maximale buigflexibiliteit nodig hebben.

 persoonlijke functie updateParentPosition (): void for (var i: uint = IKineChain.length - 1; i> 0; i--) IKineChain [i] .calcVec2Parent (); var vec: Vector2D; // het laatste kind verwerken als (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; 

Stap 13: Hoekbeperking, Concept

Eerst berekenen we de huidige hoek ingeklemd tussen de twee vectoren, vec1 en vec2. Als de hoek niet binnen het beperkte bereik ligt, wijst u de minimum- of maximumlimiet toe aan de hoek. Zodra een hoek is gedefinieerd, kunnen we een vector berekenen die wordt geroteerd uit vec1 samen met de beperking van de afstand (magnitude).

яDe volgende animatie biedt nog een alternatief voor het visualiseren van het idee.


Stap 14: Hoekbeperking, formule

De implementatie van de hoekbeperkingen is zoals hieronder.

private function vecWithinRange (currentNode: Number): Vector2D // de juiste vectoren verkrijgen var child2Me: Vector2D = IKineChain [currentNode] .IKchild.getVec2Parent (); var me2Parent: Vector2D = IKineChain [currentNode] .getVec2Parent (); // Implementeer hoekgrenzen beperking var currentAng: Number = child2Me.angleTween (me2Parent); var currentStart: Number = this.constraintRangeStart [currentNode]; var currentEnd: Number = this.constraintRangeEnd [currentNode]; var limitedAng: Number = Math2.implementBound (currentStart, currentEnd, currentAng); // Implementeer afstandsbeperking child2Me.setMagnitude (this.constraintDistance [currentNode]); child2Me.rotate (limitedAng); return child2Me

Stap 15: hoek met richtingen

Misschien is het waard om hier het idee door te nemen van het krijgen van een hoek die de richting met de klok mee en tegen de klok in interpreteert. De hoek tussen twee vectoren, zeg vec1 en vec2, kan eenvoudig worden verkregen uit het puntproduct van die twee vectoren. De uitvoer zal de kortste hoek zijn om vec1 naar vec2 te roteren. Er is echter geen idee van richting, omdat het antwoord altijd positief is. Daarom moet de standaarduitvoer worden aangepast. Voordat ik de hoek uitvoerde, gebruikte ik een vectorproduct tussen vec1 en vec2 om te bepalen of de huidige reeks positieve of negatieve rotatie is en nam het teken op in de hoek. Ik heb de directionele functie gemarkeerd in regels van de onderstaande code.

 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; 

Stap 16: Nodes oriënteren

Knopen die vakken zijn, moeten in de richting van hun vectoren worden geplaatst, zodat ze er mooi uitzien. Anders zie je een ketting zoals hieronder. (Gebruik de pijltjes toetsen om te bewegen.)

De onderstaande functie implementeert de juiste richting van knooppunten.

 persoonlijke functie 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);  

Stap 17: Laatste bit

Nu alles is ingesteld, kunnen we onze keten met behulp van animeren animeren (). Dit is een samengestelde functie waarmee wordt gebeld updateParentPosition () en updateOrientation (). Voordat dat kan worden bereikt, moeten we de relaties op alle knooppunten bijwerken. We bellen naar updateRelationships (). Nog een keer, updateRelationships () is een samengestelde functie waarmee wordt gebeld defineParent () en defineChild (). Dit gebeurt eens en altijd wanneer de structuur van de keten verandert, bijvoorbeeld dat knooppunten tijdens runtime worden toegevoegd of verwijderd.


Stap 18: Essentiële methoden in IKine

Om de IKine-klasse voor u te laten werken, zijn dit de weinige methoden waarop u moet letten. Ik heb ze gedocumenteerd in een tabelvorm.

Methode Invoerparameters Rol
IKine () lastChild: IKshape, afstand: Number bouwer.
appendNode () nodeNext: IKshape, [distance: Number, angleStart: Number, angleEnd: Number] knooppunten toevoegen om te ketenen, definiëren beperkingen geïmplementeerd door het knooppunt.
updateRelationships () Geen Update ouder-kindrelaties voor alle knooppunten.
animeren () Geen Herberekening van de positie van alle knooppunten in de keten. Moet elk frame worden genoemd.

Merk op dat hoekingangen in radialen zijn, niet in graden.


Stap 19: Een slang maken

Laten we nu een project maken in FlashDevelop. In de map src ziet u Main.as. Dit is de volgorde van taken die u moet doen:

  1. Initialiseer kopieën van IKshape of klassen die zich uitstrekken vanaf IKshape op het podium.
  2. Start IKine en gebruik het om kopieën van IKshape op het podium te ketenen.
  3. Relaties bijwerken op alle knooppunten in de keten.
  4. Implementeer gebruikersbedieningen.
  5. bezielen!

Stap 20: Teken objecten

Object wordt getekend terwijl we IKshape construeren. Dit gebeurt in een lus. Opmerking: als u de weergave van de tekening wilt wijzigen in een cirkel, schakel dan commentaar in op regel 56 en schakel commentaar op regel 57 uit. (U moet mijn bronbestanden downloaden om te zorgen dat dit werkt.)

 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);  

Stap 21: Ketting initialiseren

Voordat de IKine-klasse wordt geïnitialiseerd om de keten te construeren, worden privévariabelen van Main.as gemaakt.

 private var currentChain: IKine; private var lastNode: IKshape; private var totalNodes: uint = 10;

Voor het geval hier zijn alle knooppunten beperkt tot een afstand van 40 tussen knooppunten.

 private function initChain (): void this.lastNode = this.getChildByName ("b" + (totalNodes - 1)) als IKshape; currentChain = nieuwe 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 

Stap 22: Toetsenbordbediening toevoegen

Vervolgens verklaren we dat variabelen moeten worden gebruikt door ons toetsenbordbeheer.

 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;

Bevestig op het podium de luisteraars van de hoofdlus en het toetsenbord. Ik heb ze gemarkeerd.

private function init (e: Event = null): void removeEventListener (Event.ADDED_TO_STAGE, init); // startpunt 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);

Schrijf de luisteraars.

 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; 

Merk op dat ik een Vector2D-instantie heb gebruikt om de slang over het podium te laten bewegen. Ik heb deze vector ook binnen de begrenzing van het podium beperkt, zodat deze niet naar buiten komt. Het Actionscript dat deze beperking uitvoert, is gemarkeerd.


Stap 23: animeer!

Druk op Ctrl + Enter om uw slang levend te zien !. Regel de beweging met de pijltjestoetsen.


Conclusie

Deze zelfstudie vereist enige kennis in vectoranalyse. Voor lezers die graag een vertrouwde kijk op vectoren willen hebben, moet je een bericht lezen op de post door Daniel Sidhon. Ik hoop dat dit je helpt bij het begrijpen en implementeren van inverse kinematica. Bedankt voor het lezen. Doe suggesties en opmerkingen vallen zoals ik altijd enthousiast om te horen van het publiek. Terima Kasih.