That was suspiciously easy

Slope experiment continues apace. You can now run over little hills without being launched into the air. Take a look!

Accomplishing this was relatively straightforward. Basically, after each simulation step, you check to see if the character is in contact with the ground. If not, and there wasn’t a jump initiated, you probe underneath their feet with a ray cast, then adjust their position and velocity accordingly if there’s a slope underneath them. It looks like so:

// If we haven't jumped, stick to the ground unless it's a cliff
if ( _onGround && !_jumpInitiated && !_standingMonitor.IsStanding ) {
    float closestFraction = 1;
    Vector2 closestPoint = Vector2.Zero;
    Vector2 closestNormal = new Vector2();

    foreach (var start in new[] {
        GetStandingLocation() + new Vector2(Width / 2f, 0),
        GetStandingLocation() + new Vector2(-Width / 2f, 0),
    } ) {
        _world.RayCast((fixture, point, normal, fraction) => {
            if ( fixture.GetUserData().IsTerrain && fraction < closestFraction ) {
                closestFraction = fraction;
                closestPoint = point;
                closestNormal = normal;
            }
            return 1;
        }, start, start + new Vector2(0, .9f));
    }
    if ( closestPoint != Vector2.Zero ) {
        _body.Position = new Vector2(_body.Position.X, closestPoint.Y - Height / 2 - .01f);
        // tweak the velocity to match the normal of the stuck-to slope
        float speed = _body.LinearVelocity.Length();
        // The rotation won't work properly unless we flip the y axis
        closestNormal = new Vector2(closestNormal.X, -closestNormal.Y);
        Vector2 rotated = Vector2.Transform(closestNormal, Matrix.CreateRotationZ(_facingDirection == Direction.Left ? Projectile.PiOverTwo : -Projectile.PiOverTwo));
        _body.LinearVelocity = new Vector2(rotated.X, -rotated.Y) * speed;
    } else {
        _onGround = false;
        IsStanding = false;
    }
}

Sadly, it doesn't work 100% of the time. About one time in 10, if you're moving at absolutely top speed and you hit the downward slope just right coming off the table top, you'll still catch air. That's because the character has moved so far horizontally in one frame that the ground has fallen more than the probe distance (.9 tiles) away from their feet. But I think I can live with that for now and consider what I have so far a proof of concept.

Going full retard, touching the third rail, etc.

I really didn’t want it to come to this, but after playing Operation Smash I don’t see what choice I have in the matter. I’m talking, of course, about implementing slopes in my game. I whipped up a little test area to see what I was getting myself into, approximately. I don’t have any slopey tiles yet, so I’m just placing a ramp object into the physics engine and turning on debug mode so I can see it, represented as a green outline. The video below demonstrates why this isn’t simple:

As you can see, running up a slope that then abruptly transitions to a table top results in the character catching some air. The same is true when coming off that table top. This is the correct, physically simulated behavior, but it’s not what I want to happen. I want the character to remain glued to the ground until they jump, or until they drop off an actual ledge. There are a bunch of other complications, some of them simple to solve (like the character sliding downhill) and some of them arbitrarily complex. This is yet another demonstration of why it’s worth considering *not* using a physics engine to create platformer games, which often want non-physical behavior. Doing a little bit of research into what I was getting myself into, I came across a relevant forum post, quoted below:

The problem is Box2D rewards consistency, and old school platforms are anything but. They are usually implemented as hacks (beyond the basics, I mean).

It’s my belief you can recreate most of the feel in Box2D, by discarding large and large amounts of the physics. E.g. start doing your own velocity calculations instead of using ApplyForce. But the more you discard, the worst the physics becomes.

For the record, I’ve been manually applying linear velocities since the very beginning of the project. Another indie game dev chimes in:

Echoing the sentiment that all collision is a hack. The only time it isn’t is when everything in the world can be defined in some extremely regular and consistent way. Approximating characters as AABBs, circles, capsules, or spheres and adjusting their boundaries to fit sprites/models as they perform different animations breaks all the “expected behavior” that makes a physics engine go. Along the way, you pretty much always end up with game-specific stuff where you say something like “oh I want the player to be able to grab ledges, and crouch-walk through low areas” and then you have to add hacks that cross collision with animation and player state to get the effect down. Hence the motto of my old boss, “collision is gameplay.”

This sounds… nauseatingly familiar.

So, what’s my problem? Why am I giving myself this headache?

First, I just played the new indie Metroidvania Operation Smash, which has a ton of lovely organic environments that use sloped surfaces all over the goddamned place. It’s pretty great. Seriously, you should buy it. Slopes definitely allow your environments to look a lot more interesting and varied.

Second, and this is the more important factor, I’ve wanted sloped terrain since before I started serious work on the game. Slopes allow you to sprint through an area and change your elevation without jumping. That’s a pretty great feature, which Super Metroid makes use of to great effect. The only reason I banned slopes from the game was to make it simpler to implement and therefore faster to get done, which is laughable at this point in the development.

I’m calling this an experiment, where I have a week to convince myself that either slopes are great or totally not worth the effort. I honestly think it could go either way.

Demoing the dash booster

I know, I know: a “run faster” item is probably the single most hackneyed power up in the history of Metroidvanias. What can I say? I just really like how a nice dash adds to the control mechanics of a platformer. And of course it can break a certain kind of block. See it in action below:

This is a true dash, activated with a double-tap of the run button. This is in contrast to Super Metroid’s similar powerup, which just automatically kicks on once you hit a certain ground speed. I think the latter is actually better for Metroidvania planning purposes (since it requires a long runway), but a true dash adds a lot more to game play.

I’m not sure about whether this item will level up, so that each level-up you find makes it faster — or if it can be activated multiple times to get faster and faster boost speeds (similar to the “long runway” lock and key semantics of Super Metroid’s dash booster). I have it implemented as the “long runway” option at the moment, but I’m still mulling this one over.

On an implementation note, this is the first time I have had a reason to use Box2D’s broad-phase collision event. In theory it’s for exactly this kind of thing: warning you about an upcoming collision event before it actually takes place. In my case it looks like this:

private void OnBroadphaseDashCollision(ref FixtureProxy a, ref FixtureProxy b) {
    if ( _isDashing
            && (a.Fixture.GetUserData().IsPlayer || b.Fixture.GetUserData().IsPlayer)
            && (a.Fixture.GetUserData().IsDestructibleRegion || b.Fixture.GetUserData().IsDestructibleRegion) ) {
        Fixture f = a.Fixture.GetUserData().IsDestructibleRegion ? a.Fixture : b.Fixture;
        if ( (f.GetUserData().Destruction.DestroyedBy(EnceladusGame.DashDestructionFlag)) ) {
            Tile tile = TileLevel.CurrentLevel.GetTile(f.Body.Position);
            if ( tile != null ) {
                TileLevel.CurrentLevel.DestroyTile(tile);
            }
        }
    }
}

But of course that’s not good enough. Once you get moving fast enough, you stop getting warned about the receding edge of the wall that’s being chewed up and run smack into it on the same frame it was broken apart. This isn’t really Box2D’s fault, just a natural effect of how I’ve implemented the terrain in the game. To patch that up, I add an actual collision handler that ignores collision events that will result in a dash-booster-vulnerable block breaking:

private bool DashCollisionHandler(Fixture a, Fixture b, Contact contact) {
    if ( _isDashing ) {
        if ( b.GetUserData().IsTerrain && contact.GetPlayerNormal(_body).Y == 0 ) {
            FixedArray2 points;
            Vector2 normal;
            contact.GetWorldManifold(out normal, out points);
            var tile = TileLevel.CurrentLevel.GetCollidedTile(points[0], normal);
            if ( tile != null && TileLevel.CurrentLevel.IsTileDestroyedBy(tile, EnceladusGame.DashDestructionFlag) ) {
                TileLevel.CurrentLevel.DestroyTile(tile);
                return false;
            }
        }
    }
    return true;
}

Not the most straightforward feature to cram into Box2D, but definitely a long way from the hardest.

So can I jump or not? Determining if the ground is under your feet in Box2D

I now have a near-total grasp why the developers of the Box2D physics engine “caution” its use in platformer games. It’s really a poor match for a whole host of common interaction patterns, the most basic one being one of the most difficult (and important) to get right: is the character standing on solid ground?

The basic tutorial to answer this question doesn’t work properly, since counting collisions and separations with the character’s feet isn’t accurate in all cases, such as when bodies are created or destroyed the same simulation step as they touch the character’s body. When the count gets off, you’ll either have a character who can’t jump (broken game) or a character who can air-jump endlessly (sequence-broken game). The only thing to do is to verify, by examining the contacts touching the character’s body, whether there’s solid ground under their feet or not.

Except this doesn’t work properly in all cases, either, if you ever resize the polygon of the character’s body. Does your character need to duck, or to crawl? Then you’ll either need to a) resize or recreate his body when this happens, or b) use prism joints and multiple fixtures to achieve the same effect. The second option sounded like a rabbit hole to me, but I was still running into cases where I was getting non-physical behavior after a resize event, which you can see happening at around the 45-second mark below.

I’ve been coding this game, off and on, for over a year now, and I just this week finally figured out how to subdue this buggy behavior. I’m not going to kid myself that my fix won’t cause other bugs further down the line, but it at least addresses all the ones I know about. It consists of two changes:

  1. Don’t rely on Box2D to tell you about collision and separation events to determine whether the character is standing on solid ground. Instead, check manually every frame, using contact with the ground as a primary source and ray casts to make sure. This is the only reliable method I have found to answer the “can I jump” question when bodies are being created, destroyed, and resized.
  2. When resizing a body (e.g. to duck), Box2D can exhibit non-physical behavior, like bouncing it away from a surface it was already in contact with. To resolve this problem, ignore any updates from existing contact edges for one frame after the resize.

The latter change looks like this:

/*
 * Resizing the body sometimes results in our slight 
 * overlap with the floor, resulting in Box2D detecting 
 * a collision and bouncing the body upwards. To get 
 * around this, we just disable any existing contact 
 * edges with terrain for the next timestep.
 */
ContactEdge edge = _body.ContactList;
while ( edge != null ) {
    if ( edge.Contact.FixtureA.GetUserData().IsTerrain 
        || edge.Contact.FixtureB.GetUserData().IsTerrain ) {
        edge.Contact.Enabled = false;
    }
    edge = edge.Next;
}

Hopefully someone find these tips useful. For what it’s worth, if I were starting the project again today, I wouldn’t use a physics engine — my game doesn’t need realistic physics. What it does need is a collision engine, which Box2D does a decent job at. Unfortunately, it’s really hard to just take that one piece without grabbing more, and even that one piece is unreliable when bodies are frequently created, destroyed, or resized. Since these things happen pretty often in many platformers, I can’t really recommend Box2D (or maybe just Farseer) for this use case.