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?

  • \$\begingroup\$ To clarify: You want to make your character able to 'stick' on the walls and ceilings in your level? \$\endgroup\$
    – Qqwy
    Commented Jan 11, 2013 at 15:47
  • \$\begingroup\$ That's correct. \$\endgroup\$
    – Eric N
    Commented Jan 11, 2013 at 15:53
  • \$\begingroup\$ Here's a question on how to do the same in Unity. \$\endgroup\$
    – Anko
    Commented Dec 14, 2014 at 21:46

2 Answers 2


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


            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;

    protected function spiderMove(callBack:InteractionCallback):void {
        tArbiters = callBack.arbiters.copy();

    protected function takeOff(callBack:InteractionCallback):void {

    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);
                // 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);
                // For multiple contact points, add the vectors to find the average angle.
                    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));

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


            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.
                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;
                ...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]);
                        angle = Math.min(angle, angArr[i]);
                else {
                    if(Math.abs(angArr[i]-angle) < Math.PI)
                        angle = Math.min(angle, angArr[i]);
                        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.

  • 1
    \$\begingroup\$ Well done, thanks for posting the details here for future visitors. \$\endgroup\$
    – House
    Commented Jan 14, 2013 at 19:34

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.

  • \$\begingroup\$ 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. \$\endgroup\$
    – Eric N
    Commented Jan 14, 2013 at 14:06
  • \$\begingroup\$ Sorry, I'm not familiar either. Possibly something you need to maintain yourself when you generate the terrain. \$\endgroup\$
    – House
    Commented Jan 14, 2013 at 14:21
  • 2
    \$\begingroup\$ 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. \$\endgroup\$
    – Eric N
    Commented Jan 14, 2013 at 19:18

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .