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.
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.
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.
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;
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 ();
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 ();
Clasa IKine implementează comportamentul unui lanț IK. Explicația referitoare la această clasă respectă această ordine
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
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);
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];
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.
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;
Următoarea animație arată calculul constrângerii în lungime.
Î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;
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.
Punerea în aplicare a constrângerilor unghiulare este după cum urmează.
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;
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);
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.
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.
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:
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);
Î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
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.
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.
Apăsați Ctrl + Enter pentru a vedea șarpele tău animat !. Controlează mișcarea folosind tastele săgeată.
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.