Crea un serpente meccanico con cinematica inversa

Immagina una catena di particelle che si animano insieme in sinfonia: un treno che si muove mentre tutti i compartimenti attaccati seguono l'esempio; un burattino che danza mentre il suo padrone tira la corda; anche le tue braccia, quando i tuoi genitori ti tengono per mano mentre ti guidano in una passeggiata serale. Movevment si increspa dall'ultimo nodo all'origine, rispettando i vincoli mentre procede. Questo è cinematica inversa (IK), un algoritmo matematico che calcola i movimenti necessari. Qui, lo useremo per creare un serpente che è un po 'più avanzato di quello dei giochi Nokia.


Anteprima del risultato finale

Diamo un'occhiata al risultato finale su cui lavoreremo. Premi e tieni premuti i tasti SU, SINISTRA e DESTRA per farlo muovere.


Fase 1: relazioni in una catena

Una catena è costituita da nodi. Ogni nodo rappresenta un punto nella catena in cui possono verificarsi la traslazione e la rotazione. Nella catena IK, il movimento si inverte in senso inverso dall'ultimo nodo (ultimo figlio) al primo nodo (nodo radice) in opposizione a Forward Kinematics (FK) in cui la cinematica attraversa dal nodo radice all'ultimo bambino.

Tutte le catene iniziano con il nodo radice. Questo nodo radice è il genitore agente a cui è collegato un nuovo nodo figlio. A sua volta, questo primo figlio diventerà padre del secondo figlio della catena e questo verrà ripetuto fino all'aggiunta dell'ultimo figlio. L'animazione in basso mostra una tale relazione.


Passaggio 2: ricordare le relazioni

Il IKshape la classe implementa la nozione di nodo nella nostra catena. Le istanze della classe IKshape ricordano i loro nodi padre e figlio, con le eccezioni del nodo radice che non ha un nodo genitore e l'ultimo nodo che non ha alcun nodo figlio. Di seguito sono elencate le proprietà private di IKshape.

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

Gli accessori di queste proprietà sono mostrati come di seguito:

 set di funzioni pubbliche IKchild (childSprite: IKshape): void childNode = childSprite;  public function get IKchild (): IKshape return childNode set di funzioni pubbliche IKparent (parentSprite: IKshape): void parentNode = parentSprite;  public function ottiene IKparent (): IKshape return parentNode; 

Passaggio 3: Vector da Child a Parent

Si può notare che questa classe memorizza un Vector2D che punta dal nodo figlio al nodo genitore. La logica di questa direzione è dovuta al movimento che scorre da un bambino all'altro. Vector2D viene utilizzato perché la grandezza e la direzione del vettore che punta da bambino a genitore verranno manipolate frequentemente mentre si implementa il comportamento di una catena IK. Pertanto, è necessario tenere traccia di tali dati. Di seguito sono riportati i metodi per manipolare le quantità vettoriali per IKshape.

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

Passaggio 4: Nodo di disegno

Ultimo ma non meno importante, abbiamo bisogno di un metodo per disegnare la nostra forma. Dovremo disegnare un rettangolo per rappresentare ciascun nodo. Tuttavia, qualsiasi altra preferenza può essere inserita sovrascrivendo il metodo di disegno qui. Iv includeva un esempio di una classe che sovrascriveva il metodo di estrazione di default, la classe Ball. (Alla fine di questo tutorial verrà mostrato un rapido passaggio tra le forme.) Con questo, completiamo la creazione della classe Ikshape.

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

Passaggio 5: La catena IK

La classe IKin implementa il comportamento di una catena IK. Le spiegazioni relative a questa classe seguono questo ordine

  1. Introduzione alle variabili private in questa classe.
  2. Metodi di base utilizzati in questa classe.
  3. Spiegazione matematica sul funzionamento di funzioni specifiche.
  4. Implementazione di quelle funzioni specifiche.

Passaggio 6: i dati in una catena

Il codice seguente mostra le variabili private della classe IKine.

 private var IKineChain: Vector.; // membri della catena // Struttura dei dati per i vincoli private var constraintDistance: Vector.; // distanza tra i nodi private var constraintRangeStart: Vector.; // inizio della libertà di rotazione private var constraintRangeEnd: Vector.; // fine della libertà di rotazione

Step 7: Istanziare la catena

La catena IKine memorizzerà un tipo di dati Sprite che ricorda la relazione tra genitore e figlio. Questi sprite sono istanze di IKshape. La catena risultante vede il nodo radice nell'indice 0, il prossimo figlio nell'indice 1 ,? fino all'ultimo figlio in modo sequenziale. Tuttavia, la costruzione della catena non è dalla radice all'ultimo figlio; è dall'ultimo figlio alla radice.

Supponendo che la catena sia di lunghezza n, la costruzione segue questa sequenza: n-esimo nodo, (n-1) -th nodo, (n-2) -th nodo? 0 ° nodo. L'animazione sotto mostra questa sequenza.

All'istanziazione della catena IK, viene inserito l'ultimo nodo. I nodi principali verranno aggiunti in seguito. L'ultimo nodo aggiunto è la radice. Il codice seguente sono metodi di costruzione della catena IK, che aggiungono e rimuovono i nodi alla catena.

 funzione pubblica IKine (lastChild: IKshape, distance: Number) // avvia tutte le variabili private IKineChain = new Vector.(); constraintDistance = new Vector.(); constraintRangeStart = new Vector.(); constraintRangeEnd = new Vector.(); // Imposta i vincoli this.IKineChain [0] = lastChild; this.constraintDistance [0] = distanza; this.constraintRangeStart [0] = 0; this.constraintRangeEnd [0] = 0;  / * Metodi per manipolare 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 (distanza); 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); 

Passaggio 8: ottenere nodi a catena

Questi metodi seguenti vengono utilizzati per recuperare i nodi dalla catena ogni volta che ce n'è bisogno.

 funzione pubblica 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]; 

Passaggio 9: vincoli

Abbiamo visto come la catena di nodi viene rappresentata in un array: il nodo radice all'indice 0 ,? (n-1) -th nodo all'indice (n-2), n-esimo nodo all'indice (n-1), n ​​essendo la lunghezza della catena. Possiamo anche organizzare i nostri vincoli in questo ordine. I vincoli si presentano in due forme: distanza tra i nodi e grado di libertà di flessione tra i nodi.

La distanza da mantenere tra i nodi è riconosciuta come vincolo del nodo figlio sul suo genitore. Per comodità di riferimento, possiamo memorizzare questo valore come constraintDistance array con indice simile a quello del nodo figlio. Si noti che il nodo radice non ha genitore. Tuttavia, il vincolo di distanza deve essere registrato dopo aver aggiunto il nodo radice in modo che se la catena viene estesa in seguito, il "genitore" appena aggiunto di questo nodo radice possa utilizzare i suoi dati.

Successivamente, l'angolo di piegatura per un nodo genitore è limitato a un intervallo. Memorizzeremo il punto iniziale e finale per la gamma in constraintRangeStart e ConstraintRangeEnd array. La figura seguente mostra un nodo figlio in verde e due nodi padre in blu. Solo il nodo contrassegnato con "OK" è consentito perché si trova all'interno del vincolo di angolo. Possiamo usare un approccio simile nel referenziare i valori in questi array. Nota ancora che i vincoli di angolo del nodo radice dovrebbero essere registrati anche se non in uso a causa di una logica simile a quella precedente. Inoltre, i vincoli di angolo non si applicano all'ultimo bambino perché vogliamo flessibilità nel controllo.


Passaggio 10: vincoli: acquisizione e impostazione

I metodi descritti di seguito potrebbero rivelarsi utili quando si sono avviati vincoli su un nodo ma si desidera modificare il valore in futuro.

 / * Manipolazione dei vincoli corrispondenti * / 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; 

Passo 11: Vincolo di lunghezza, concetto

яLa seguente animazione mostra il calcolo del vincolo di lunghezza.


Passaggio 12: Vincolo di lunghezza, formula

In questo passaggio, daremo un'occhiata ai comandi in un metodo che aiuta a limitare la distanza tra i nodi. Nota le linee evidenziate. Potresti notare che solo l'ultimo figlio ha applicato questo vincolo. Bene, per quanto riguarda il comando, questo è vero. I nodi parentali sono richiesti per soddisfare non solo la lunghezza ma i vincoli di angolo. Tutti questi sono gestiti con l'implementazione del metodo vecWithinRange (). L'ultimo bambino non deve essere vincolato in angolo perché abbiamo bisogno della massima flessibilità di piegatura.

 funzione privata updateParentPosition (): void for (var i: uint = IKineChain.length - 1; i> 0; i--) IKineChain [i] .calcVec2Parent (); var vec: Vector2D; // gestire l'ultimo figlio 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; 

Step 13: Angle Constraint, Concept

Innanzitutto, calcoliamo l'angolo corrente inserito tra i due vettori, vec1 e vec2. Se l'angolo non rientra nell'intervallo limitato, assegna il limite minimo o massimo all'angolo. Una volta definito un angolo, possiamo calcolare un vettore che viene ruotato da vec1 insieme al vincolo della distanza (magnitudine).

яLa seguente animazione offre un'altra alternativa alla visualizzazione dell'idea.


Passaggio 14: Vincolo dell'angolo, formula

L'implementazione dei vincoli angolari è la seguente.

private function vecWithinRange (currentNode: Number): Vector2D // ottenendo i vettori appropriati var child2Me: Vector2D = IKineChain [currentNode] .ikchild.getVec2Parent (); var me2Parent: Vector2D = IKineChain [currentNode] .getVec2Parent (); // Limite dei limiti dell'angolo dell'attrezzo 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); // Implementare la limitazione della distanza child2Me.setMagnitude (this.constraintDistance [currentNode]); child2Me.rotate (limitedAng); return child2Me

Step 15: Angolo con le direzioni

Forse è degno di passare da qui l'idea di ottenere un angolo che interpreti in senso orario e antiorario. L'angolo inserito tra due vettori, ad esempio vec1 e vec2, può essere facilmente ottenuto dal prodotto punto di quei due vettori. L'uscita sarà l'angolo più breve per ruotare da vec1 a vec2. Tuttavia, non vi è alcun concetto di direzione in quanto la risposta è sempre positiva. Pertanto la modifica sull'output regolare dovrebbe essere eseguita. Prima di produrre l'angolo, ho usato il prodotto vettoriale tra vec1 e vec2 per determinare se la sequenza corrente è positiva o negativa e ha incorporato il segno nell'angolo. Ho evidenziato la caratteristica direzionale nelle righe di codice qui sotto.

 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; 

Passaggio 16: orientamento dei nodi

I nodi che sono scatole devono essere orientati alla direzione dei loro vettori in modo che siano belli. Altrimenti, vedrai una catena come sotto. (Usa le frecce per muoverti.)

La funzione seguente implementa il corretto orientamento dei nodi.

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

Passaggio 17: ultimo bit

Ora che tutto è impostato, possiamo animare la nostra catena usando animare(). Questa è una funzione composita che effettua chiamate a updateParentPosition () e updateOrientation (). Tuttavia, prima che questo possa essere raggiunto, dobbiamo aggiornare le relazioni su tutti i nodi. Facciamo una chiamata a updateRelationships (). Ancora, updateRelationships () è una funzione composita che effettua chiamate a defineParent () e defineChild (). Questo viene fatto una volta e ogni volta che c'è una modifica nella struttura della catena, ad esempio i nodi vengono aggiunti o rilasciati in fase di runtime.


Passaggio 18: Metodi essenziali in IKine

Per far sì che la classe IKine lavori per te, questi sono i pochi metodi che dovresti esaminare. Li ho documentati sotto forma di tabella.

Metodo Parametri di input Ruolo
IKine () lastChild: IKshape, distance: Number Costruttore.
appendNode () nodeNext: IKshape, [distance: Number, angleStart: Number, angleEnd: Number] aggiungi nodi alla catena, definisci i vincoli implementati dal nodo.
updateRelationships () Nessuna Aggiorna le relazioni genitore-figlio per tutti i nodi.
animare() Nessuna Ricalcolo della posizione di tutti i nodi in catena. Deve essere chiamato ogni frame.

Nota che gli input dell'angolo sono in radianti non gradi.


Passaggio 19: creazione di un serpente

Ora consente di creare un progetto in FlashDevelop. Nella cartella src vedrai Main.as. Questa è la sequenza di attività che dovresti fare:

  1. Iniziare copie di IKshape o classi che si estendono da IKshape sul palco.
  2. Avvia IKine e usalo per mettere in fila copie di IKshape sul palco.
  3. Aggiorna relazioni su tutti i nodi in catena.
  4. Implementare i controlli utente.
  5. Animare!

Passaggio 20: Disegna gli oggetti

L'oggetto viene disegnato mentre costruiamo IKshape. Questo è fatto in un ciclo. Nota se vuoi modificare l'outlook del disegno in una cerchia, abilita il commento sulla riga 56 e disabilita il commento sulla linea 57. (Dovrai scaricare i miei file sorgente affinché funzioni).

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

Passo 21: Inizializzazione della catena

Prima di inizializzare la classe IKine per costruire la catena, vengono create le variabili private di Main.as.

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

Per il caso qui, tutti i nodi sono vincolati ad una distanza di 40 tra i nodi.

 funzione privata initChain (): void this.lastNode = this.getChildByName ("b" + (totalNodes - 1)) come IKshape; currentChain = new IKine (lastNode, 40); per (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 

Passaggio 22: aggiungere i controlli della tastiera

Successivamente, dichiariamo le variabili che devono essere utilizzate dal nostro controllo da tastiera.

 private var leadingVec: Vector2D; private var currentMagnitude: Number = 0; private var currentAngle: Number = 0; private var increaseAng: Number = 5; private var increaseMag: Number = 1; diminuzione var privataMag: Number = 0.8; private var capMag: Number = 10; private var pressedUp: Boolean = false; private var pressedLeft: Boolean = false; private var pressedRight: Boolean = false;

Attacca sul palco il loop principale e gli ascoltatori della tastiera. Li ho evidenziati.

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

Scrivi gli ascoltatori.

 funzione privata handleEnterFrame (e: Event): void if (pressedUp == true) currentMagnitude + = increaseMag; currentMagnitude = Math.min (currentMagnitude, capMag);  else currentMagnitude * = decreasMag;  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; 

Si noti che ho utilizzato un'istanza di Vector2D per guidare il serpente che si muove sul palco. Ho anche limitato questo vettore all'interno del limite dello stage in modo che non si sposti. L'ActionScript che esegue questo vincolo è evidenziato.


Passaggio 23: animazione!

Premi Ctrl + Invio per vedere il tuo serpente animato !. Controlla il suo movimento usando i tasti freccia.


Conclusione

Questo tutorial richiede alcune conoscenze in Vector Analysis. Per i lettori che vorrebbero avere uno sguardo familiare ai vettori, avere una lettura sul post di Daniel Sidhon. Spero che questo ti aiuti a comprendere e implementare la cinematica inversa. Grazie per la lettura. Fai cadere suggerimenti e commenti come sono sempre desiderosi di ascoltare dal pubblico. Terima Kasih.