Jak zmusić postać do chodzenia po nierównych ścianach w platformówce 2D?

11

Chcę mieć grywalną postać, która może „chodzić” po organicznej powierzchni pod dowolnym kątem, w tym na boki i do góry nogami. Przez „organiczne” poziomy ze skośnymi i zakrzywionymi elementami zamiast linii prostych pod kątem 90 stopni.

Obecnie pracuję w AS3 (umiarkowane doświadczenie amatorskie) i używam Nape'a (całkiem początkującego) do podstawowej fizyki opartej na grawitacji, dla której ta mechanika chodzenia będzie oczywistym wyjątkiem.

Czy istnieje proceduralny sposób wykonania tego rodzaju mechaniki chodzenia, być może przy użyciu ograniczeń Nape'a? A może najlepiej byłoby utworzyć wyraźne „ścieżki” idące po konturach płaskich powierzchni i użyć ich do ograniczenia ruchu chodzenia?

Eric N.
źródło
Aby wyjaśnić: Chcesz, aby twoja postać mogła „przyklejać się” do ścian i sufitów na swoim poziomie?
Qqwy,
To jest poprawne.
Eric N

Odpowiedzi:

9

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.

Eric N.
źródło
1
Dobra robota, dziękuję za opublikowanie szczegółów tutaj dla przyszłych gości.
MichaelHouse
8

Co powiesz na wykonanie jakiejkolwiek „lepkiej” powierzchni, której dotknie postać, przyłoży siłę wzdłuż odwrotnej normalnej powierzchni? Siła pozostaje tak długo, jak długo styka się ona z powierzchnią i zastępuje grawitację, dopóki jest aktywna. Zatem zeskoczenie z sufitu spowoduje oczekiwany efekt upadku na podłogę.

Prawdopodobnie zechcesz zaimplementować inne funkcje, aby działało to płynnie i było łatwiejsze do wdrożenia. Na przykład zamiast tego, czego dotyka postać, użyj okręgu wokół postaci i podsumuj odwrócone normalne. Jak pokazuje ten gówniany obraz farby:

wprowadź opis zdjęcia tutaj

(Przedstawione podobieństwo pająka jest własnością Byte56)

Niebieskie linie są odwrotnymi normalnymi do powierzchni w tym punkcie. Zielona linia to sumowana siła przyłożona do pająka. Czerwone kółko oznacza zasięg, którego pająk szuka normalnych do użycia.

Pozwoliłoby to na pewną wyboistość w terenie bez pająka „tracącego przyczepność”. Jeśli chodzi o rozmiar i kształt koła, możesz po prostu użyć półkola skierowanego pająkiem w dół, a może po prostu prostokąta obejmującego nogi.

To rozwiązanie pozwala utrzymać włączoną fizykę, bez konieczności zajmowania się określonymi ścieżkami, którymi może podążać postać. Wykorzystuje również informacje, które dość łatwo można uzyskać i zinterpretować (normalne). Wreszcie jest dynamiczny. Nawet zmiana kształtu świata jest łatwa do rozliczenia, ponieważ można łatwo uzyskać normalne dla dowolnej rysowanej geometrii.

Pamiętaj, że gdy żadna twarz nie znajduje się w zasięgu pająka, przejmuje normalna grawitacja.

MichaelHouse
źródło
Podsumowując, normalne prawdopodobnie rozwiązałyby problemy mojego obecnego rozwiązania z ostrymi wklęsłymi narożnikami, ale nie wiem, jak je zdobyć w AS3.
Eric N
Przepraszam, ja też nie znam. Być może coś, co musisz utrzymać podczas generowania terenu.
MichaelHouse
2
Udało mi się wdrożyć ten pomysł, ponieważ mogę wykryć punkty kontaktowe kolizji Nape i uśrednić je, jeśli jest ich więcej niż jeden. Nie wydaje się to konieczne do poruszania się po płaskich lub wypukłych powierzchniach, ale rozwiązało mój największy problem: co zrobić, gdy mój pająk napotka ostry róg. Jak wspomniano w mojej nowej odpowiedzi, mogę wypróbować odmianę tego pomysłu, aby pomóc zorientować grafikę mojego pająka.
Eric N