Wie kann ich einen Charakter in einem 2D-Plattformer auf unebenen Wänden laufen lassen?

11

Ich möchte einen spielbaren Charakter haben, der in jedem Winkel auf einer organischen Oberfläche "laufen" kann, auch seitwärts und verkehrt herum. Durch "organische" Ebenen mit schrägen und gekrümmten Merkmalen anstelle von geraden Linien in 90-Grad-Winkeln.

Ich arbeite derzeit in AS3 (moderate Amateur-Erfahrung) und benutze Nape (so ziemlich ein Neuling) für die grundlegende schwerkraftbasierte Physik, für die diese Laufmechanik eine offensichtliche Ausnahme sein wird.

Gibt es eine prozedurale Möglichkeit, diese Art von Laufmechanik durchzuführen, möglicherweise unter Verwendung von Nape-Einschränkungen? Oder wäre es am besten, explizite "Gehwege" zu erstellen, die den Konturen der ebenen Flächen folgen und diese verwenden, um die Gehbewegung einzuschränken?

Eric N.
quelle
Zur Verdeutlichung: Sie möchten, dass Ihr Charakter in Ihrem Level an den Wänden und Decken haften bleibt?
Qqwy
Das ist richtig.
Eric N

Antworten:

9

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.

Eric N.
quelle
1
Gut gemacht, danke, dass Sie die Details hier für zukünftige Besucher veröffentlicht haben.
MichaelHouse
8

Wie wäre es, wenn eine "Stock" -Oberfläche, die ein Charakter berührt, eine Kraft entlang der inversen Normalen der Oberfläche ausübt? Die Kraft bleibt so lange bestehen, wie sie mit der Oberfläche in Kontakt steht, und setzt die Schwerkraft außer Kraft, solange sie aktiv ist. Das Abspringen von der Decke hat also den erwarteten Effekt, auf den Boden zu fallen.

Sie möchten wahrscheinlich einige andere Funktionen implementieren, damit dies reibungslos funktioniert und einfacher zu implementieren ist. Verwenden Sie beispielsweise anstelle dessen, was der Charakter berührt, einen Kreis um den Charakter und fassen Sie die invertierten Normalen zusammen. Wie dieses beschissene Farbbild zeigt:

Geben Sie hier die Bildbeschreibung ein

(Die dargestellte Spinnenähnlichkeit ist Eigentum von Byte56)

Die blauen Linien sind die inversen Normalen zur Oberfläche an diesem Punkt. Die grüne Linie ist die summierte Kraft, die auf die Spinne ausgeübt wird. Der rote Kreis repräsentiert den Bereich, in dem die Spinne nach Normalen sucht.

Dies würde eine gewisse Unebenheit im Gelände ermöglichen, ohne dass die Spinne "ihren Griff verliert". Wenn Sie die Größe und Form des Kreises kennen, verwenden Sie möglicherweise nur einen Halbkreis, der mit der Spinne nach unten ausgerichtet ist, möglicherweise nur ein Rechteck, das die Beine umfasst.

Mit dieser Lösung können Sie die Physik eingeschaltet lassen, ohne sich mit bestimmten Pfaden befassen zu müssen, denen der Charakter folgen kann. Es werden auch Informationen verwendet, die ziemlich einfach zu erhalten und zu interpretieren sind (Normalen). Schließlich ist es dynamisch. Selbst das Ändern der Form der Welt ist leicht zu erklären, da Sie leicht Normalen für jede Geometrie erhalten können, die Sie zeichnen.

Denken Sie daran, dass die normale Schwerkraft die Kontrolle übernimmt, wenn sich keine Gesichter in Reichweite der Spinne befinden.

MichaelHouse
quelle
Summierte Normalen würden wahrscheinlich die Probleme lösen, die meine aktuelle Lösung mit scharfen konkaven Ecken hat, aber ich bin nicht vertraut damit, wie Sie sie in AS3 erhalten.
Eric N
Entschuldigung, ich bin auch nicht vertraut. Möglicherweise etwas, das Sie benötigen, um sich selbst zu pflegen, wenn Sie das Gelände erzeugen.
MichaelHouse
2
Ich habe es geschafft, diese Idee insofern umzusetzen, als ich die Kollisionskontaktpunkte von Nape erkennen und mitteln kann, wenn es mehr als einen gibt. Es scheint nicht notwendig zu sein, um sich über flache oder konvexe Oberflächen zu bewegen, aber es hat mein größtes Problem gelöst: Was tun, wenn meine Spinne auf eine scharfe Ecke stößt? Wie in meiner neuen Antwort erwähnt, kann ich versuchen, eine Variation dieser Idee zu verwenden, um die Grafik meiner Spinne zu orientieren.
Eric N