Créer un serpent mécanique avec une cinématique inverse

Imaginez une chaîne de particules s'animant ensemble dans une symphonie: un train qui bouge alors que tous les compartiments attachés le suivent; une marionnette danse alors que son maître tire sa corde; même tes bras, quand tes parents te tiennent par la main alors qu’ils te conduisent le soir. Le mouvement se répercute du dernier nœud à l’origine, en respectant les contraintes. C'est cinématique inverse (IK), un algorithme mathématique qui calcule les mouvements nécessaires. Ici, nous allons l’utiliser pour créer un serpent un peu plus avancé que celui des jeux Nokia..


Aperçu du résultat final

Jetons un coup d'œil au résultat final sur lequel nous allons travailler. Appuyez et maintenez les touches HAUT, GAUCHE et DROITE pour le faire bouger.


Étape 1: relations dans une chaîne

Une chaîne est construite de nœuds. Chaque nœud représente un point de la chaîne où la translation et la rotation peuvent avoir lieu. Dans la chaîne IK, le mouvement se répercute en sens inverse du dernier nœud (dernier enfant) au premier nœud (nœud racine), par opposition à la cinématique de transfert avant (FK), où la cinématique traverse le nœud racine jusqu'au dernier enfant..

Toutes les chaînes commencent par le nœud racine. Ce nœud racine est le parent actif auquel un nouveau nœud enfant est attaché. À son tour, ce premier enfant sera le deuxième enfant de la chaîne et sera répété jusqu'à ce que le dernier enfant soit ajouté. L'animation ci-dessous décrit une telle relation.


Étape 2: Souvenir des relations

le IKshape La classe implémente la notion de noeud dans notre chaîne. Les instances de la classe IKshape mémorisent leurs nœuds parent et enfant, à l'exception du nœud racine qui n'a pas de nœud parent et du dernier nœud qui n'a pas de nœud enfant. Ci-dessous les propriétés privées de l'IKshape.

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

Les accesseurs de ces propriétés sont indiqués ci-dessous:

 ensemble de fonctions publiques IKchild (childSprite: IKshape): void childNode = childSprite;  fonction publique get IKchild (): IKshape return childNode ensemble de fonctions publiques IKparent (parentSprite: IKshape): void parentNode = parentSprite;  fonction publique get IKparent (): IKshape return parentNode; 

Étape 3: Vecteur d'un enfant à un parent

Vous remarquerez peut-être que cette classe stocke un Vector2D qui pointe d'un nœud enfant vers un nœud parent. La raison de cette orientation est due au mouvement de l’enfant au parent. Vector2D est utilisé parce que la magnitude et la direction du vecteur pointant de l'enfant au parent seront fréquemment manipulées lors de la mise en œuvre du comportement d'une chaîne IK. Il est donc nécessaire de garder trace de ces données. Vous trouverez ci-dessous des méthodes pour manipuler des quantités vectorielles pour IKshape..

 fonction publique calcVec2Parent (): void var xlength: Number = parentNode.x - this.x; var ylength: Number = parentNode.y - this.y; vec2Parent = new Vector2D (xlength, ylength);  fonction publique setVec2Parent (vec: Vector2D): void vec2Parent = vec.duplicate ();  fonction publique getVec2Parent (): Vector2D return vec2Parent.duplicate ();  fonction publique getAng2Parent (): Number return vec2Parent.getAngle (); 

Étape 4: Noeud de dessin

Enfin et surtout, nous avons besoin d’une méthode pour dessiner notre forme. Nous allons dessiner un rectangle pour représenter chaque nœud. Cependant, toutes les autres préférences peuvent être spécifiées en remplaçant la méthode de dessin ici. Iv a inclus un exemple de classe remplaçant la méthode de dessin par défaut, la classe Ball. (Un changement rapide entre les formes sera présenté à la fin de ce didacticiel.) Avec cela, nous terminons la création de la classe Ikshape..

 fonction protégée draw (): void var col: Number = 0x00FF00; var w: nombre = 50; var h: nombre = 10; graphics.beginFill (col); graphics.drawRect (-w / 2, -h / 2, w, h); graphics.endFill (); 

Étape 5: La chaîne IK

La classe IKine implémente le comportement d'une chaîne IK. L'explication concernant cette classe suit cet ordre

  1. Introduction aux variables privées dans cette classe.
  2. Méthodes de base utilisées dans cette classe.
  3. Explication mathématique sur le fonctionnement de fonctions spécifiques.
  4. Mise en œuvre de ces fonctions spécifiques.

Étape 6: Les données dans une chaîne

Le code ci-dessous montre les variables privées de la classe IKine.

 var privé IKineChain: vecteur.; // membres de la chaîne // Structure de données pour les contraintes private var constraintDistance: Vector.; // distance entre les nœuds private var constraintRangeStart: Vector.; // début de la liberté de rotation privée var constraintRangeEnd: Vector.; // fin de la liberté de rotation

Étape 7: Instancier la chaîne

La chaîne IKine stockera un type de données Sprite qui mémorise la relation entre ses parents et ses enfants. Ces sprites sont des exemples d'IKshape. La chaîne résultante voit le nœud racine à l'index 0, le prochain enfant à l'index 1 ,? jusqu'au dernier enfant de manière séquentielle. Cependant, la construction de la chaîne ne va pas de la racine au dernier enfant; c'est du dernier enfant à la racine.

En supposant que la chaîne ait une longueur n, la construction suit cette séquence: n-ième nœud, (n-1)-ème nœud, (n-2)-ème nœud? 0-ème nœud. L'animation ci-dessous décrit cette séquence.

Lors de l'instanciation de la chaîne IK, le dernier nœud est inséré. Les nœuds parents seront ajoutés ultérieurement. Le dernier noeud ajouté est la racine. Le code ci-dessous décrit les méthodes de construction de chaînes IK, d’ajout et de suppression de nœuds à la chaîne..

 fonction publique IKine (lastChild: IKshape, distance: nombre) // initialise toutes les variables privées IKineChain = nouveau vecteur.(); contrainteDistance = nouveau vecteur.(); constraintRangeStart = nouveau vecteur.(); constraintRangeEnd = nouveau vecteur.(); // Définit les contraintes this.IKineChain [0] = lastChild; this.constraintDistance [0] = distance; this.constraintRangeStart [0] = 0; this.constraintRangeEnd [0] = 0;  / * Méthodes pour manipuler la chaîne IK * / fonction publique appendNode (nodeNext: IKshape, distance: Nombre = 60, angleDébut: Nombre = -1 * Math.PI, angleEnd: Nombre = Math.PI): void this.IKineChain. unshift (nodeNext); this.constraintDistance.unshift (distance); this.constraintRangeStart.unshift (angleStart); this.constraintRangeEnd.unshift (angleEnd);  fonction publique removeNode (node: Number): void this.IKineChain.splice (node, 1); this.constraintDistance.splice (noeud, 1); this.constraintRangeStart.splice (noeud, 1); this.constraintRangeEnd.splice (noeud, 1); 

Étape 8: Obtention des nœuds de chaîne

Les méthodes suivantes sont utilisées pour récupérer des nœuds de la chaîne chaque fois que cela est nécessaire..

 fonction publique getRootNode (): IKshape return this.IKineChain [0];  fonction publique getLastNode (): IKshape return this.IKineChain [IKineChain.length - 1];  fonction publique getNode (node: Number): IKshape return this.IKineChain [node]; 

Étape 9: Contraintes

Nous avons vu comment la chaîne de nœuds est représentée dans un tableau: nœud racine à l'indice 0 ,? (n-1) -ème nœud à l'index (n-2), n-ème nœud à l'index (n-1), n ​​étant la longueur de la chaîne. Nous pouvons également organiser nos contraintes dans cet ordre. Les contraintes se présentent sous deux formes: distance entre les nœuds et degré de liberté de flexion entre les nœuds.

La distance à maintenir entre les nœuds est reconnue en tant que contrainte d'un nœud enfant sur son parent. Par souci de commodité de référencement, nous pouvons stocker cette valeur sous la forme suivante: contrainteDistance tableau avec index similaire à celui du nœud enfant. Notez que le nœud racine n'a pas de parent. Cependant, la contrainte de distance doit être enregistrée lors de l'ajout du nœud racine afin que, si la chaîne est étendue ultérieurement, le "parent" nouvellement ajouté de ce nœud racine puisse utiliser ses données..

Ensuite, l'angle de flexion d'un noeud parent est limité à une plage. Nous allons stocker le début et la fin de la plage dans constraintRangeStart et ConstraintRangeEnd tableau. La figure ci-dessous montre un nœud enfant en vert et deux nœuds parents en bleu. Seul le nœud marqué "OK" est autorisé car il se situe dans la contrainte d'angle. Nous pouvons utiliser une approche similaire pour référencer les valeurs dans ces tableaux. Notez à nouveau que les contraintes d'angle du nœud racine doivent être enregistrées même si elles ne sont pas utilisées pour des raisons similaires à celles décrites précédemment. De plus, les contraintes d'angle ne s'appliquent pas au dernier enfant, car nous souhaitons une souplesse de contrôle..


Étape 10: Contraintes: obtenir et définir

Les méthodes ci-après peuvent s'avérer utiles lorsque vous avez défini des contraintes sur un nœud mais que vous souhaitez modifier la valeur à l'avenir..

 / * Manipulation des contraintes correspondantes * / fonction publique getDistance (node: Number): Number return this.constraintDistance [node];  fonction publique setDistance (newDistance: Number, node: Number): void this.constraintDistance [node] = newDistance;  fonction publique getAngleStart (node: Number): Number return this.constraintRangeStart [noeud];  fonction publique setAngleStart (newAngleStart: Number, noeud: Number): void this.constraintRangeStart [node] = newAngleStart;  fonction publique getAngleRange (node: Number): Number return this.constraintRangeEnd [noeud];  fonction publique setAngleRange (newAngleRange: Number, noeud: Number): void this.constraintRangeEnd [noeud] = newAngleRange; 

Étape 11: Contrainte de longueur, Concept

L'animation suivante montre le calcul de la contrainte de longueur.


Étape 12: Contrainte de longueur, formule

Dans cette étape, nous examinerons les commandes d'une méthode permettant de limiter la distance entre les nœuds. Notez les lignes en surbrillance. Vous pouvez remarquer que seul le dernier enfant a été appliqué à cette contrainte. Eh bien, pour ce qui est de la commande, c'est vrai. Les nœuds parents doivent non seulement respecter les contraintes de longueur, mais aussi d’angle. Tous ces éléments sont traités avec la mise en œuvre de la méthode vecWithinRange (). Le dernier enfant n'a pas besoin d'être contraint en angle car nous avons besoin d'une flexibilité maximale.

 fonction privée updateParentPosition (): void pour (var i: uint = IKineChain.length - 1; i> 0; i--) IKineChain [i] .calcVec2Parent (); var vec: Vector2D; // gestion du dernier enfant if (i == IKineChain.length - 1) var: Number = IKineChain [i] .getAng2Parent (); vec = nouveau 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; 

Étape 13: Contrainte d’angle, Concept

Tout d'abord, nous calculons l'angle actuel pris en sandwich entre les deux vecteurs, vec1 et vec2. Si l'angle n'est pas compris dans la plage restreinte, affectez la limite minimale ou maximale à l'angle. Une fois qu'un angle est défini, nous pouvons calculer un vecteur qui tourne à partir de vec1 avec la contrainte de distance (magnitude).

L'animation suivante offre une autre alternative à la visualisation de l'idée.


Étape 14: Contrainte d’angle, formule

L'implémentation des contraintes d'angle est comme ci-dessous.

fonction privée vecWithinRange (currentNode: Number): Vector2D // obtenir les vecteurs appropriés var child2Me: Vector2D = IKineChain [currentNode] .IKchild.getVec2Parent (); var me2Parent: Vector2D = IKineChain [currentNode] .getVec2Parent (); // Implémentation de la limitation des limites d'angle 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); // Implémentation de la limitation de distance child2Me.setMagnitude (this.constraintDistance [currentNode]); child2Me.rotate (limitedAng); retourne child2Me

Étape 15: Angle avec les directions

Peut-être est-il digne d’examiner ici l’idée d’obtenir un angle qui interprète sens et sens opposés. L'angle pris en sandwich entre deux vecteurs, par exemple vec1 et vec2, peut être facilement obtenu à partir du produit scalaire de ces deux vecteurs. La sortie sera l'angle le plus court pour faire pivoter vec1 vers vec2. Cependant, il n'y a pas de notion de direction car la réponse est toujours positive. Par conséquent, une modification sur la sortie régulière doit être effectuée. Avant de sortir l'angle, j'ai utilisé le produit vectoriel entre vec1 et vec2 pour déterminer si la séquence en cours correspond à une rotation positive ou négative et incorporer le signe dans l'angle. J'ai mis en évidence la fonctionnalité directionnelle dans les lignes de code ci-dessous.

 fonction publique vectorProduct (vec2: Vector2D): Number return this.vec_x * vec2.y - this.vec_y * vec2.x;  fonction publique angleBetween (vec2: Vector2D): Number angle var: Number = Math.acos (this.normalise (). dotProduct (vec2.normalise ())); var vec1: Vector2D = this.duplicate (); if (vec1.vectorProduct (vec2) < 0)  angle *= -1;  return angle; 

Étape 16: Nœuds d'orientation

Les nœuds qui sont des boîtes doivent être orientés dans la direction de leurs vecteurs afin qu'ils aient fière allure. Sinon, vous verrez une chaîne comme ci-dessous. (Utilisez les touches fléchées pour vous déplacer.)

La fonction ci-dessous implémente la bonne orientation des nœuds.

 fonction privée updateOrientation (): void pour (var i: uint = 0; i < IKineChain.length - 1; i++)  var orientation:Number = IKineChain[i].IKchild.getVec2Parent().getAngle(); IKineChain[i].rotation = Math2.degreeOf(orientation);  

Étape 17: Dernier bit

Maintenant que tout est défini, nous pouvons animer notre chaîne en utilisant animer(). C’est une fonction composite qui appelle pour updateParentPosition () et updateOrientation (). Cependant, avant que cela puisse être réalisé, nous devons mettre à jour les relations sur tous les nœuds. Nous faisons un appel à updateRelationships (). Encore, updateRelationships () est une fonction composite qui appelle defineParent () et defineChild (). Ceci est fait une fois et chaque fois qu'il y a un changement dans la structure de la chaîne, par exemple des nœuds sont ajoutés ou supprimés au moment de l'exécution.


Étape 18: Méthodes essentielles dans IKine

Pour que la classe IKine fonctionne pour vous, voici les quelques méthodes que vous devriez étudier. Je les ai documentés sous forme de tableau.

Méthode Paramètres d'entrée Rôle
IKine () lastChild: IKshape, distance: nombre Constructeur.
appendNode () nodeNext: IKshape, [distance: Number, angleStart: Number, angleEnd: Number] ajouter des nœuds à la chaîne, définir les contraintes mises en œuvre par nœud.
updateRelationships () Aucun Mettre à jour les relations parent-enfant pour tous les nœuds.
animer() Aucun Recalculer la position de tous les nœuds dans la chaîne. Doit être appelé chaque image.

Notez que les entrées d'angle sont en radians et non en degrés.


Étape 19: Créer un serpent

Créons maintenant un projet dans FlashDevelop. Dans le dossier src, vous verrez Main.as. Voici la séquence de tâches à effectuer:

  1. Initiez des copies d'IKshape ou de classes s'étendant d'IKshape sur scène..
  2. Initiez IKine et utilisez-le pour chaîner des copies d'IKshape sur scène.
  3. Mettre à jour les relations sur tous les nœuds de la chaîne.
  4. Implémenter des contrôles utilisateur.
  5. Animer!

Étape 20: Dessiner des objets

L'objet est dessiné lorsque nous construisons IKshape. Cela se fait en boucle. Notez que si vous souhaitez modifier les perspectives du dessin en un cercle, activez les commentaires sur la ligne 56 et désactivez les commentaires sur la ligne 57. (Vous devrez télécharger mes fichiers source pour que cela fonctionne.)

 fonction privée drawObjects (): void pour (var i: uint = 0; i < totalNodes; i++)  var currentObj:IKshape = new IKshape(); //var currentObj:Ball = new Ball(); currentObj.name = "b" + i; addChild(currentObj);  

Étape 21: Initialiser la chaîne

Avant d’initialiser la classe IKine pour construire la chaîne, les variables privées de Main.as sont créées.

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

Pour le cas ici, tous les nœuds sont contraints à une distance de 40 entre les nœuds.

 fonction privée initChain (): void this.lastNode = this.getChildByName ("b" + (totalNodes - 1)) en tant que IKshape; currentChain = new IKine (lastNode, 40); pour (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 

Étape 22: Ajouter des contrôles de clavier

Ensuite, nous déclarons que les variables doivent être utilisées par notre contrôle au clavier.

 var privé LeadingVec: Vector2D; private var currentMagnitude: Nombre = 0; private var currentAngle: Number = 0; private var augmentationAng: Number = 5; augmentation privée var: Number = 1; variable privée décroissante: nombre = 0,8; private var capMag: Number = 10; private var pressureUp: Boolean = false; private var pressureLeft: Boolean = false; private var pressureRight: Boolean = false;

Fixez sur la scène la boucle principale et les écouteurs du clavier. Je les ai surlignés.

fonction privée init (e: Event = null): void removeEventListener (Event.ADDED_TO_STAGE, init); // point d'entrée this.drawObjects (); this.initChain (); LeadingVec = nouveau Vector2D (0, 0); stage.addEventListener (Event.ENTER_FRAME, handleEnterFrame); stage.addEventListener (KeyboardEvent.KEY_DOWN, handleKeyDown); stage.addEventListener (KeyboardEvent.KEY_UP, handleKeyUp);

Ecrivez les auditeurs.

 fonction privée handleEnterFrame (e: Event): void if (pressureUp == true) currentMagnitude + = augmentationMag; currentMagnitude = Math.min (currentMagnitude, capMag);  else currentMagnitude * = diminutionMag;  if (pressureLeft == true) currentAngle - = Math2.radianOf (augmentationAng);  if (pressureRight == true) currentAngle + = Math2.radianOf (augmentationAng);  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 ();  fonction privée handleKeyDown (e: KeyboardEvent): void if (e.keyCode == Keyboard.UP) coloredUp = true;  if (e.keyCode == Keyboard.LEFT) coloredLeft = true;  else if (e.keyCode == Keyboard.RIGHT) pressureRight = true;  fonction privée handleKeyUp (e: KeyboardEvent): void if (e.keyCode == Keyboard.UP) coloredUp = false;  if (e.keyCode == Keyboard.LEFT) coloredLeft = false;  else if (e.keyCode == Keyboard.RIGHT) pressureRight = false; 

Notez que j'ai utilisé une instance de Vector2D pour diriger le déplacement du serpent sur la scène. J'ai également contraint ce vecteur dans les limites de la scène pour qu'il ne sorte pas. Le script Action qui exécute cette contrainte est mis en surbrillance.


Étape 23: animer!

Appuyez sur Ctrl + Entrée pour voir votre serpent s'animer!. Contrôler son mouvement à l'aide des touches fléchées.


Conclusion

Ce tutoriel nécessite quelques connaissances en analyse vectorielle. Pour les lecteurs qui souhaitent se familiariser avec les vecteurs, veuillez lire le billet de Daniel Sidhon. J'espère que cela vous aidera à comprendre et à mettre en œuvre la cinématique inverse. Merci pour la lecture. Ne laissez pas tomber les suggestions et les commentaires, car je suis toujours impatient de connaître l’audience. Terima Kasih.