Saturday, September 17, 2011

Box2D, Collision, Damage, for JavaScript

Sponsor: Register today for New Game, the conference for HTML5 game developers. Learn from Mozilla, Opera, Google, Spil, Bocoup, Mandreel, Subsonic, Gamesalad, EA, Zynga, and others at this intimate and technically rich conference. Join us for two days of content from developers building HTML5 games today. Nov 1-2, 2011 in San Francisco. Register now!




After exploring Box2D and Impulses, which can make a body jump and move, we will turn our attention to detecting and dealing with body collisions.

Intro

Box2D of course knows when and how each body and fixture is contacting other bodies and fixtures, but so far we haven't reacted to these events nor done anything interesting. Luckily, there is an easy way to be notified when two bodies contact each other, when they stop contacting, and even how much impulse is felt by the bodies. We will explore these concepts in this blog post.

Can you feel it?

The b2ContactListener class from Box2D provides the four callbacks you can use to be notified of contact related events. These events are:
  • BeginContact - fired when two fixtures start contacting (aka touching) each other
  • EndContact - fired when two fixtures cease contact
  • PreSolve - fired before contact is resolved. you have the opportunity to override the contact here.
  • PostSolve - fired once the contact is resolved. the event also includes the impulse from the contact.
Note that all of the above events are fired during the world Step. This means you need to be very careful not to manipulate the world inside of these events, as the Box2D simulation isn't finished for the step, and you don't want to alter the world at this point.

EndContact can be fired outside of the Step, in the case of a body being removed from the world.

An example listener follows:

    var listener = new Box2D.Dynamics.b2ContactListener;
    listener.BeginContact = function(contact) {
        // console.log(contact.GetFixtureA().GetBody().GetUserData());
    }
    listener.EndContact = function(contact) {
        // console.log(contact.GetFixtureA().GetBody().GetUserData());
    }
    listener.PostSolve = function(contact, impulse) {
        
    }
    listener.PreSolve = function(contact, oldManifold) {

    }
    this.world.SetContactListener(listener);

Each of the events take a contact parameter, which has details of the contact, most of important are GetFixtureA() and GetFixtureB() which return the fixtures involved in the contact. From the fixtures you can reference the bodies. This implies that collisions occur between fixtures, not bodies.

How big was the impact?

You might be wondering, "Great, I see I can tell which two fixtures collided, but what was the force at the collision?" Knowing how hard or soft the collision was is important for lots of game logic.

Looking closely, you'll notice that Box2D doesn't hand you the force or impulse inside BeginContact(). You can of course calculate your own value for the magnitude of the collision, using linearVelocity and mass, both found on the body object. We might even do this in a future article.

Notice, though, that the PostSolve() method includes an impulse object, which includes an array of impulse values for the collision. This value is the easiest way to access the collision's magnitude.

If only things were so easy!

PostSolve() is called seemingly every time a body feels an impulse from another body. If object A hits object B which is connected to object C, then object C will feel the impulse from object A's initial hit (because the impulse "bumps" object B which "bumps" object C). This may or may not be what you want, depending on your game logic.

For example, if you have a ball rolling across the ground, this creates an impulse (albeit tiny) for every frame, and PostSolve() will be fired every frame. If you have other objects touching the ground, those objects will feel an impulse from the ground (which is "rumbled" by the ball rolling.)

If you are intending to calculate damage to an object from an impact, be very aware the PostSolve() event will be fired a LOT. If you are subtracting damage from an object's total "hit points" during every PostSolve() be sure you understand that your object will incur damage from basically every impulse.

You can of course create initial "hit points" high enough to withstand impulses from everywhere, or dismiss impulses below a threshold. Your needs will vary.

The important point to remember is that PostSolve() will be a called a LOT.

Example

For a simple example, I extended a previous example to add support for collisions. The below example will color objects black if they are feeling a contact (i.e. a PostSolve() event is fired) and will apply damage from the impulse.



You can view a stand alone version and see the source code.

I added a simple wrapper for the callback registration to my simple Box2D wrapper class:

bTest.prototype.addContactListener = function(callbacks) {
    var listener = new Box2D.Dynamics.b2ContactListener;
    if (callbacks.BeginContact) listener.BeginContact = function(contact) {
        callbacks.BeginContact(contact.GetFixtureA().GetBody().GetUserData(),
                               contact.GetFixtureB().GetBody().GetUserData());
    }
    if (callbacks.EndContact) listener.EndContact = function(contact) {
        callbacks.EndContact(contact.GetFixtureA().GetBody().GetUserData(),
                             contact.GetFixtureB().GetBody().GetUserData());
    }
    if (callbacks.PostSolve) listener.PostSolve = function(contact, impulse) {
        callbacks.PostSolve(contact.GetFixtureA().GetBody().GetUserData(),
                             contact.GetFixtureB().GetBody().GetUserData(),
                             impulse.normalImpulses[0]);
    }
    this.world.SetContactListener(listener);
}

I register a callback in the main page like this:

      box.addContactListener({
        BeginContact: function(idA, idB) {
        },
        
        PostSolve: function(idA, idB, impulse) {
          if (impulse < 0.1) return; // playing with thresholds
          var entityA = world[idA];
          var entityB = world[idB];
          entityA.hit(impulse, entityB);
          entityB.hit(impulse, entityA);
        }
      });

Note that the above code is not standard Box2D code, it is a simplification I've created for these demos.

I added a new method to Entity for the collision:

    Entity.prototype.hit = function(impulse, source) {
      this.isHit = true;
      if (this.strength) {
        this.strength -= impulse;
        if (this.strength <= 0) {
          this.dead = true
        }
      }
      
      //console.log(this.id + ", " + impulse + ", " + source.id + ", " + this.strength);
    }

For my purposes, entities may or may not have a strength (aka "hit points"). If my entity runs out of strength, it is "dead" and will be removed from the world:

    function update(animStart) {
      box.update();
      bodiesState = box.getState();
      
      var graveyard = [];
      
      for (var id in bodiesState) {
        var entity = world[id];
        
        if (entity && world[id].dead) {
          box.removeBody(id);
          graveyard.push(id);
        } else if (entity) {
          entity.update(bodiesState[id]);
        }
      }
      
      for (var i = 0; i < graveyard.length; i++) {
        delete world[graveyard[i]];
      }
    }

And my simple Box2D wrapper implements removeBody() as such:

bTest.prototype.removeBody = function(id) {
    this.world.DestroyBody(this.bodiesMap[id]);
}

Summary

The Box2D b2ContactListener provides four events for you to be notified when two fixtures contact/collide/touch. The PostSolve() method includes an impulse object which you can use to calculate damage or otherwise measure the intensity of the collision. However, PostSolve() is fired whenever a fixture "feels" another fixture, which means it will be fired quite a lot.

If you're at this point in your development, you'll probably notice that you're now involved with game tuning as much as raw coding. That is, with aspects like damage, the "feel" of the game now becomes quite important. It's not enough to figure out how the APIs work, you need to tune and tweak your game with quite a few "magic" values like damages, thresholds, etc.

Case in point is Angry Birds, which seems easy to copy until you start wanting to capture the essence of the game itself. That essence is wrapped up in many different small values that all add up to the "feel" of the game.

In other words... game balance is hard! :)

Next Steps

More details on Box2D contacts can be found in the Box2D manual. A Box2D collision tutorial from Alan Bishop covers PostSolve() but only deals with an instantaneous collisions and therefore doesn't run into the "PostSolve() gets called more frequently than you might think" issue. The Box2D forums feature a post on finding the maximum impulse from a collision.

Please do leave questions and comments below, and continue to let me know what else you'd like to see!
Post a Comment

Disclaimer

I'm probably required to say that the views expressed in this blog are my own, and do not necessarily reflect those of my employer. Also, except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 3.0 License, and code samples are licensed under the BSD License.