Hier ist meine vollständige Lernerfahrung, die zu einer ziemlich funktionalen Version der Bewegung führt, die ich wollte, alle unter Verwendung der internen Methoden von Nape. Der gesamte Code befindet sich in meiner Spider-Klasse und bezieht einige Eigenschaften von der übergeordneten Klasse, einer Level-Klasse.
Die meisten anderen Klassen und Methoden sind Teil des Nape-Pakets. Hier ist der relevante Teil meiner Importliste:
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;
Erstens, wenn die Spinne zur Bühne hinzugefügt wird, füge ich der Nape-Welt Hörer für Kollisionen hinzu. Wenn ich mich weiter entwickle, muss ich Kollisionsgruppen unterscheiden. Im Moment werden diese Rückrufe technisch ausgeführt, wenn ein Körper mit einem anderen Körper kollidiert.
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);
Die Rückrufe ändern die "state" -Eigenschaft der Spinne, bei der es sich um eine Reihe von Booleschen Werten handelt, und zeichnen alle Nape-Kollisionsschiedsrichter zur späteren Verwendung in meiner Lauflogik auf. Sie setzen und löschen auch toimer, wodurch die Spinne bis zu 100 ms lang den Kontakt zur ebenen Oberfläche verlieren kann, bevor die Schwerkraft der Welt wieder greifen kann.
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;
}
Schließlich berechne ich anhand ihres Zustands und ihrer Beziehung zur Ebenengeometrie, welche Kräfte auf die Spinne wirken sollen. Ich werde die Kommentare meistens für sich selbst sprechen lassen.
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;
}
}
}
Der wirklich klebrige Teil, den ich fand, war, dass der Bewegungswinkel in einem Szenario mit mehreren Kontaktpunkten, in dem die Spinne einen scharfen Winkel erreicht oder in einem tiefen Tal sitzt, in der tatsächlich gewünschten Bewegungsrichtung liegen musste. Zumal angesichts meiner summierten Vektoren für die Adhäsionskraft diese Kraft aus der Richtung, in die wir uns bewegen möchten, weggezogen wird, anstatt senkrecht dazu, also müssen wir dem entgegenwirken. Ich brauchte also Logik, um einen der Kontaktpunkte auszuwählen, die als Grundlage für den Winkel des Bewegungsvektors dienen sollten.
Ein Nebeneffekt des "Zuges" der Adhäsionskraft ist ein leichtes Zögern, wenn die Spinne einen scharfen konkaven Winkel / eine scharfe konkave Kurve erreicht lass es wie es ist. Wenn nötig, kann ich eine Variation dieser Methode verwenden, um die Adhäsionskraft zu berechnen.
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;
}
Diese Logik ist ziemlich "perfekt", da sie bisher das zu tun scheint, was ich möchte. Es gibt jedoch ein anhaltendes kosmetisches Problem: Wenn ich versuche, die Grafik der Spinne entweder auf die Adhäsions- oder die Bewegungskräfte auszurichten, stelle ich fest, dass sich die Spinne in Bewegungsrichtung "neigt", was in Ordnung wäre, wenn er eine wäre Zweibeiniger athletischer Sprinter, aber er ist es nicht, und die Winkel sind sehr anfällig für Variationen im Gelände, so dass die Spinne zittert, wenn sie über die geringste Beule fährt. Ich kann eine Variation der Byte56-Lösung verfolgen, indem ich die nahegelegene Landschaft abtastete und diese Winkel mittelte, um die Ausrichtung der Spinne glatter und realistischer zu gestalten.