Creați un șarpe mecanic cu o cinematică inversă

Imaginați-vă un lanț de particule animate în simfonie împreună: Un tren care se mișcă, pe măsură ce toate compartimentele atașate urmează exemplul; o marionetă dansând ca stăpânul să-și tragă șirul; chiar și brațele, când părinții tăi ține mâinile când te conduc într-o plimbare de seară. Mișcarea se răstoarnă de la ultimul nod la origine, respectând constrângerile în timp ce merge. Aceasta este inversă cinematică (IK), un algoritm matematic care calculează mișcările necesare. Aici o vom folosi pentru a crea un șarpe care este puțin mai avansat decât cel de la jocurile Nokia.


Rezultatul final al rezultatelor

Să aruncăm o privire asupra rezultatului final la care vom lucra. Apăsați și țineți apăsate tastele SUS, LEFT și DREAPTA pentru ao face să se miște.


Pasul 1: Relațiile într-un lanț

Un lanț este construit din noduri. Fiecare nod reprezintă un punct din lanț în care se poate întâmpla traducerea și rotația. În lanțul IK, mișcarea se extinde în sens invers față de ultimul nod (ultimul copil) până la primul nod (nodul rădăcină), spre deosebire de Kinematica Forward (FK) unde kinematica traversează de la nodul rădăcină la ultimul copil.

Toate lanțurile încep cu nodul rădăcină. Acest nod rădăcină este părintele care acționează la care este atașat un nou nod copil. La rândul său, acest prim copil va părăsi al doilea copil în lanț, iar acest lucru se va repeta până la adăugarea ultimului copil. Animația de mai jos descrie o astfel de relație.


Pasul 2: Amintirea relațiilor

IKshape clasa implementează noțiunea de nod în lanțul nostru. Instanțele clasei IKshape își amintesc nodurile părinte și copil, cu excepția nodului rădăcină care nu are nod părinte și ultimul nod care nu are un nod copil. Mai jos sunt proprietățile private ale IKshape.

 private var childNode: IKshape; private var parentNode: IKshape; privat var vec2Parent: Vector2D;

Accesorii pentru aceste proprietăți sunt afișați mai jos:

 funcția publică IKchild (childSprite: IKshape): void childNode = childSprite;  funcția publică IKchild (): IKshape return childNode setul de funcții publice IKparent (parentSprite: IKshape): void parentNode = parentSprite;  funcția publică obține IKparent (): IKshape return parentNode; 

Pasul 3: Vectorul de la copil la părinte

Este posibil să observați că această clasă stochează un Vector2D care indică de la nodul copil la nodul părinte. Motivul pentru această direcție se datorează mișcării care curge de la copil la părinte. Vector2D este folosit deoarece magnitudinea și direcția vectorului care indică de la copil la părinte vor fi manipulate frecvent în timp ce se implementează comportamentul unui lanț IK. Astfel, este necesar să se țină evidența acestor date. Mai jos sunt metode pentru a manipula cantitățile vectoriale pentru IKshape.

 funcția publică calcVec2Parent (): void var xlength: Number = parentNode.x - this.x; var ylength: Number = parentNode.y - this.y; vec2Parent = Vector2D nou (xlength, ylength);  funcția publică setVec2Parent (vec: Vector2D): void vec2Parent = vec.duplicate ();  funcția publică getVec2Parent (): Vector2D return vec2Parent.duplicate ();  funcția publică getAng2Parent (): Număr return vec2Parent.getAngle (); 

Pasul 4: Nodul de desen

Nu în ultimul rând, avem nevoie de o metodă pentru a ne desena forma. Vom desena un dreptunghi pentru a reprezenta fiecare nod. Cu toate acestea, orice alte preferințe pot fi introduse prin suprimarea metodei de tragere aici. Iv a inclus un exemplu de clasă care înlocuiește metoda de tragere implicită, clasa Ball. (O schimbare rapidă între forme va fi demonstrată la sfârșitul acestui tutorial.) Cu aceasta vom finaliza crearea clasei Ikshape.

 protecția funcției protejate (): void var col: Number = 0x00FF00; var w: Număr = 50; var h: număr = 10; graphics.beginFill (col); grafic.drawRect (-w / 2, -h / 2, w, h); graphics.endFill (); 

Pasul 5: Lanțul IK

Clasa IKine implementează comportamentul unui lanț IK. Explicația referitoare la această clasă respectă această ordine

  1. Introducere în variabilele private din această clasă.
  2. Metode de bază utilizate în această clasă.
  3. Explicație matematică privind funcționarea anumitor funcții.
  4. Implementarea acestor funcții specifice.

Pasul 6: Datele dintr-un lanț

Codul de mai jos prezintă variabilele private din clasa IKine.

 privat var IKineChain: Vector.; // membri ai lanțului // Structura datelor pentru constrângerile private var constraintDistance: Vector.; // distanța dintre noduri private var constraintRangeStart: Vector.; // începutul libertății de rotație private var constraintRangeEnd: Vector.; // sfârșitul libertății de rotație

Pasul 7: Instanțiați lanțul

Lanțul IKine va stoca un tip de date Sprite care își amintește relația dintre părinte și copil. Aceste sprite sunt exemple de IKshape. Lanțul rezultat vede nodul rădăcină la indexul 0, următorul copil la indexul 1 ,? până la ultimul copil în mod succesiv. Cu toate acestea, construcția lanțului nu este de la rădăcină la ultimul copil; este de la ultimul copil la rădăcină.

Presupunând că lanțul are lungimea n, construcția urmează următoarea secvență: nodul n, nodul (n-1), nodul (n-2)? Nodul 0. Animația de mai jos descrie această secvență.

La instanțierea lanțului IK, se introduce ultimul nod. Nodurile părinte vor fi atașate mai târziu. Ultimul nod atașat este rădăcina. Codul de mai jos sunt metodele de construcție a lanțului IK, adăugarea și eliminarea nodurilor în lant.

 funcția publică IKine (lastChild: IKshape, distanța: Număr) // inițiază toate variabilele private IKineChain = vector nou.(); constraintDistance = vector nou.(); constraintRangeStart = Vector nou.(); constraintRangeEnd = Vector nou.(); // Stabiliți constrângerile this.IKineChain [0] = lastChild; this.constraintDistance [0] = distanță; this.constraintRangeStart [0] = 0; this.constraintRangeEnd [0] = 0;  / * Metode pentru manipularea lanțului IK * / funcția publică appendNode (nodeNext: IKshape, distanță: Număr = 60, unghiStart: Număr = -1 * Math.PI, angleEnd: Number = Math.PI): void this.IKineChain. unshift (nodeNext); this.constraintDistance.unshift (distanța); this.constraintRangeStart.unshift (angleStart); this.constraintRangeEnd.unshift (angleEnd);  funcția publică removeNode (nod: Număr): void this.IKineChain.splice (nod, 1); this.constraintDistance.splice (nod, 1); this.constraintRangeStart.splice (nod, 1); this.constraintRangeEnd.splice (nod, 1); 

Pasul 8: Obținerea nodurilor de lanț

Următoarele metode sunt folosite pentru a recupera nodurile din lanț ori de câte ori este nevoie.

 funcția publică getRootNode (): IKshape return this.IKineChain [0];  funcția publică getLastNode (): IKshape return this.IKineChain [IKineChain.length - 1];  funcția publică getNode (nod: Număr): IKshape return this.IKineChain [nod]; 

Pasul 9: Constrângeri

Am văzut modul în care lanțul de noduri este reprezentat într-o matrice: Nodul rădăcină la indexul 0 ,? (n-1) - nod la index (n-2), n-n nod la index (n-1), n ​​fiind lungimea lanțului. Putem aranja constrângerile noastre într-o asemenea ordine. Constrângerile vin în două forme: distanța dintre noduri și gradul de libertate la încovoiere între noduri.

Distanța de întreținere între noduri este recunoscută ca o constrângere a unui nod copil față de părintele său. Din motive de comparare a comodității, putem stoca această valoare ca constraintDistance array cu index similar cu cel al nodului copil. Rețineți că nodul rădăcină nu are părinte. Cu toate acestea, constrângerea distanței ar trebui înregistrată la adăugarea nodului rădăcină, astfel încât dacă lanțul este extins mai târziu, noul "părinte" atașat al acestui nod rădăcină poate folosi datele sale.

Apoi, unghiul de încovoiere pentru un nod părinte este limitat la un interval. Vom păstra punctul de început și sfârșit pentru intervalul în constraintRangeStart și ConstraintRangeEnd matrice. Figura de mai jos prezintă un nod copil în verde și două noduri părinte în albastru. Numai nodul marcat cu "OK" este permis deoarece se află în limita constrângerii. Putem folosi abordarea similară în valorile de referință din aceste tablouri. Rețineți din nou că constrângerile de unghiuri ale nodului rădăcină ar trebui să fie înregistrate chiar dacă nu sunt utilizate datorită unui raționament similar celui precedent. În plus, constrângerile unghiulare nu se aplică ultimului copil, deoarece dorim flexibilitate în control.


Pasul 10: Constrângeri: Obținerea și setarea

Metodele de mai jos se pot dovedi utile atunci când ați inițiat constrângeri pe un nod, dar doriți să modificați valoarea în viitor.

 / * Manipularea constrângerilor corespunzătoare * / funcția publică getDistance (nod: Număr): Număr return this.constraintDistance [node];  funcția publică setDistance (newDistance: Număr, nod: Număr): void this.constraintDistance [node] = newDistance;  funcția publică getAngleStart (nod: Număr): Număr return this.constraintRangeStart [node];  funcția publică setAngleStart (newAngleStart: Număr, nod: Număr): void this.constraintRangeStart [node] = newAngleStart;  funcția publică getAngleRange (nod: Număr): Număr return this.constraintRangeEnd [node];  funcția publică setAngleRange (newAngleRange: Număr, nod: Număr): void this.constraintRangeEnd [node] = newAngleRange; 

Pasul 11: Constrângerea lungimii, conceptul

Următoarea animație arată calculul constrângerii în lungime.


Pasul 12: Condiția de lungime, formula

În acest pas, vom examina comenzile într-o metodă care ajută la limitarea distanței dintre noduri. Rețineți liniile evidențiate. Este posibil să observați că numai ultimul copil a aplicat această constrângere. Ei bine, în ceea ce privește comanda, este adevărat. Nodurile părinte sunt obligate să îndeplinească nu numai constrângerile de lungime, ci și unghiurile. Toate acestea sunt tratate cu implementarea metodei vecWithinRange (). Ultimul copil nu trebuie să fie constrâns în unghi deoarece avem nevoie de o flexibilitate maximă în îndoire.

 funcția privată updateParentPosition (): void pentru (var i: uint = IKineChain.length - 1; i> 0; i--) IKineChain [i] .calcVec2Parent (); var vec: Vector2D; // manipularea ultimului copil dacă (i == IKineChain.length - 1) var ang: Număr = IKineChain [i] .getAng2Parent (); vec = Vector2D nou (0, 0); vec.redefine (această distanță de constrângere [IKineChain.length - 1], ang);  altceva 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; 

Pasul 13: constrângere de unghi, concept

Mai întâi, calculăm unghiul curent între cele două vectori, vec1 și vec2. Dacă unghiul nu este în intervalul limitat, atribuiți limita minimă sau maximă unghiului. Odată ce un unghi este definit, putem calcula un vector care este rotit de la vec1 împreună cu constrângerea distanței (magnitudinea).

Următoarea animație oferă o altă alternativă pentru vizualizarea ideii.


Pasul 14: Constrângere în unghi, Formula

Punerea în aplicare a constrângerilor unghiulare este după cum urmează.

funcția privată vecWithinRange (actualNode: Number): Vector2D // obținerea vectorilor adecvați var child2Me: Vector2D = IKineChain [currentNode] .IKchild.getVec2Parent (); var me2Parent: Vector2D = IKineChain [actualNode] .getVec2Parent (); // Limitarea limitelor de unghi de implementare var currentAng: Number = child2Me.angleBetween (me2Parent); var curentStart: Număr = this.constraintRangeStart [currentNode]; Var curentEnd: Număr = this.constraintRangeEnd [currentNode]; var limitedAng: Număr = Math2.implementBound (curentStart, currentEnd, currentAng); // Implementați limitarea distanței child2Me.setMagnitude (this.constraintDistance [currentNode]); child2Me.rotate (limitedAng); retur copii2Me

Pasul 15: Unghi cu indicații

Poate că merită să trecem aici ideea de a obține un unghi care să interpreteze în sens orar și în sensul acelor de ceasornic. Unghiul împărțit între două vectori, să zicem vec1 și vec2, poate fi ușor obținut din produsul punctat al acestor două vectori. Ieșirea va fi cel mai scurt unghi pentru a roti vec1-vec2. Cu toate acestea, nu există nicio noțiune de direcție, deoarece răspunsul este întotdeauna pozitiv. Prin urmare, trebuie făcută o modificare a producției obișnuite. Înainte de ieșirea unghiului, am folosit produsul vector între vec1 și vec2 pentru a determina dacă secvența curentă este rotativă pozitivă sau negativă și a încorporat semnul în unghi. Am subliniat caracteristica direcțională în liniile de cod de mai jos.

 vector public vectorProduct (vec2: Vector2D): Număr return this.vec_x * vec2.y - this.vec_y * vec2.x;  unghiul funcției publice Între (vec2: Vector2D): Numărul var unghi: Număr = Math.acos (this.normalise (). dotProduct (vec2.normalise ()); var vec1: Vector2D = acest.duplicat (); dacă (vec1.vectorProduct (vec2) < 0)  angle *= -1;  return angle; 

Pasul 16: Orientarea nodurilor

Nodurile care sunt cutii trebuie să fie orientate spre direcția vectorilor lor, astfel încât să arate frumos. În caz contrar, veți vedea un lanț ca mai jos. (Utilizați tastele săgeată pentru a vă deplasa.)

Funcția de mai jos implementează orientarea corectă a nodurilor.

 funcția privată updateOrientation (): void pentru (var i: uint = 0; i < IKineChain.length - 1; i++)  var orientation:Number = IKineChain[i].IKchild.getVec2Parent().getAngle(); IKineChain[i].rotation = Math2.degreeOf(orientation);  

Pasul 17: ultimul bit

Acum că totul este setat, putem anima lanțul nostru folosind anima(). Aceasta este o funcție compusă care face apeluri updateParentPosition () și updateOrientation (). Cu toate acestea, înainte de a putea fi atinse, trebuie să actualizăm relațiile pe toate nodurile. Suntem la un telefon updateRelationships (). Din nou, updateRelationships () este o funcție compusă care face apeluri defineParent () și defineChild (). Acest lucru se face o dată și ori de câte ori există o schimbare în structura lanțului, de ex. Nodurile sunt adăugate sau abandonate în timpul execuției.


Pasul 18: Metode esențiale în IKine

Pentru a face ca clasa IKine să funcționeze pentru dvs., acestea sunt cele câteva metode pe care ar trebui să le priviți. Le-am documentat într-o formă tabelară.

Metodă Parametrii de intrare Rol
IKine () lastChild: IKshape, distanță: Număr Constructor.
appendNode () nodeNext: IKshape, [distanța: Număr, unghiStart: Număr, unghiEnd: Număr] adăugați noduri în lanț, definiți constrângerile implementate de nod.
updateRelationships () Nici unul Actualizați relațiile părinte-copil pentru toate nodurile.
anima() Nici unul Recalculând poziția tuturor nodurilor în lanț. Trebuie să fie numit fiecare cadru.

Rețineți că intrările de unghi sunt în radiani nu în grade.


Pasul 19: Crearea unui șarpe

Acum permiteți crearea unui proiect în FlashDevelop. În directorul src veți vedea Main.as. Aceasta este secvența sarcinilor pe care ar trebui să le faceți:

  1. Inițiați copii ale IKshape sau clase care se extind de la IKshape pe scenă.
  2. Inițiați IKine și folosiți-l pentru a alătura copiile lui IKshape pe scenă.
  3. Actualizați relațiile pe toate nodurile din lanț.
  4. Implementarea controalelor de utilizator.
  5. Anima!

Pasul 20: Desenați obiecte

Obiectul este desenat pe măsură ce construim IKshape. Acest lucru se face într-o buclă. Rețineți dacă doriți să schimbați perspectiva desenului într-un cerc, să activați comentariul pe linia 56 și să dezactivați comentariul pe linia 57. (Va trebui să descărcați fișierele sursă pentru ca acest lucru să funcționeze.)

 funcția privată drawObjects (): void pentru (var i: uint = 0; i < totalNodes; i++)  var currentObj:IKshape = new IKshape(); //var currentObj:Ball = new Ball(); currentObj.name = "b" + i; addChild(currentObj);  

Pasul 21: Inițierea lanțului

Înainte de a iniția clasa IKine pentru a construi lanțul, variabilele private ale Main.as sunt create.

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

În cazul de față, toate nodurile sunt constrânse la o distanță de 40 de noduri.

 funcția privată initChain (): void this.lastNode = this.getChildByName ("b" + (totalNodes - 1)) ca IKshape; currentChain = noul IKine (lastNode, 40); pentru (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 

Pasul 22: Adăugați comenzi pentru tastatură

Apoi, declarăm variabilele care urmează să fie utilizate de controlul tastaturii noastre.

 priv var varVec: Vector2D; private var currentMagnitude: Număr = 0; private var currentAngle: Number = 0; creșterea privată a var: Numărul = 5; creștere privată varMag: Număr = 1; private var decreaseMag: număr = 0.8; private var capMag: număr = 10; privat var presedUp: Boolean = false; privat var presedLeft: boolean = fals; private var presedRight: Boolean = false;

Atașați pe scenă bucla principală și ascultătorii tastaturii. Le-am subliniat.

funcția privată init (e: Event = null): void removeEventListener (Event.ADDED_TO_STAGE, init); // punct de intrare this.drawObjects (); this.initChain (); leadingVec = Vector2D nou (0, 0); stage.addEventListener (Event.ENTER_FRAME, handleEnterFrame); stage.addEventListener (tastaturăEvent.KEY_DOWN, handleKeyDown); stage.addEventListener (tastaturăEvent.KEY_UP, handleKeyUp);

Scrieți ascultătorii.

 funcția privată handleEnterFrame (e: Event): void if (presedUp == true) currentMagnitude + = increaseMag; curentMagnitude = Math.min (actualMagnitude, capMag);  altceva currentMagnitude * = decreaseMag;  dacă (presedLeft == true) currentAngle - = Math2.radianOf (increaseAng);  dacă (presedRight == true) currentAngle + = Math2.radianOf (increaseAng);  leadingVec.redefine (currentMagnitude, currentAngle); var viitorX: Number = leadingVec.x + lastNode.x; var viitorY: Numărul = leadingVec.y + lastNode.y; futureX = Math2.implementBound (0, stadiu.stageWidth, futureX); futureY = Math2.implementBound (0, stage.stageHeight, futureY); lastNode.x = futureX; lastNode.y = futureY; lastNode.rotation = Math2.degreeOf (leadingVec.getAngle ()); currentChain.animate ();  funcția privată handleKeyDown (e: KeyboardEvent): void if (e.keyCode == Keyboard.UP) presedUp = true;  dacă (e.keyCode == Keyboard.LEFT) presedLeft = true;  altceva dacă (e.keyCode == Keyboard.RIGHT) pressedRight = true;  funcția privată handleKeyUp (e: KeyboardEvent): void if (e.keyCode == Keyboard.UP) presedUp = false;  dacă (e.keyCode == Keyboard.LEFT) presedLeft = false;  altceva dacă (e.keyCode == Tastatură.RIGHT) pressedRight = false; 

Observați că am folosit o instanță Vector2D pentru a conduce șarpele în jurul scenei. De asemenea, am constrâns acest vector în limita scenei, astfel încât să nu se miște. Actionscript care execută această constrângere este evidențiat.


Pasul 23: Animați-vă!

Apăsați Ctrl + Enter pentru a vedea șarpele tău animat !. Controlează mișcarea folosind tastele săgeată.


Concluzie

Acest tutorial necesită cunoștințe în analiza vectorială. Pentru cititorii care doresc să se familiarizeze cu vectorii, au citit pe postul lui Daniel Sidhon. Sper că acest lucru vă ajută să înțelegeți și să implementați cinematica inversă. Mulțumesc pentru citire. Dați sugestii și comentarii ca fiind întotdeauna dornici să aud de la public. Terima Kasih.

Cod