Take the 2-minute tour ×
Game Development Stack Exchange is a question and answer site for professional and independent game developers. It's 100% free, no registration required.

I want to have a playable character who can "walk" on an organic surface at any angle, including sideways and upside-down. By "organic" levels with slanted and curved features instead of straight lines at 90-degree angles.

I'm currently working in AS3 (moderate amateur experience) and using Nape (pretty much a newbie) for basic gravity-based physics, to which this walking mechanic will be an obvious exception.

Is there a procedural way to do this kind of walk mechanic, perhaps using Nape constraints? Or would it be best to create explicit walking "paths" following the contours of the level surfaces and use them to constrain the walking movement?

share|improve this question
    
To clarify: You want to make your character able to 'stick' on the walls and ceilings in your level? –  Qqwy Jan 11 '13 at 15:47
    
That's correct. –  eclecto Jan 11 '13 at 15:53
add comment

2 Answers 2

up vote 7 down vote accepted

Here is my complete learning experience, resulting in a pretty much functional version of the movement I wanted, all using Nape's internal methods. All of this code is within my Spider class, pulling some properties from its parent, a Level class.

Most of the other classes and methods are part of the Nape package. Here's the pertinent part of my import list:

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;

First, when the spider is added to the stage, I add listeners to the Nape world for collisions. As I get further into development I will need to differentiate collision groups; for the moment, these callbacks will technically be run when ANY body collides with any other body.

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

The callbacks change the spider's "state" property, which is a set of booleans, and record any Nape collision arbiters for later use in my walking logic. They also set and clear toTimer, which allows the spider to lose contact with the level surface for up to 100ms before allowing world gravity to take hold again.

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

Finally, I calculate what forces to apply to the spider based on its state and its relationship to the level geometry. I'll mostly let the comments speak for themselves.

    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;

            }
        }
    }

The real sticky part I found was that the angle of movement needed to be in the actual desired direction of movement in a multiple contact point scenario where the spider reaches a sharp angle or sits in a deep valley. Especially since, given my summed vectors for the adhesion force, that force will be pulling AWAY from the direction we want to move instead of perpendicular to it, so we need to counteract that. So I needed logic to pick one of the contact points to use as the basis for the angle of the movement vector.

A side effect of the adhesion force's "pull" is a slight hesitance when the spider reaches a sharp concave angle/curve, but that's actually kind of realistic from a look-and-feel standpoint so unless it causes problems down the road I'll leave it as is. If I need to, I can use a variation on this method to calculate the adhesion force.

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

This logic is pretty much "perfect," inasmuch as so far it seems to be doing what I want it to do. There is a lingering cosmetic issue, however, in that if I try to align the spider's graphic to either the adhesion or movement forces I find that the spider ends up "leaning" in the direction of movement, which would be ok if he were a two-legged athletic sprinter but he's not, and the angles are highly susceptible to variations in the terrain, so the spider jitters when it goes over the slightest bump. I may pursue a variation on Byte56's solution, sampling the nearby landscape and averaging those angles, to make the spider's orientation smoother and more realistic.

share|improve this answer
1  
Well done, thanks for posting the details here for future visitors. –  Byte56 Jan 14 '13 at 19:34
add comment

How about making any "stick" surface a character touches apply a force along the inverse normal of the surface? The force remains as long as they're in contact with the surface and overrides gravity as long as it's active. So jumping off the ceiling will have the expected effect of dropping down to the floor.

You would probably want to implement some other features to make this work smoothly and be easier to implement. For example, instead of just what the character touches use a circle around the character and sum up the inverted normals. As this crappy paint image shows:

enter image description here

(The spider likeness represented is property of Byte56)

The blue lines are the inverse normals to the surface at that point. The green line is the summed force being applied to the spider. The red circle represents the range the spider is looking for normals to use.

This would allow for some bumpyness in the terrain without the spider "loosing its grip". Experiement on the circle size and shape for that matter, maybe just use a half circle oriented with the spider down, maybe just a rectangle that encompasses the legs.

This solution allows for you to keep the physics turned on, without needing to deal with specific paths the character can follow. It's also using information that's fairly easy to get and interpret (normals). Finally, it's dynamic. Even changing the shape of the world is easy to account for, since you can easily get normals for whatever geometry you're drawing.

Remember that when no faces are within range of the spider, normal gravity takes over.

share|improve this answer
    
Summed normals would probably solve the issues my current solution is having with sharp concave corners, but I'm not familiar with how you get them in AS3. –  eclecto Jan 14 '13 at 14:06
    
Sorry, I'm not familiar either. Possibly something you need to maintain yourself when you generate the terrain. –  Byte56 Jan 14 '13 at 14:21
2  
I've managed to implement this idea inasmuch as I can detect Nape's collision contact points and average them if there are more than one. It doesn't seem to be necessary for moving over flat or convex surfaces, but it did solve my biggest issue: what to do when my spider encounters a sharp corner. As mentioned in my new answer, I may try a variation on this idea to help orient my spider's graphic. –  eclecto Jan 14 '13 at 19:18
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.