Ecco la mia esperienza di apprendimento completa, risultante in una versione praticamente funzionale del movimento che desideravo, il tutto usando i metodi interni di Nape. Tutto questo codice è all'interno della mia classe Spider, estraendo alcune proprietà dal suo genitore, una classe Level.
La maggior parte delle altre classi e metodi fa parte del pacchetto Nape. Ecco la parte pertinente del mio elenco di importazione:
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;
Innanzitutto, quando il ragno viene aggiunto al palco, aggiungo ascoltatori nel mondo della Nape per le collisioni. Man mano che avrò ulteriore sviluppo, dovrò differenziare i gruppi di collisione; per il momento, questi callback verranno tecnicamente eseguiti quando QUALSIASI corpo si scontra con qualsiasi altro 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);
I callback cambiano la proprietà "state" del ragno, che è un insieme di booleani, e registra qualsiasi arbitro di collisione della Nape per un uso successivo nella mia logica ambulante. Inoltre, impostano e cancellano il timer, che consente al ragno di perdere il contatto con la superficie piana per un massimo di 100 ms prima di consentire nuovamente la gravità mondiale.
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;
}
Infine, calcolo quali forze applicare al ragno in base al suo stato e alla sua relazione con la geometria del livello. Per lo più lascerò che i commenti parlino da soli.
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 vera parte appiccicosa che ho scoperto è che l'angolo di movimento doveva trovarsi nella direzione del movimento desiderata in uno scenario con punti di contatto multipli in cui il ragno raggiunge un angolo acuto o si trova in una valle profonda. Soprattutto da quando, dati i miei vettori sommati per la forza di adesione, quella forza tirerà LONTANO dalla direzione che vogliamo spostare invece che perpendicolare ad essa, quindi dobbiamo contrastarla. Quindi avevo bisogno della logica per scegliere uno dei punti di contatto da usare come base per l'angolo del vettore di movimento.
Un effetto collaterale del "tiro" della forza di adesione è una leggera esitazione quando il ragno raggiunge un angolo / curva concavo acuto, ma in realtà è un po 'realistico dal punto di vista estetico, quindi a meno che non causi problemi lungo la strada lascialo così com'è. Se necessario, posso utilizzare una variazione di questo metodo per calcolare la forza di adesione.
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;
}
Questa logica è praticamente "perfetta", in quanto sembra che stia facendo quello che voglio. C'è un problema estetico persistente, tuttavia, nel caso in cui se provo ad allineare la grafica del ragno alle forze di adesione o di movimento, trovo che il ragno finisca per "inclinarsi" nella direzione del movimento, il che sarebbe ok se fosse un velocista atletico a due zampe, ma non lo è, e gli angoli sono altamente sensibili alle variazioni del terreno, quindi il ragno trema quando supera il minimo dosso. Potrei perseguire una variazione sulla soluzione di Byte56, campionando il paesaggio vicino e mediando quegli angoli, per rendere l'orientamento del ragno più regolare e più realistico.