Aquí está mi experiencia de aprendizaje completa, resultando en una versión bastante funcional del movimiento que quería, todo utilizando los métodos internos de Nape. Todo este código está dentro de mi clase Spider, extrayendo algunas propiedades de su padre, una clase Level.
La mayoría de las otras clases y métodos son parte del paquete Nape. Aquí está la parte pertinente de mi lista de importación:
import flash.events.TimerEvent;
import flash.utils.Timer;
import nape.callbacks.CbEvent;
import nape.callbacks.CbType;
import nape.callbacks.InteractionCallback;
import nape.callbacks.InteractionListener;
import nape.callbacks.InteractionType;
import nape.callbacks.OptionType;
import nape.dynamics.Arbiter;
import nape.dynamics.ArbiterList;
import nape.geom.Geom;
import nape.geom.Vec2;
Primero, cuando la araña se agrega al escenario, agrego oyentes al mundo de Nape para colisiones. A medida que avance en el desarrollo, necesitaré diferenciar los grupos de colisión; por el momento, estas devoluciones de llamada se ejecutarán técnicamente cuando CUALQUIER cuerpo colisiona con cualquier otro cuerpo.
var opType:OptionType = new OptionType([CbType.ANY_BODY]);
mass = body.mass;
// Listen for collision with level, before, during, and after.
var landDetect:InteractionListener = new InteractionListener(CbEvent.BEGIN, InteractionType.COLLISION, opType, opType, spiderLand)
var moveDetect:InteractionListener = new InteractionListener(CbEvent.ONGOING, InteractionType.COLLISION, opType, opType, spiderMove);
var toDetect:InteractionListener = new InteractionListener(CbEvent.END, InteractionType.COLLISION, opType, opType, takeOff);
Level(this.parent).world.listeners.add(landDetect);
Level(this.parent).world.listeners.add(moveDetect);
Level(this.parent).world.listeners.add(toDetect);
/*
A reference to the spider's parent level's master timer, which also drives the nape world,
runs a callback within the spider class every frame.
*/
Level(this.parent).nTimer.addEventListener(TimerEvent.TIMER, tick);
Las devoluciones de llamada cambian la propiedad de "estado" de la araña, que es un conjunto de valores booleanos, y registran los árbitros de colisión de la nuca para su uso posterior en mi lógica de caminar. También establecen y desactivan el Temporizador, lo que permite que la araña pierda contacto con la superficie nivelada hasta 100 ms antes de permitir que la gravedad mundial se apodere nuevamente.
protected function spiderLand(callBack:InteractionCallback):void {
tArbiters = callBack.arbiters.copy();
state.isGrounded = true;
state.isMidair = false;
body.gravMass = 0;
toTimer.stop();
toTimer.reset();
}
protected function spiderMove(callBack:InteractionCallback):void {
tArbiters = callBack.arbiters.copy();
}
protected function takeOff(callBack:InteractionCallback):void {
tArbiters.clear();
toTimer.reset();
toTimer.start();
}
protected function takeOffTimer(e:TimerEvent):void {
state.isGrounded = false;
state.isMidair = true;
body.gravMass = mass;
state.isMoving = false;
}
Finalmente, calculo qué fuerzas aplicar a la araña en función de su estado y su relación con la geometría de nivel. Principalmente dejaré que los comentarios hablen por sí mismos.
protected function tick(e:TimerEvent):void {
if(state.isGrounded) {
switch(tArbiters.length) {
/*
If there are no arbiters (i.e. spider is in midair and toTimer hasn't expired),
aim the adhesion force at the nearest point on the level geometry.
*/
case 0:
closestA = Vec2.get();
closestB = Vec2.get();
Geom.distanceBody(body, lvBody, closestA, closestB);
stickForce = closestA.sub(body.position, true);
break;
// For one contact point, aim the adhesion force at that point.
case 1:
stickForce = tArbiters.at(0).collisionArbiter.contacts.at(0).position.sub(body.position, true);
break;
// For multiple contact points, add the vectors to find the average angle.
default:
var taSum:Vec2 = tArbiters.at(0).collisionArbiter.contacts.at(0).position.sub(body.position, true);
tArbiters.copy().foreach(function(a:Arbiter):void {
if(taSum != a.collisionArbiter.contacts.at(0).position.sub(body.position, true))
taSum.addeq(a.collisionArbiter.contacts.at(0).position.sub(body.position, true));
});
stickForce=taSum.copy();
}
// Normalize stickForce's strength.
stickForce.length = 1000;
var curForce:Vec2 = new Vec2(stickForce.x, stickForce.y);
// For graphical purposes, align the body (simulation-based rotation is disabled) with the adhesion force.
body.rotation = stickForce.angle - Math.PI/2;
body.applyImpulse(curForce);
if(state.isMoving) {
// Gives "movement force" a dummy value since (0,0) causes problems.
mForce = new Vec2(10,10);
mForce.length = 1000;
// Dir is movement direction, a boolean. If true, the spider is moving left with respect to the surface; otherwise right.
// Using the corrected "down" angle, move perpendicular to that angle
if(dir) {
mForce.angle = correctAngle()+Math.PI/2;
} else {
mForce.angle = correctAngle()-Math.PI/2;
}
// Flip the spider's graphic depending on direction.
texture.scaleX = dir?-1:1;
// Now apply the movement impulse and decrease speed if it goes over the max.
body.applyImpulse(mForce);
if(body.velocity.length > 1000) body.velocity.length = 1000;
}
}
}
La verdadera parte pegajosa que encontré fue que el ángulo de movimiento tenía que estar en la dirección de movimiento deseada real en un escenario de múltiples puntos de contacto donde la araña alcanza un ángulo agudo o se sienta en un valle profundo. Especialmente porque, dados mis vectores sumados para la fuerza de adhesión, esa fuerza se alejará de la dirección que queremos mover en lugar de perpendicular a ella, por lo que debemos contrarrestar eso. Así que necesitaba lógica para elegir uno de los puntos de contacto para usar como base para el ángulo del vector de movimiento.
Un efecto secundario del "tirón" de la fuerza de adhesión es una leve vacilación cuando la araña alcanza un ángulo / curva cóncavo nítido, pero en realidad es algo realista desde el punto de vista de la apariencia, así que a menos que cause problemas en el camino, déjalo como está. Si lo necesito, puedo usar una variación de este método para calcular la fuerza de adhesión.
protected function correctAngle():Number {
var angle:Number;
if(tArbiters.length < 2) {
// If there is only one (or zero) contact point(s), the "corrected" angle doesn't change from stickForce's angle.
angle = stickForce.angle;
} else {
/*
For more than one contact point, we want to run perpendicular to the "new" down, so we copy all the
contact point angles into an array...
*/
var angArr:Array = [];
tArbiters.copy().foreach(function(a:Arbiter):void {
var curAng:Number = a.collisionArbiter.contacts.at(0).position.sub(body.position, true).angle;
if (curAng < 0) curAng += Math.PI*2;
angArr.push(curAng);
});
/*
...then we iterate through all those contact points' angles with respect to the spider's COM to figure out
which one is more clockwise or more counterclockwise, depending, with some restrictions...
...Whatever, the correct one.
*/
angle = angArr[0];
for(var i:int = 1; i<angArr.length; i++) {
if(dir) {
if(Math.abs(angArr[i]-angle) < Math.PI)
angle = Math.max(angle, angArr[i]);
else
angle = Math.min(angle, angArr[i]);
}
else {
if(Math.abs(angArr[i]-angle) < Math.PI)
angle = Math.min(angle, angArr[i]);
else
angle = Math.max(angle, angArr[i]);
}
}
}
return angle;
}
Esta lógica es bastante "perfecta", ya que hasta ahora parece estar haciendo lo que yo quiero que haga. Sin embargo, hay un problema cosmético persistente, ya que si trato de alinear el gráfico de la araña con las fuerzas de adhesión o movimiento, encuentro que la araña termina "inclinada" en la dirección del movimiento, lo cual estaría bien si fuera un velocista atlético de dos patas, pero no lo es, y los ángulos son muy susceptibles a las variaciones en el terreno, por lo que la araña se agita cuando pasa por el más mínimo golpe. Puedo buscar una variación en la solución de Byte56, muestreando el paisaje cercano y promediando esos ángulos, para hacer que la orientación de la araña sea más suave y realista.