Come posso far camminare un personaggio su pareti irregolari in un platform 2D?


11

Voglio avere un personaggio giocabile in grado di "camminare" su una superficie organica in qualsiasi angolo, anche lateralmente e capovolto. Dai livelli "organici" con elementi inclinati e curvi anziché linee rette con angoli di 90 gradi.

Attualmente sto lavorando in AS3 (moderata esperienza amatoriale) e sto usando Nape (praticamente un principiante) per la fisica di base basata sulla gravità, a cui questa meccanica ambulante sarà un'ovvia eccezione.

Esiste un modo procedurale per eseguire questo tipo di meccanico di camminata, magari usando i vincoli della nuca? O sarebbe meglio creare espliciti "percorsi" di camminata seguendo i contorni delle superfici piane e usarli per limitare il movimento di camminata?


Per chiarire: vuoi rendere il tuo personaggio in grado di "attaccare" alle pareti e ai soffitti del tuo livello?
Qqwy,

È corretto.
Eric N,

Risposte:


9

Ecco la mia esperienza di apprendimento completa, risultante in una versione praticamente funzionale del movimento che desideravo, il tutto usando i metodi interni di Nape. Tutto questo codice è all'interno della mia classe Spider, estraendo alcune proprietà dal suo genitore, una classe Level.

La maggior parte delle altre classi e metodi fa parte del pacchetto Nape. Ecco la parte pertinente del mio elenco di importazione:

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;

Innanzitutto, quando il ragno viene aggiunto al palco, aggiungo ascoltatori nel mondo della Nape per le collisioni. Man mano che avrò ulteriore sviluppo, dovrò differenziare i gruppi di collisione; per il momento, questi callback verranno tecnicamente eseguiti quando QUALSIASI corpo si scontra con qualsiasi altro corpo.

        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);

I callback cambiano la proprietà "state" del ragno, che è un insieme di booleani, e registra qualsiasi arbitro di collisione della Nape per un uso successivo nella mia logica ambulante. Inoltre, impostano e cancellano il timer, che consente al ragno di perdere il contatto con la superficie piana per un massimo di 100 ms prima di consentire nuovamente la gravità mondiale.

    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;
    }

Infine, calcolo quali forze applicare al ragno in base al suo stato e alla sua relazione con la geometria del livello. Per lo più lascerò che i commenti parlino da soli.

    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 vera parte appiccicosa che ho scoperto è che l'angolo di movimento doveva trovarsi nella direzione del movimento desiderata in uno scenario con punti di contatto multipli in cui il ragno raggiunge un angolo acuto o si trova in una valle profonda. Soprattutto da quando, dati i miei vettori sommati per la forza di adesione, quella forza tirerà LONTANO dalla direzione che vogliamo spostare invece che perpendicolare ad essa, quindi dobbiamo contrastarla. Quindi avevo bisogno della logica per scegliere uno dei punti di contatto da usare come base per l'angolo del vettore di movimento.

Un effetto collaterale del "tiro" della forza di adesione è una leggera esitazione quando il ragno raggiunge un angolo / curva concavo acuto, ma in realtà è un po 'realistico dal punto di vista estetico, quindi a meno che non causi problemi lungo la strada lascialo così com'è. Se necessario, posso utilizzare una variazione di questo metodo per calcolare la forza di adesione.

    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;
    }

Questa logica è praticamente "perfetta", in quanto sembra che stia facendo quello che voglio. C'è un problema estetico persistente, tuttavia, nel caso in cui se provo ad allineare la grafica del ragno alle forze di adesione o di movimento, trovo che il ragno finisca per "inclinarsi" nella direzione del movimento, il che sarebbe ok se fosse un velocista atletico a due zampe, ma non lo è, e gli angoli sono altamente sensibili alle variazioni del terreno, quindi il ragno trema quando supera il minimo dosso. Potrei perseguire una variazione sulla soluzione di Byte56, campionando il paesaggio vicino e mediando quegli angoli, per rendere l'orientamento del ragno più regolare e più realistico.


1
Ben fatto, grazie per aver pubblicato i dettagli qui per i futuri visitatori.
MichaelHouse

8

Che ne dici di creare una superficie "stick" che un personaggio tocca applica una forza lungo la normale inversa della superficie? La forza rimane fintanto che sono in contatto con la superficie e sovrasta la gravità fintanto che è attiva. Quindi saltare dal soffitto avrà l'effetto previsto di cadere a terra.

Probabilmente vorrai implementare alcune altre funzionalità per farlo funzionare senza problemi ed essere più facile da implementare. Ad esempio, anziché semplicemente ciò che tocca il personaggio, usa un cerchio attorno al personaggio e riassumi le normali invertite. Come mostra questa immagine di vernice schifosa:

inserisci qui la descrizione dell'immagine

(La somiglianza di ragno rappresentata è di proprietà di Byte56)

Le linee blu sono le normali inverse alla superficie in quel punto. La linea verde è la forza sommata applicata al ragno. Il cerchio rosso rappresenta l'intervallo che il ragno sta cercando le normali da usare.

Ciò consentirebbe una certa irregolarità nel terreno senza che il ragno "perda la sua presa". Sperimenta le dimensioni e la forma del cerchio per quella materia, forse usa solo un semicerchio orientato con il ragno verso il basso, forse solo un rettangolo che racchiude le gambe.

Questa soluzione ti consente di mantenere attiva la fisica, senza dover affrontare percorsi specifici che il personaggio può seguire. Utilizza anche informazioni che sono abbastanza facili da ottenere e interpretare (normali). Finalmente è dinamico. Anche cambiare la forma del mondo è facile da spiegare, poiché puoi facilmente ottenere normali per qualsiasi geometria stai disegnando.

Ricorda che quando non ci sono facce nel raggio del ragno, la gravità normale prende il sopravvento.


Le normali sommate probabilmente risolverebbero i problemi che la mia attuale soluzione sta avendo con angoli acuti concavi, ma non ho familiarità con come li ottieni in AS3.
Eric N,

Mi dispiace, non ho nemmeno familiarità. Forse qualcosa di cui hai bisogno per mantenerti quando generi il terreno.
MichaelHouse

2
Sono riuscito a implementare questa idea in quanto posso rilevare i punti di contatto di collisione di Nape e valutarli in media se ce ne sono più di uno. Non sembra essere necessario per spostarsi su superfici piane o convesse, ma ha risolto il mio problema più grande: cosa fare quando il mio ragno incontra un angolo acuto. Come menzionato nella mia nuova risposta, posso provare una variante di questa idea per aiutare a orientare la grafica del mio ragno.
Eric N,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.