Pages

Sunday, November 11, 2012

Box2D & Javascript: Part 2

In part 1: I covered what Box2d is along with some of its application and a number of links to possible box2d resource. Now in this blog, I'm going to demonstrate a basic setup for box2d based simulations and the next one will cover the integration with KineticJS library.



Press 'd' to toggle debugDraw mode
Box2d Demo - by Aniruddha Loya

This example was adapted from the Box2dWeb example.

Now let's break up the code and see how it works

For any box2d application, we start with defining the world with 2 parameters: gravity and sleep.

world = new b2World(
new b2Vec2(0, 10),
true
);

here, we define gravity as a vector equal to 10units in y direction where (0,0) is top left and y is +ve downwards.
And, sleep parameter tells box2d to not process the object if it is in equilibrium.

var scale = 20.0;

Scale is another important declaration which maps the distances in Box2d world (in meters) to corresponding distance on our canvas (in pixels). So our code here define 20m = 1px.

Next we see, how to define the ground for our world.
     
// Define the Ground
// Basic properties of ground
var fixDef = new b2FixtureDef;
fixDef.density = 2.0;
fixDef.friction = 0.9;
fixDef.restitution = 0.8;

// Ground is simply a static rectangular body with its center at screenW/2 and screenH
var bodyDef = new b2BodyDef;
bodyDef.type = b2Body.b2_staticBody;
bodyDef.position.x = screenW/2/scale;
// We use screenH for y coordinate as the ground has to be at the bottom of our screen
bodyDef.position.y = screenH/scale;

// here we define ground as a rectangular box of width = screenW and height = 10 (just some small number to make a thin strip placed at the bottom)
fixDef.shape = new b2PolygonShape;
fixDef.shape.SetAsBox(screenW/scale, 10/scale);

// And finally add our ground object to our world
world.CreateBody(bodyDef).CreateFixture(fixDef);

In the above piece of code, we first define the properties (density, friction & restitution) of the ground. Then we define ground as a "static" body (Box2d has 3 types of  bodies, static, dynamic & kinematic, refer to manual for more details). A rectangular body in box2d is defined by its center, width & height. Thus, we position our ground's center at screenH(eight) {to place it at bottom} and half of screenW(idth) and divide it by scale to map to box2d coordinates. Finally, we define ground as a polygon shape and draw it as a box with width = screenW and height = 10px, and then create a body with these defined parameters, added to our world.

 // Left Edge
 // The edge is positioned at the left most i.e. x = 0 and y = screenH/2 as the center. width is 1 and height = screenH
 bodyDef.position.x = 0;
 bodyDef.position.y = screenH/2/scale;
 fixDef.shape = new b2PolygonShape;
 fixDef.shape.SetAsBox(1/scale, screenH/scale);
 world.CreateBody(bodyDef).CreateFixture(fixDef);

 // Right Edge - same as left edge, positioned on the rightmost end of our canvas
 bodyDef.position.x = screenW/scale;
 bodyDef.position.y = screenH/2/scale;
 fixDef.shape = new b2PolygonShape;
 fixDef.shape.SetAsBox(1/scale, screenH/scale);
 world.CreateBody(bodyDef).CreateFixture(fixDef);

Similar to our ground, we define the left & right boundaries for our simulation space to keep the simulation objects within visible scope. This is optional and depends up on the application. With the boundaries set, now lets add some objects to our world

// Adding objects to our simulation space.
// The difference here being, that these are dynamic objects and are affected by forces and impulses
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.5, Math.random() + 0.5);
 } else {
  fixDef.shape = new b2CircleShape(Math.random() + 0.5);
 }
 bodyDef.position.x = Math.random() * screenW/scale;
 bodyDef.position.y = Math.random() * screenH/scale/4;
 world.CreateBody(bodyDef).CreateFixture(fixDef);
}
First important thing to note here is the change of body type to "dynamic" as these are objects in our simulation space that interact with each other under laws of physics. The code above randomly adds rectangles and circles to the world placing them randomly across the canvas width and in top quarter of the canvas height.

window.requestAnimFrame = (function(){
  return  window.requestAnimationFrame       ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame    ||
    window.oRequestAnimationFrame      ||
    window.msRequestAnimationFrame     ||
    function(/* function */ callback, /* DOMElement */ element){
   window.setTimeout(callback, 1000 / 60);
    };
})();
This is our callback for animation. It is widely suggested to use requestAnimFrame instead of setTimeout or setInterval for better animation performance.

// Our update function
function update() {
 world.Step(1 / 60, 3, 3);

 if(drawDebug)
 {
    // Clears the canvas context
    context.clearRect(0,0,screenW, screenH);

    world.DrawDebugData();
    drawCanvasObjects();
 }
 else
    world.DrawDebugData();

 // This is called after we are done with time steps to clear the forces
 world.ClearForces();

 // callback for next update
 requestAnimFrame(update);
 };
Next comes our update function. It first tells world to step ahead in time, calculate the forces, their impact and resultant positions of the objects in the world. After that, it draws the world using DrawDebugData() (defined below) , a native renderer of box2d particularly useful in debugging application and testing interaction of various objects in simulation. if(drawDebug) checks whether to overwrite the basic box2d rendering with html5 elements. In this demo, I simply fill the circles with "red" color. Finally, we clear the forces and call our animation handler.

var debugDraw = new b2DebugDraw();
 debugDraw.SetSprite(context);
 debugDraw.SetDrawScale(scale);
 debugDraw.SetFillAlpha(0.5);
 debugDraw.SetLineThickness(1.0);
 debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit | b2DebugDraw.e_centerOfMassBit);

world.SetDebugDraw(debugDraw);

debugDraw => defining DrawDebugData()

// Function to detect circles and color them Red
function drawCanvasObjects()
{
   // get list of all bodies attached to this world
   var node = world.GetBodyList(); 
   while(node)
   {
 var curr = node;
 node = node.GetNext();

 // Check if the identified body is of type dynamic. Remember we defined our circles as dynamic body types
 if(curr.GetType() == b2Body.b2_dynamicBody)
 {
    // Get the shape from the list of retrieved fixtures defined during initialization for each body
    var shape = curr.GetFixtureList().GetShape();

    if(shape.GetType() == circle.GetType()) // If shape is circle
    {   
  // Get the body's position in the world
                var position = curr.GetPosition();
  // Scale back the position to map them canvas coordinates
  var canvasY = position.y*scale;
  var canvasX = position.x*scale;

  // boundary color = white
  context.strokeStyle = "#000000";
  // fill color = red
  context.fillStyle = "#FF0000";

  context.beginPath();
  context.arc(canvasX,canvasY,shape.GetRadius()*scale,0,Math.PI*2,true);
  context.closePath();
  context.stroke();
  context.fill();
    }
 }
   }
}

Function drawCanvasObjects(), gets the list of objects attached to the world and iterate over them checking if the object is a dynamic body. Then it checks for the body type to be "circle", in which case we retrieve the position of the body, maps it to canvas coordinates and then draw them filled red.

function keydown(e)
{
   if (e.keyCode == 68) //press 'd'
   {
 drawDebug = !drawDebug;
   }
}

// insert the listener for onload event to call our init function
window.addEventListener("load", init, true); 
       
Finally, the handler to check key-press 'd' and the event listener to "load" event.

This completes the part 2. I hope it helps my fellow programmers who are starting/ planning to start developing next Angry Birds for all of us to enjoy over and over. I'll love to hear your suggestions!

Next in this series would be how to use KinecticJS library with box2d.

2 comments:

mickro said...

hey bro!

Some stuffs are linked bad. Look at your js console to discover Box2d is not loaded.

And sample you are pointing to original project on googlecode is not exactly same you're using in tutorial.

Else nice job ! I like it.

Unknown said...

Hey,
Thanks for your comments.
I checked the console is all working fine. Send me your console coz I don't see any error.
And this work is adapted from the original linked and not copied, so its not exactly the same as it should not be.