Friday, September 2, 2011

Box2D JavaScript Example Walkthrough

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!




This is the second article in a Box2D series, following the Box2D Orientation article.

The Box2DWeb port of Box2D contains a nice example to show off the basics of integrating physics simulations into your web app. This post will provide a walkthrough of the example, explaining the high level concepts and code.

First, let's see the example in action.



The code for the above is open source and available on GitHub. It was adapted from Box2DWeb's example.

Animating

Before we look at Box2D, it's important to understand how the above simulation is animated. You might think setInterval or setTimeout is your friend. Wrong, they are evil little buggers. For smart animating, you should use requestAnimationFrame. Mozilla also recommends requestAnimationFrame for efficient animation. In fact, they wrote about it again.

With setInterval, the browser has no idea about your intention. It can't help you optimize accordingly, as it's not apparent you are trying to animate efficiently. With requestAnimationFrame, though, the browser understands what you are trying to do, and can help. It will even turn off animations if the tab isn't visible, saving power and battery. Mr. Paul Irish has a nice cross-browser solution for requestAnimationFrame, even falling back to older browsers.

window.requestAnimFrame = (function(){
          return  window.requestAnimationFrame       || 
                  window.webkitRequestAnimationFrame || 
                  window.mozRequestAnimationFrame    || 
                  window.oRequestAnimationFrame      || 
                  window.msRequestAnimationFrame     || 
                  function(/* function */ callback, /* DOMElement */ element){
                    window.setTimeout(callback, 1000 / 60);
                  };
    })();

For all of our Box2D work, we will use requestAnimationFrame.

Units

Remember from our previous Box2D Orientation that Box2D likes to operate on objects between 0.1 meters and 10 meters. You should not use pixels as your units, otherwise Box2D would be trying to simulate dust particles or cruise ships. You will often see Box2D code specify a scale factor, usually around 30.

The World

A Box2D world requires a gravity vector and a boolean to signal if objects at rest should be put to sleep. Note that Box2D doesn't assume gravity falls straight down at 9.8 m/s/s.

Older versions of Box2D required you to define the size of the world. Versions 2.1 and later (at the time of this writing, Box2D is now up to 2.2) do not require a predefined world size.

world = new b2World(
  new b2Vec2(0, 10)    //gravity
  ,  true                 //allow sleep
);

The Ground

A ground isn't provided for you, you need to define it. There isn't anything special about a ground, it's a static object like any other static item in the world.

We need things things to make an object:

  1. Fixture Definition
  2. Body Definition
  3. Shape
A Fixture Definition defines the attributes of the object, such as density, friction, and restitution (bounciness).

var fixDef = new b2FixtureDef;
fixDef.density = 1.0;
fixDef.friction = 0.5;
fixDef.restitution = 0.2;

A Body Definition defines where in the world the object is, and if it is dynamic (reacts to things) or static. A Body Definition's position is set to the middle center-point of the object, which is different than the normal upper left of HTML5 Canvas.

var bodyDef = new b2BodyDef;
bodyDef.type = b2Body.b2_staticBody;
       
// positions the center of the object (not upper left!)
bodyDef.position.x = CANVAS_WIDTH / 2 / SCALE;
bodyDef.position.y = CANVAS_HEIGHT / SCALE;

A Shape defines the actual 2D geometrical object, such as a circle or polygon. For simple boxes, be sure to note that Box2D wants half-width and half-height as arguments.

fixDef.shape = new b2PolygonShape;
       
// half width, half height.
fixDef.shape.SetAsBox((600 / SCALE) / 2, (10/SCALE) / 2);

With the static ground defined, we add it to the world:

world.CreateBody(bodyDef).CreateFixture(fixDef);

Falling Objects

Building the falling objects isn't much different than the static ground. You can see below that the real difference is the b2Body.b2_dynamicBody type for bodyDef.

The example code is creating a mix of rectangular objects (similar to the ground) and circles. One by one, it creates a random assortment of types and sizes, and then adds them to the world.

bodyDef.type = b2Body.b2_dynamicBody;
for(var i = 0; i < 10; ++i) {
    if(Math.random() > 0.5) {
        fixDef.shape = new b2PolygonShape;
        fixDef.shape.SetAsBox(
              Math.random() + 0.1 //half width
           ,  Math.random() + 0.1 //half height
        );
    } else {
        fixDef.shape = new b2CircleShape(
            Math.random() + 0.1 //radius
        );
    }
    bodyDef.position.x = Math.random() * 25;
    bodyDef.position.y = Math.random() * 10;
    world.CreateBody(bodyDef).CreateFixture(fixDef);
}

Visualizing the Simulation

You'll be happy to know that Box2D provides for a simple debug drawing output, allowing you to easily see what's happening in your world. You need to specify the scale into the graphics canvas (typically 30, as mentioned above) and provide a reference to the HTML5 Canvas.

//setup debug draw
var debugDraw = new b2DebugDraw();
debugDraw.SetSprite(document.getElementById("c").getContext("2d"));
debugDraw.SetDrawScale(SCALE);
debugDraw.SetFillAlpha(0.3);
debugDraw.SetLineThickness(1.0);
debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit);
world.SetDebugDraw(debugDraw);

Running the Simulation

Stepping through the simulation requires three configuration values: the time step, the velocity iteration count, and the position iteration count.

Box2D recommends keeping the time step constant. For example, don't tie the simulation time step to the frame rate, as frame rate can really vary during a game. The Box2D manual will suggest a 60Hz time step, which is 1/60 seconds.

The velocity phase "computes the impulses necessary for the bodies to move correctly."  Box2D will suggest a velocity iteration count of 8, but you can change this depending on your needs. It's a trade-off between performance and accuracy.

The position phase "adjusts the positions of the bodies ro reduce overlap and joint detachment." The recommended value is 3, though you can change this with the same performance and accuracy trade-offs. The position phase might exist early if the solver determines the errors are small enough.

world.Step(
    1 / 60   //frame-rate
    ,  10       //velocity iterations
    ,  10       //position iterations
);

Final Integration

All we have left is to merge our animation loop, powered by requestAnimationFrame, with Box2D's physics simulations.

function update() {
   world.Step(
         1 / 60   //frame-rate
      ,  10       //velocity iterations
      ,  10       //position iterations
   );
   world.DrawDebugData();
   world.ClearForces();
     
   requestAnimFrame(update);
}; // update()

And we kick it all off with a single call:

requestAnimFrame(update);

Final Thoughts

Hopefully we've demystified Box2D a bit here, at least for the simple case. It's important to know that Box2D can do more than what we've shown here. For example, you can model different kinds of joints, add callbacks for when two objects are touching (sensors),  and set velocity, amongst other options.

What do you want to see next for our Box2D series? Thanks for following along!

UPDATE: The next post is up, discussing Box2D, FPS, and Step Rate. Complete with a side by side comparison demo!
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.