Oto moje pełne doświadczenie w nauce, dzięki czemu powstała całkiem funkcjonalna wersja pożądanego przeze mnie ruchu, wszystko przy użyciu wewnętrznych metod Nape. Cały ten kod należy do mojej klasy Spider, czerpiąc niektóre właściwości od jego rodzica, klasy Level.
Większość innych klas i metod jest częścią pakietu Nape. Oto istotna część mojej listy importów:
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;
Po pierwsze, kiedy pająk jest dodawany do sceny, dodaję słuchaczy do świata Nape w celu kolizji. W miarę rozwoju będę musiał różnicować grupy kolizji; na razie te wywołania zwrotne będą technicznie uruchamiane, gdy JAKIEKOLWIEK ciało zderzy się z innym ciałem.
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);
Oddzwaniania zmieniają właściwość „stanu” pająka, która jest zbiorem boolanów, i zapisują dowolnych arbitrów kolizji Nape do późniejszego wykorzystania w mojej logice chodzenia. Ustawiają także i usuwają toTimer, co pozwala pająkowi stracić kontakt z równą powierzchnią na czas do 100 ms, zanim znów pozwoli na to, by grawitacja świata znów się utrzymała.
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;
}
Na koniec obliczam, jakie siły zastosować do pająka na podstawie jego stanu i jego związku z geometrią poziomu. Przeważnie pozwolę, by komentarze mówiły same za siebie.
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;
}
}
}
Naprawdę lepką częścią, którą znalazłem, było to, że kąt ruchu musiał być w rzeczywistym pożądanym kierunku ruchu w scenariuszu z wieloma punktami kontaktowymi, w którym pająk osiąga ostry kąt lub siedzi w głębokiej dolinie. Zwłaszcza, że biorąc pod uwagę moje zsumowane wektory siły adhezji, siła ta będzie ciągnąć DALEJ od kierunku, w którym chcemy się poruszać, zamiast prostopadle do niej, więc musimy temu przeciwdziałać. Potrzebowałem więc logiki, aby wybrać jeden z punktów styku, który posłużyłby jako podstawa kąta wektora ruchu.
Efektem ubocznym „przyciągania” siły przyczepności jest niewielka niepewność, gdy pająk osiągnie ostry kąt / wklęsły kąt, ale w rzeczywistości jest to trochę realistyczne z punktu widzenia wyglądu, więc jeśli nie spowoduje problemów na drodze, zostaw to tak, jak jest. W razie potrzeby mogę użyć wariantu tej metody do obliczenia siły przyczepności.
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;
}
Ta logika jest prawie „idealna”, ponieważ wydaje się, że do tej pory robi to, co chcę. Pozostaje jednak kwestia kosmetyczna, która polega na tym, że jeśli spróbuję dopasować grafikę pająka do siły przyczepności lub siły ruchu, stwierdzę, że pająk „pochyla się” w kierunku ruchu, co byłoby w porządku, gdyby był dwunogi atletyczny sprinter, ale nie jest, a kąty są bardzo podatne na zmiany terenu, więc pająk drży, gdy przechodzi przez najmniejsze uderzenie. Mogę zająć się wariantem rozwiązania Byte56, próbkując pobliski krajobraz i uśredniając te kąty, aby orientacja pająka była płynniejsza i bardziej realistyczna.