Voici mon expérience d'apprentissage complète, résultant en une version à peu près fonctionnelle du mouvement que je voulais, utilisant toutes les méthodes internes de Nape. Tout ce code est dans ma classe Spider, tirant certaines propriétés de son parent, une classe Level.
La plupart des autres classes et méthodes font partie du package Nape. Voici la partie pertinente de ma liste d'importation:
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;
Tout d'abord, lorsque l'araignée est ajoutée à la scène, j'ajoute des auditeurs au monde Nape pour les collisions. À mesure que j'avancerai dans le développement, je devrai différencier les groupes de collision; pour le moment, ces rappels seront techniquement exécutés lorsque n'importe quel corps entre en collision avec un autre corps.
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);
Les rappels modifient la propriété "state" de l'araignée, qui est un ensemble de booléens, et enregistrent tous les arbitres de collision Nape pour une utilisation ultérieure dans ma logique de marche. Ils ont également réglé et effacé la minuterie, ce qui permet à l'araignée de perdre le contact avec la surface de niveau pendant jusqu'à 100 ms avant de permettre à la gravité mondiale de se réinstaller.
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;
}
Enfin, je calcule les forces à appliquer à l'araignée en fonction de son état et de sa relation avec la géométrie de niveau. Je vais surtout laisser les commentaires parler d'eux-mêmes.
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 vraie partie collante que j'ai trouvée était que l'angle de mouvement devait être dans la direction réelle de mouvement souhaitée dans un scénario à points de contact multiples où l'araignée atteint un angle aigu ou se trouve dans une vallée profonde. D'autant plus que, compte tenu de mes vecteurs sommés pour la force d'adhésion, cette force va s'éloigner de la direction que nous voulons déplacer au lieu de la perpendiculaire à elle, nous devons donc contrecarrer cela. J'avais donc besoin de logique pour choisir l'un des points de contact à utiliser comme base pour l'angle du vecteur de mouvement.
Un effet secondaire de la "traction" de la force d'adhésion est une légère hésitation lorsque l'araignée atteint un angle / courbe concave, mais c'est en fait assez réaliste du point de vue de l'apparence, donc à moins que cela ne cause des problèmes sur la route, je vais laissez-le tel quel. Si nécessaire, je peux utiliser une variante de cette méthode pour calculer la force d'adhésion.
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;
}
Cette logique est à peu près «parfaite», dans la mesure où, jusqu'à présent, elle semble faire ce que je veux qu'elle fasse. Il y a cependant un problème cosmétique persistant: si j'essaie d'aligner le graphique de l'araignée sur les forces d'adhérence ou de mouvement, je trouve que l'araignée finit par "pencher" dans le sens du mouvement, ce qui serait bien s'il était un sprinter athlétique à deux pattes, mais il ne l'est pas, et les angles sont très sensibles aux variations du terrain, de sorte que l'araignée tremble quand elle passe sur la moindre bosse. Je peux poursuivre une variation sur la solution de Byte56, en échantillonnant le paysage voisin et en faisant la moyenne de ces angles, pour rendre l'orientation de l'araignée plus lisse et plus réaliste.