Crea una serpiente mecánica con cinemática inversa.

Imagina una cadena de partículas que se animan juntas en sinfonía: un tren que se mueve como todos los compartimentos unidos siguen su ejemplo; un títere bailando mientras su amo tira de su cuerda; incluso tus brazos, cuando tus padres sostienen tus manos mientras te guían en un paseo nocturno. Movevment ondula desde el último nodo hasta el origen, cumpliendo las restricciones a medida que avanza. Esto es cinemática inversa (IK), un algoritmo matemático que calcula los movimientos necesarios. Aquí, lo usaremos para crear una serpiente un poco más avanzada que la de los juegos de Nokia..


Vista previa del resultado final

Echemos un vistazo al resultado final en el que trabajaremos. Mantenga presionadas las teclas ARRIBA, IZQUIERDA y DERECHA para hacer que se mueva.


Paso 1: Relaciones en una cadena

Una cadena está construida de nodos. Cada nodo representa un punto de la cadena en el que pueden ocurrir traslación y rotación. En la cadena IK, el movimiento se desplaza hacia abajo en sentido inverso desde el último nodo (último elemento secundario) al primer nodo (nodo raíz) en oposición a la cinemática directa (FK), donde la cinemática atraviesa desde el nodo raíz hasta el último elemento.

Todas las cadenas comienzan con el nodo raíz. Este nodo raíz es el padre activo al que se adjunta un nuevo nodo secundario. A su vez, este primer hijo criará al segundo hijo de la cadena, y esto se repite hasta que se agregue el último hijo. La siguiente animación muestra una relación de este tipo..


Paso 2: Recordando las relaciones

los IKshape La clase implementa la noción de un nodo en nuestra cadena. Las instancias de la clase IKshape recuerdan sus nodos padre e hijo, con las excepciones del nodo raíz que no tiene nodo padre y el último nodo que no tiene nodo hijo. A continuación se muestran las propiedades privadas de IKshape..

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

Los accesores de estas propiedades se muestran a continuación:

 conjunto de funciones públicas IKchild (childSprite: IKshape): void childNode = childSprite;  función pública obtener IKchild (): IKshape return childNode función pública establecido IKparent (parentSprite: IKshape): void parentNode = parentSprite;  función pública obtener IKparent (): IKshape return parentNode; 

Paso 3: Vector de niño a padre

Puede observar que esta clase almacena un Vector2D que apunta desde un nodo secundario a un nodo primario. La justificación de esta dirección se debe al movimiento que fluye del niño al padre. Vector2D se usa porque la magnitud y la dirección del vector que apunta del niño al padre se manipulará con frecuencia al implementar el comportamiento de una cadena IK. Por lo tanto, es necesario llevar un registro de tales datos. A continuación hay métodos para manipular cantidades vectoriales para IKshape..

 función pública calcVec2Parent (): void var xlength: Number = parentNode.x - this.x; var ylength: Number = parentNode.y - this.y; vec2Parent = nuevo Vector2D (xlength, ylength);  función pública setVec2Parent (vec: Vector2D): void vec2Parent = vec.duplicate ();  función pública getVec2Parent (): Vector2D return vec2Parent.duplicate ();  función pública getAng2Parent (): Number return vec2Parent.getAngle (); 

Paso 4: Nodo de dibujo

Por último, pero no menos importante, necesitamos un método para dibujar nuestra forma. Dibujaremos un rectángulo para representar cada nodo. Sin embargo, cualquier otra preferencia puede incluirse anulando el método de sorteo aquí. Iv incluyó un ejemplo de una clase que anulaba el método de sorteo predeterminado, la clase Ball. (Al final de este tutorial se mostrará un cambio rápido entre formas). Con esto, completamos la creación de la clase Ikshape.

 función protegida draw (): void var col: Number = 0x00FF00; var w: número = 50; var h: Número = 10; graphics.beginFill (col); graphics.drawRect (-w / 2, -h / 2, w, h); graphics.endFill (); 

Paso 5: La Cadena IK

La clase IKine implementa el comportamiento de una cadena IK. La explicación sobre esta clase sigue este orden.

  1. Introducción a las variables privadas en esta clase..
  2. Métodos básicos utilizados en esta clase..
  3. Explicación matemática sobre el funcionamiento de funciones específicas..
  4. Implementación de esas funciones específicas..

Paso 6: Los datos en una cadena

El siguiente código muestra las variables privadas de la clase IKine.

 var privado IKineChain: Vector.; // miembros de la cadena // Estructura de datos para restricciones privado var constricciónDistancia: Vector.; // distancia entre nodos privados var constricción de rangos de inicio: vector.; // inicio de la libertad de rotación private var constricciónRangeEnd: Vector.; // fin de la libertad de rotación

Paso 7: instanciar la cadena

La cadena IKine almacenará un tipo de datos Sprite que recuerda la relación de su padre y su hijo. Estos sprites son instancias de IKshape. La cadena resultante ve el nodo raíz en el índice 0, el siguiente elemento secundario en el índice 1 ,? Hasta el último hijo de manera secuencial. Sin embargo, la construcción de la cadena no es desde la raíz hasta el último hijo; es del último hijo a la raíz.

Suponiendo que la cadena es de longitud n, la construcción sigue esta secuencia: n-th nodo, (n-1) -th nodo, (n-2) -th nodo? Nodo 0-th. La siguiente animación muestra esta secuencia..

Tras la creación de instancias de la cadena IK, se inserta el último nodo. Los nodos padres se adjuntarán más adelante. El último nodo añadido es la raíz. El siguiente código son métodos de construcción de cadenas IK, agregando y eliminando nodos para encadenar.

 función pública IKine (lastChild: IKshape, distance: Number) // inicia todas las variables privadas IKineChain = nuevo vector.(); constricciónDistancia = nuevo vector.(); constricción de cambio = nuevo vector.(); constricciónRangeEnd = nuevo vector.(); // Establecer restricciones this.IKineChain [0] = lastChild; this.constraintDistance [0] = distancia; this.constraintRangeStart [0] = 0; this.constraintRangeEnd [0] = 0;  / * Métodos para manipular la cadena IK * / 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 (distance); 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); 

Paso 8: Obtención de nodos de cadena

Los siguientes métodos se utilizan para recuperar nodos de la cadena siempre que sea necesario..

 función pública getRootNode (): IKshape return this.IKineChain [0];  función pública getLastNode (): IKshape return this.IKineChain [IKineChain.length - 1];  función pública getNode (nodo: número): IKshape return this.IKineChain [nodo]; 

Paso 9: Restricciones

Hemos visto cómo se representa la cadena de nodos en una matriz: ¿Nodo raíz en el índice 0? (n-1) -th nodo en el índice (n-2), n-th nodo en el índice (n-1), n ​​es la longitud de la cadena. También podemos organizar convenientemente nuestras restricciones en ese orden. Las restricciones vienen en dos formas: distancia entre nodos y Grado de libertad de flexión entre nodos..

La distancia a mantener entre nodos se reconoce como una restricción de nodo secundario sobre su padre. Por conveniencia de referencia, podemos almacenar este valor como constricciónDistancia matriz con índice similar al de los nodos secundarios. Tenga en cuenta que el nodo raíz no tiene padre. Sin embargo, la restricción de distancia se debe registrar al agregar el nodo raíz, de modo que si la cadena se extiende más tarde, el "padre" recién agregado de este nodo raíz puede utilizar sus datos.

A continuación, el ángulo de flexión para un nodo padre se restringe a un rango. Almacenaremos el punto de inicio y final del rango en constricciónRangeStart y ConstraintRangeEnd formación. La siguiente figura muestra un nodo secundario en verde y dos nodos principales en azul. Solo se permite el nodo marcado "OK" porque se encuentra dentro de la restricción de ángulo. Podemos usar un enfoque similar para hacer referencia a los valores en estas matrices. Tenga en cuenta una vez más que las restricciones de ángulo del nodo raíz deben registrarse aunque no estén en uso debido a una lógica similar a la anterior. Además, las restricciones de ángulo no se aplican al último niño porque queremos flexibilidad en el control.


Paso 10: Restricciones: Obtención y configuración

Los siguientes métodos pueden resultar útiles cuando haya iniciado restricciones en un nodo pero le gustaría modificar el valor en el futuro.

 / * Manipulación de las restricciones correspondientes * / función pública getDistance (nodo: Número): Número return this.constraintDistance [nodo];  función pública setDistance (newDistance: Number, node: Number): void this.constraintDistance [node] = newDistance;  public function getAngleStart (node: Number): Number return this.constraintRangeStart [node];  función pública setAngleStart (newAngleStart: Number, node: Number): void this.constraintRangeStart [node] = newAngleStart;  función pública getAngleRange (node: Number): Number return this.constraintRangeEnd [node];  función pública setAngleRange (newAngleRange: Number, node: Number): void this.constraintRangeEnd [node] = newAngleRange; 

Paso 11: Restricción de longitud, concepto

яLa siguiente animación muestra el cálculo de la restricción de longitud.


Paso 12: Restricción de longitud, fórmula

En este paso, veremos los comandos en un método que ayuda a restringir la distancia entre nodos. Tenga en cuenta las líneas resaltadas. Puede notar que solo al último hijo se le aplica esta restricción. Bueno, hasta donde llega el comando, esto es cierto. Los nodos principales deben cumplir no solo la longitud, sino también las restricciones de ángulo. Todos estos se manejan con la implementación del método. vecWithininRange (). El último niño no necesita estar limitado en ángulo porque necesitamos la máxima flexibilidad de flexión.

 función privada updateParentPosition (): void for (var i: uint = IKineChain.length - 1; i> 0; i--) IKineChain [i] .calcVec2Parent (); var vec: Vector2D; // manejo del último hijo if (i == IKineChain.length - 1) var ang: Number = IKineChain [i] .getAng2Parent (); vec = nuevo 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; 

Paso 13: Restricción de ángulo, concepto

Primero, calculamos el ángulo actual entre los dos vectores, vec1 y vec2. Si el ángulo no está dentro del rango restringido, asigne el límite mínimo o máximo al ángulo. Una vez que se define un ángulo, podemos calcular un vector que se gira desde vec1 junto con la restricción de la distancia (magnitud).

яLa siguiente animación ofrece otra alternativa para visualizar la idea..


Paso 14: Restricción de ángulo, fórmula

La implementación de las restricciones de ángulo es la siguiente.

función privada vecWithinRange (currentNode: Number): Vector2D // obteniendo los vectores apropiados var child2Me: Vector2D = IKineChain [currentNode] .IKchild.getVec2Parent (); var me2Parent: Vector2D = IKineChain [currentNode] .getVec2Parent (); // Implementar la limitación de los límites de ángulo 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); // Implementar la limitación de distancia child2Me.setMagnitude (this.constraintDistance [currentNode]); child2Me.rotate (limitedAng); devuelve child2Me

Paso 15: Ángulo con las direcciones

Quizás sea digno de pasar por aquí la idea de obtener un ángulo que interprete en sentido horario y antihorario. El ángulo intercalado entre dos vectores, por ejemplo, vec1 y vec2, se puede obtener fácilmente a partir del producto puntual de esos dos vectores. La salida será el ángulo más corto para rotar vec1 a vec2. Sin embargo, no hay una noción de dirección ya que la respuesta es siempre positiva. Por lo tanto la modificación en la salida regular debe hacerse. Antes de emitir el ángulo, utilicé el producto vectorial entre vec1 y vec2 para determinar si la secuencia actual es de rotación positiva o negativa e incorporé el signo en el ángulo. He resaltado la característica direccional en las líneas de código a continuación.

 función pública vectorProduct (vec2: Vector2D): Number return this.vec_x * vec2.y - this.vec_y * vec2.x;  función pública 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; 

Paso 16: Orientando los Nodos

Los nodos que son recuadros deben orientarse en la dirección de sus vectores para que se vean bien. De lo contrario, verá una cadena como la de abajo. (Use las teclas de dirección para desplazarse.)

La siguiente función implementa la orientación correcta de los nodos..

 función privada 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);  

Paso 17: Último Bit

Ahora que todo está listo, podemos animar nuestra cadena usando animar(). Esta es una función compuesta que hace llamadas a updateParentPosition () y updateOrientation (). Sin embargo, antes de poder lograrlo, debemos actualizar las relaciones en todos los nodos. Hacemos una llamada a actualizaciónRelaciones (). Otra vez, actualizaciónRelaciones () Es una función compuesta que hace llamadas a defineParent () y defineChild (). Esto se hace una vez y siempre que haya un cambio en la estructura de la cadena, por ejemplo, los nodos se agregan o se eliminan en tiempo de ejecución.


Paso 18: Métodos esenciales en IKine

Para hacer que la clase de IKine funcione para usted, estos son los pocos métodos que debe considerar. Los he documentado en forma de tabla..

Método Parámetros de entrada Papel
IKine () lastChild: IKshape, distance: Number Constructor.
appendNode () nodeNext: IKshape, [distance: Number, angleStart: Number, angleEnd: Number] agregar nodos a la cadena, definir restricciones implementadas por nodo.
actualizaciónRelaciones () Ninguna Actualizar las relaciones padre-hijo para todos los nodos..
animar() Ninguna Recalcular la posición de todos los nodos en cadena. Debe llamarse cada fotograma.

Tenga en cuenta que las entradas de ángulo están en radianes no grados.


Paso 19: Creando una serpiente

Ahora vamos a crear un proyecto en FlashDevelop. En la carpeta src verá Main.as. Esta es la secuencia de tareas que debes hacer:

  1. Iniciar copias de IKshape o clases que se extiendan desde IKshape en el escenario..
  2. Inicie IKine y utilícelo para encadenar copias de IKshape en el escenario..
  3. Actualizar relaciones en todos los nodos en cadena.
  4. Implementar controles de usuario..
  5. Animar!

Paso 20: Dibujar objetos

El objeto se dibuja mientras construimos IKshape. Esto se hace en un bucle. Tenga en cuenta que si desea cambiar la perspectiva del dibujo a un círculo, active el comentario en la línea 56 y deshabilite el comentario en la línea 57. (Para que esto funcione, deberá descargar mis archivos de origen).

 función privada 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);  

Paso 21: Inicializar Cadena

Antes de inicializar la clase IKine para construir la cadena, se crean variables privadas de Main.as.

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

Para el caso aquí, todos los nodos están restringidos a una distancia de 40 entre nodos.

 función privada initChain (): void this.lastNode = this.getChildByName ("b" + (totalNodes - 1)) como IKshape; currentChain = new IKine (lastNode, 40); para (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 

Paso 22: Añadir controles de teclado

A continuación, declaramos variables para ser utilizadas por nuestro control de teclado..

 private var leadingVec: Vector2D; private var currentMagnitude: Number = 0; private var currentAngle: Number = 0; aumento de var privadoAng: Número = 5; private var IncreaseMag: Number = 1; privado var decremento: número = 0.8; private var capMag: Number = 10; var privado presionado: booleano = falso; private var pressLeft: Boolean = false; private var pressRight: Boolean = false;

Adjuntar al escenario el bucle principal y los oyentes del teclado. Los he destacado.

función privada init (e: Event = null): void removeEventListener (Event.ADDED_TO_STAGE, init); // punto de entrada 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);

Escribe a los oyentes.

 Función privada handleEnterFrame (e: Event): void if (PressUp == true) currentMagnitude + = IncreaseMag; currentMagnitude = Math.min (currentMagnitude, capMag);  else currentMagnitude * = disminuirMag;  if (PressLeft == true) currentAngle - = Math2.radianOf (IncreaseAng);  if (PressRight == 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 ();  la función privada handleKeyDown (e: KeyboardEvent): void if (e.keyCode == Keyboard.UP) pressUp = true;  if (e.keyCode == Keyboard.LEFT) pressLeft = true;  else if (e.keyCode == Keyboard.RIGHT) PressRight = true;  función privada handleKeyUp (e: KeyboardEvent): void if (e.keyCode == Keyboard.UP) PressUp = false;  if (e.keyCode == Keyboard.LEFT) pressLeft = false;  else if (e.keyCode == Keyboard.RIGHT) PressRight = false; 

Observa que he usado una instancia de Vector2D para guiar a la serpiente en movimiento alrededor del escenario. También he restringido este vector dentro del límite de la etapa para que no se mueva. El Actionscript que realiza esta restricción está resaltado.


Paso 23: Animar!

Presiona Ctrl + Enter para ver tu serpiente animada !. Controla su movimiento con las teclas de flecha..


Conclusión

Este tutorial requiere algunos conocimientos en análisis vectorial. Para los lectores que deseen obtener un aspecto familiar de los vectores, lea el post de Daniel Sidhon. Espero que esto te ayude a entender e implementar la cinemática inversa. Gracias por la lectura. Suelte las sugerencias y los comentarios ya que siempre estoy ansioso por escuchar de las audiencias. Terima Kasih.