Comment faire marcher un personnage sur des murs inégaux dans un jeu de plateforme 2D?

11

Je veux avoir un personnage jouable qui puisse "marcher" sur une surface organique à n'importe quel angle, y compris latéralement et à l'envers. Par des niveaux "organiques" avec des caractéristiques inclinées et courbes au lieu de lignes droites à des angles de 90 degrés.

Je travaille actuellement en AS3 (expérience amateur modérée) et utilise Nape (à peu près un débutant) pour la physique de base basée sur la gravité, à laquelle ce mécanicien de marche sera une exception évidente.

Existe-t-il un moyen procédural de faire ce type de mécanisme de marche, peut-être en utilisant des contraintes Nape? Ou serait-il préférable de créer des "chemins" de marche explicites en suivant les contours des surfaces planes et de les utiliser pour contraindre le mouvement de marche?

Eric N
la source
Pour clarifier: vous voulez rendre votre personnage capable de «coller» sur les murs et les plafonds de votre niveau?
Qqwy
C'est correct.
Eric N

Réponses:

9

Voici mon expérience d'apprentissage complète, résultant en une version à peu près fonctionnelle du mouvement que je voulais, utilisant toutes les méthodes internes de Nape. Tout ce code est dans ma classe Spider, tirant certaines propriétés de son parent, une classe Level.

La plupart des autres classes et méthodes font partie du package Nape. Voici la partie pertinente de ma liste d'importation:

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;

Tout d'abord, lorsque l'araignée est ajoutée à la scène, j'ajoute des auditeurs au monde Nape pour les collisions. À mesure que j'avancerai dans le développement, je devrai différencier les groupes de collision; pour le moment, ces rappels seront techniquement exécutés lorsque n'importe quel corps entre en collision avec un autre corps.

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

Les rappels modifient la propriété "state" de l'araignée, qui est un ensemble de booléens, et enregistrent tous les arbitres de collision Nape pour une utilisation ultérieure dans ma logique de marche. Ils ont également réglé et effacé la minuterie, ce qui permet à l'araignée de perdre le contact avec la surface de niveau pendant jusqu'à 100 ms avant de permettre à la gravité mondiale de se réinstaller.

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

Enfin, je calcule les forces à appliquer à l'araignée en fonction de son état et de sa relation avec la géométrie de niveau. Je vais surtout laisser les commentaires parler d'eux-mêmes.

    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 vraie partie collante que j'ai trouvée était que l'angle de mouvement devait être dans la direction réelle de mouvement souhaitée dans un scénario à points de contact multiples où l'araignée atteint un angle aigu ou se trouve dans une vallée profonde. D'autant plus que, compte tenu de mes vecteurs sommés pour la force d'adhésion, cette force va s'éloigner de la direction que nous voulons déplacer au lieu de la perpendiculaire à elle, nous devons donc contrecarrer cela. J'avais donc besoin de logique pour choisir l'un des points de contact à utiliser comme base pour l'angle du vecteur de mouvement.

Un effet secondaire de la "traction" de la force d'adhésion est une légère hésitation lorsque l'araignée atteint un angle / courbe concave, mais c'est en fait assez réaliste du point de vue de l'apparence, donc à moins que cela ne cause des problèmes sur la route, je vais laissez-le tel quel. Si nécessaire, je peux utiliser une variante de cette méthode pour calculer la force d'adhésion.

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

Cette logique est à peu près «parfaite», dans la mesure où, jusqu'à présent, elle semble faire ce que je veux qu'elle fasse. Il y a cependant un problème cosmétique persistant: si j'essaie d'aligner le graphique de l'araignée sur les forces d'adhérence ou de mouvement, je trouve que l'araignée finit par "pencher" dans le sens du mouvement, ce qui serait bien s'il était un sprinter athlétique à deux pattes, mais il ne l'est pas, et les angles sont très sensibles aux variations du terrain, de sorte que l'araignée tremble quand elle passe sur la moindre bosse. Je peux poursuivre une variation sur la solution de Byte56, en échantillonnant le paysage voisin et en faisant la moyenne de ces angles, pour rendre l'orientation de l'araignée plus lisse et plus réaliste.

Eric N
la source
1
Bravo, merci d'avoir publié les détails ici pour les futurs visiteurs.
MichaelHouse
8

Que diriez-vous de faire une surface "bâton" qu'un personnage touche appliquer une force le long de la normale inverse de la surface? La force reste aussi longtemps qu'ils sont en contact avec la surface et l'emporte sur la gravité tant qu'elle est active. Donc, sauter du plafond aura l'effet attendu de tomber au sol.

Vous voudrez probablement implémenter d'autres fonctionnalités pour rendre ce travail fluide et plus facile à implémenter. Par exemple, au lieu de ce que le personnage touche, utilisez un cercle autour du personnage et résumez les normales inversées. Comme le montre cette image de peinture merdique:

entrez la description de l'image ici

(La ressemblance d'araignée représentée est la propriété de Byte56)

Les lignes bleues sont les normales inverses de la surface à ce point. La ligne verte est la force additionnée appliquée à l'araignée. Le cercle rouge représente la plage que l'araignée recherche pour les normales à utiliser.

Cela permettrait une certaine bosse sur le terrain sans que l'araignée "perde son emprise". Expérience sur la taille et la forme du cercle, d'ailleurs, utilisez peut-être simplement un demi-cercle orienté avec l'araignée vers le bas, peut-être juste un rectangle qui englobe les jambes.

Cette solution vous permet de garder la physique activée, sans avoir à traiter de chemins spécifiques que le personnage peut suivre. Il utilise également des informations assez faciles à obtenir et à interpréter (normales). Enfin, c'est dynamique. Même la modification de la forme du monde est facile à prendre en compte, car vous pouvez facilement obtenir des normales pour la géométrie que vous dessinez.

N'oubliez pas que lorsqu'aucune face n'est à portée de l'araignée, la gravité normale prend le dessus.

MichaelHouse
la source
Les normales sommées résoudraient probablement les problèmes que ma solution actuelle rencontre avec les coins concaves pointus, mais je ne sais pas comment les obtenir dans AS3.
Eric N
Désolé, je ne suis pas familier non plus. Peut-être quelque chose dont vous avez besoin pour vous maintenir lorsque vous générez le terrain.
MichaelHouse
2
J'ai réussi à mettre en œuvre cette idée dans la mesure où je peux détecter les points de contact de collision de Nape et en faire la moyenne s'il y en a plus d'un. Il ne semble pas nécessaire de se déplacer sur des surfaces planes ou convexes, mais cela a résolu mon plus gros problème: que faire lorsque mon araignée rencontre un coin pointu. Comme mentionné dans ma nouvelle réponse, je peux essayer une variante de cette idée pour aider à orienter le graphique de mon araignée.
Eric N