Box2D floor sensors with destructible terrain

I’ve been running into lots of interesting issues while experimenting with character control. One of the most basic ones in a platformer, which I have solved a handful of different ways so far, is the “can I jump” question. Basically, you only want to allow jumping if your character is standing on the ground — and if you’re allowing for air control of the character (I am), then you’ll also need to know whether it’s airborne to differentiate between moving on the ground v. in the air.

The way I started answering this question was to examine the list of contacts on the character’s Body object, then looking for one with a normal vector pointing straight up. This worked fine, but I was a little worried about scenarios I hadn’t envisioned, and it also seemed to be a relatively expensive calculation to perform every frame. The solution that most people online suggest, the above tutorial included, was to use a floor sensor attached to the character’s feet. Here’s what mine looks like (along with a ceiling sensor) in the debug view:

Floor and ceiling sensors attached to the main character’s fixture

But after implementing this scheme, I quickly found some buggy behavior that I couldn’t figure out, and went through several iterations of different approaches before finally solving the mystery. First, here’s an approach which is guaranteed not to work for you:

private bool IsSensorTouchingWall(Fixture sensor) {
    var contactEdge = sensor.Body.ContactList;
    while ( contactEdge != null && contactEdge.Contact != null ) {
        if ( contactEdge.Contact.IsTouching() && contactEdge.Contact.FixtureA == sensor || contactEdge.Contact.FixtureB == sensor ) {
            return true;
        }
        contactEdge = contactEdge.Next;
    }
    return false;
}

You might expect, as I did, that you could figure out whether a given sensor was overlapping another world object with this function, but that’s not how sensors work. As I discovered, Box2D plays very fast and loose with its contact edges, so that whenever the main body of my character was touching any other object, all the fixtures attached to him (including the sensors) would report this contact as well.

To make sensors useful, you have to register collision and separation handlers on them like so:

_floorSensor.OnCollision += (a, b, contact) => {
    _floorSensorContactCount++;
    Console.WriteLine("Hit floor");
    return true;
};
_floorSensor.OnSeparation += (a, b) => {
    if ( _floorSensorContactCount > 0 ) {
        _floorSensorContactCount--;
        Console.WriteLine("Left floor");
    }
};

Then to determine whether you’re standing on solid ground, you just ask whether the current contact count for the floor sensor is greater than zero.

Or, at least, that’s how literally every tutorial I discovered claimed it was supposed to work. What I was seeing in practice, however, is that occasionally this counter got out of sync, and my character could jump in the air. After a lot of painful debugging, I finally figured out the problem: when you destroy a Body, Box2D doesn’t trigger an OnSeparation event for any sensors touching it. Of course, if you then create a new body in exactly the same place, Box2D will happily send an OnCollision event from the new fixture. Consider what happens in the scenario below when the character shoots out the corner block to his left.

I know, the gun doesn’t line up with the block

Internally, what happens is that the body on which he’s standing is destroyed, then recreated as two different bodies to accommodate the now-missing tile. The floor sensor gets a new OnCollision event, but no OnSeparation event when the original Fixture is destroyed. In my humble opinion, this is a bug, so I went in and fixed it in Box2D. Here’s the patch, in Box2D’s World.cs file, with my added lines highlighted:

private void ProcessRemovedBodies()
{
    if (_bodyRemoveList.Count > 0)
    {
        foreach (Body body in _bodyRemoveList)
        {
            Debug.Assert(BodyList.Count > 0);
 
            // You tried to remove a body that is not contained in the BodyList.
            // Are you removing the body more than once?
            Debug.Assert(BodyList.Contains(body));
 
            // Delete the attached joints.
            JointEdge je = body.JointList;
            while (je != null)
            {
                JointEdge je0 = je;
                je = je.Next;
 
                RemoveJoint(je0.Joint, false);
            }
            body.JointList = null;
 
            // Delete the attached contacts.
            ContactEdge ce = body.ContactList;
            while ( ce != null ) {
                ContactEdge ce0 = ce;

                Fixture fixtureA = ce0.Contact.FixtureA;
                Fixture fixtureB = ce0.Contact.FixtureB;
                if ( fixtureA.OnSeparation != null ) {
                    fixtureA.OnSeparation(fixtureA, fixtureB);
                }
                if ( fixtureB.OnSeparation != null ) {
                    fixtureB.OnSeparation(fixtureA, fixtureB);
                }
 
                ce = ce.Next;
                ContactManager.Destroy(ce0.Contact);
            }

I don’t understand nearly enough about Box2D to know if this is the right approach to this problem, but it seems to work for me. Hopefully this post will help someone else running into the same issue. One issue with this patch is that sometimes, for reasons I don’t understand, the OnSeparation event gets triggered more than once. The fix is easy: just make sure the contact counter never drops below zero. But the fact that this happens at all makes me wary, and it’s definitely possible I’ll return to the less complicated, less efficient solution I implemented before I knew about sensors.

Leave a Reply