Aqui está minha experiência completa de aprendizado, resultando em uma versão praticamente funcional do movimento que eu queria, todos usando os métodos internos da Nape. Todo esse código está dentro da minha classe Spider, puxando algumas propriedades de seu pai, uma classe Level.
A maioria das outras classes e métodos faz parte do pacote Nape. Aqui está a parte pertinente da minha lista de importação:
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;
Primeiro, quando a aranha é adicionada ao palco, eu adiciono ouvintes ao mundo da nuca para colisões. À medida que me aprofundar no desenvolvimento, precisarei diferenciar grupos de colisão; no momento, esses retornos de chamada tecnicamente serão executados quando QUALQUER corpo colidir com qualquer outro corpo.
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);
Os retornos de chamada alteram a propriedade "state" da aranha, que é um conjunto de booleanos, e registram quaisquer árbitros de colisão de nuca para uso posterior em minha lógica de caminhada. Eles também definem e limpam o Timer, o que permite que a aranha perca o contato com a superfície nivelada por até 100ms antes de permitir que a gravidade mundial se segure novamente.
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 quais forças aplicar à aranha com base em seu estado e sua relação com a geometria do nível. Vou deixar principalmente os comentários falarem por si.
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;
}
}
}
A verdadeira parte complicada que descobri foi que o ângulo de movimento precisava estar na direção de movimento real desejada em um cenário de múltiplos pontos de contato em que a aranha atinge um ângulo agudo ou fica em um vale profundo. Especialmente porque, dados meus vetores somados para a força de adesão, essa força estará se afastando da direção em que queremos mover, em vez de perpendicular a ela, por isso precisamos combater isso. Então, eu precisava de lógica para escolher um dos pontos de contato para usar como base para o ângulo do vetor de movimento.
Um efeito colateral da "força" da força de adesão é uma ligeira hesitação quando a aranha atinge um ângulo / curva côncava acentuado, mas isso é realmente realista do ponto de vista da aparência, a menos que cause problemas no caminho. deixe como está. Se precisar, posso usar uma variação desse método para calcular a força de adesão.
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;
}
Essa lógica é praticamente "perfeita", na medida em que até agora parece estar fazendo o que eu quero. No entanto, existe um problema estético prolongado: se eu tentar alinhar o gráfico da aranha às forças de adesão ou movimento, acho que a aranha acaba "inclinando-se" na direção do movimento, o que seria bom se ele fosse um velocista atlético de duas pernas, mas ele não é, e os ângulos são altamente suscetíveis a variações no terreno, de modo que a aranha treme quando passa pelo menor solavanco. Eu posso buscar uma variação na solução do Byte56, amostrando a paisagem próxima e calculando a média desses ângulos, para tornar a orientação da aranha mais suave e mais realista.