Programming box2d in javascript – tutorial on basics

By | January 13, 2023

Box2d

Box2d is a 2d physics simulation library written for C++ and ported to many languages including Javascript.

From Box2d's Website

Box2D is a feature rich 2D rigid body physics engine, written in C++ by Erin Catto. It has been used in many games, including Crayon Physics Deluxe, winner of the 2008 Independant Game Festival Grand Prize.

Box2d can is used to develop 2d games in various languages. In this tutorial we shall use the Box2d library to write a simple Hello World program.

We shall use the Box2d Javascript port from http://code.google.com/p/box2dweb/. It used version Box2d ver. 2.1 (at the time of writing this article).

Demo

Lets first take a look at what we shall be making in this tutorial. Try clicking anywhere in the white area and a solid object would be created at the point and fall down. The fall would be realistic.

Code

Components of a Box2d Animation

Any Box2d animation or say environment has some components :

1. A World
2. Some Gravity - It can be in any direction , or even absent (like deep space).
3. Some real objects that have properties like density , friction , restitution etc

In the above demo if you click somewhere , objects would appear and start falling down. So the whole area is a world which has a downward gravity (10 m/s2 in this case). And whatever is falling and staying fixed , are objects.

Code

World :

function createWorld() 
{
	//Gravity vector x, y - 10 m/s2 - thats earth!!
	var gravity = new b2Vec2(0, -10);
	
	world = new b2World( gravity , true );
	createGround(world);
	
	return world;
}

The b2World is a Box2d world, where objects exist and interact with each other. The gravity parameter is a vector which indicates the force magnitude and direction.

Tip :

If you are developing a super mario kind of game then you need a world with normal gravity, so that things fall downwards by default.

If you are writing a car racing game where car move in any direction, then you need to remove the gravity.

Ground :

function createGround(world) 
{
	var bodyDef = new b2BodyDef();
	
	fixDef = new b2FixtureDef;
	fixDef.density = 1.0;
	fixDef.friction = 1.0;
	fixDef.restitution = 0.5;
	
	fixDef.shape = new b2PolygonShape;
	
	fixDef.shape.SetAsBox(4.00 , .30);
	
	bodyDef.position.Set(4.10 , 4.70);
	
	return world.CreateBody(bodyDef).CreateFixture(fixDef);
}

Ground is a rectangular object , that stays fixed at a place and serves as a ground where other things can fall.

Every object in Box2d has the following parts :

1. Fixture - Defines properties like friction , restitution , density
2. Shape - 2d geometrical shape. Can be circle or polygon (set of vertices).
3. Body - Defines the point where the object is and what is its type - dynamic , static or kinetic

Type of bodies

In box2d there are 3 kinds of bodies

1. Static - These stay fixed at a place and do not move. Like a wall or the floor

2. Kinematic - These move according to their velocity and do not respond to forces acting on it.

3. Dynamic - These are the bodies that move in the simulation according to the forces acting on it. Like the game characters. Dynamic bodies can collide with all other kinds of bodies.

Scale

Box2d uses the unit metre to measure its world and the objects within. Things should be between 0.1 - 10 metre for proper simulation. When drawing on screen , a certain scale is needed to convert from metre to pixels. Over here I have used a scale of 100 , which means , an item 1 metre wide would be drawn as 100 pixels wide.

Simulation

Once everything is setup , its time to make Box2d "run" the world or make it become live. This is achieved by calling the Step method of the world object.

function step(cnt) 
{
	//fps = 60 , time steps
	var fps = 60;
	var timeStep = 1.0/fps;
	
	//move the world ahead , step ahead man!!
	world.Step(timeStep , 8 , 3);
	world.ClearForces();
	
	//first clear the canvas
	ctx.clearRect( 0 , 0 , canvas_width, canvas_height );
	
	//redraw the world
	draw_world(world , ctx);
	
	//call this function again after 10 seconds
	setTimeout('step(' + (cnt || 0) + ')', 10);
}

The world.Step method takes 3 parameters - timestep , velocity iterations , position iterations
The timestep indicates , how much time the world should move ahead by , say 1 second or 0.1 second.
The step method is called again and again using setTimeout so that the world keeps on moving. As the world moves the position , velocity etc of all bodies change according to the laws of physics.

Drawing Shapes

Box2d provides a debugdraw function to draw shapes. It draw the bodies according to their shapes and also marks the radius of circles and joints if any.

First a b2DebugDraw object is created that the box2d world can use to draw itself to.

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

The SetSprite function of b2DebugDraw needs the context to draw on. The scale is the conversion factor that converts box2d units to pixel. A scale of 50 means that 1 metre in box2d would be draw as 50 pixels on the canvas.
There are a few more properties to set the details of which can be found in the documentation.

Now whenever the world needs to draw itself, it can do the following

//convert the canvas coordinate directions to cartesian
ctx.save();
ctx.translate(0 , canvas_height);
ctx.scale(1 , -1);
world.DrawDebugData();
ctx.restore();

The line of interest is only world.DrawDebugData(), however there are few more things that we have done to the canvas context. Most important is the change of coordinate system. Box2d works in cartesian coordinate system which is like a graph with x axis increasing positively rightwards and y axis increasing positively upwards. And the origin is lower left corner.

However the canvas has a different coordinate system. In the canvas the x axis increases to right as usual, but the y axis increases downwards. Means upwards it is negative. And the origin is the top left corner.

To fix this we used a quick trick to make the canvas change its rules.

ctx.translate(0 , canvas_height);
ctx.scale(1 , -1);

The translate brings the origin to the lower left corner. That could be somewhere near 0,500 (in canvas coordinates). The scale changes the direction of the y axis to its opposite, that is, it starts increasing upwards. Now the canvas is more like a cartesian coordinate graph and objects draw would appear correct.

Feel free to experiment with this and see the results.

Final Code

/**
	Box2d basics, uses debugdraw
	Silver Moon
	[email protected]
*/

//Global classnames from Box2d namespace
var b2Vec2 = Box2D.Common.Math.b2Vec2
	, b2AABB = Box2D.Collision.b2AABB
	, b2BodyDef = Box2D.Dynamics.b2BodyDef
	, b2Body = Box2D.Dynamics.b2Body
	, b2FixtureDef = Box2D.Dynamics.b2FixtureDef
	, b2Fixture = Box2D.Dynamics.b2Fixture
	, b2World = Box2D.Dynamics.b2World
	, b2MassData = Box2D.Collision.Shapes.b2MassData
	, b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape
	, b2CircleShape = Box2D.Collision.Shapes.b2CircleShape
	, b2DebugDraw = Box2D.Dynamics.b2DebugDraw
	, b2Shape = Box2D.Collision.Shapes.b2Shape
	, b2Joint = Box2D.Dynamics.Joints.b2Joint
	, b2Settings = Box2D.Common.b2Settings
	;

var world;
var ctx;
var canvas_width;
var canvas_height;

//box2d to canvas scale , therefor 1 metre of box2d = 100px of canvas :)
var scale = 100;

/*
	Draw a world
	this method is called in a loop to redraw the world
*/	 
function draw_world(world, context) 
{
	//first clear the canvas
	ctx.clearRect( 0 , 0 , canvas_width, canvas_height );
	
	ctx.fillStyle = '#FFF4C9';
	ctx.fillRect(0,0, canvas_width, canvas_height);
		
	//convert the canvas coordinate directions to cartesian
	ctx.save();
	ctx.translate(0 , canvas_height);
	ctx.scale(1 , -1);
	world.DrawDebugData();
	ctx.restore();
	
	ctx.font = 'bold 18px arial';
	ctx.textAlign = 'center';
	ctx.fillStyle = '#000000';
	ctx.fillText("Box2d Hello World Example", 400, 20);
	ctx.font = 'bold 14px arial';
	ctx.fillText("Click the screen to add more objects", 400, 40);
}

//Create box2d world object
function createWorld() 
{
	//Gravity vector x, y - 10 m/s2 - thats earth!!
	var gravity = new b2Vec2(0, -10);
	
	world = new b2World(gravity , true );
		
	//setup debug draw
	var debugDraw = new b2DebugDraw();
	debugDraw.SetSprite(document.getElementById("canvas").getContext("2d"));
	debugDraw.SetDrawScale(scale);
	debugDraw.SetFillAlpha(0.5);
	debugDraw.SetLineThickness(1.0);
	debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit);
	
	world.SetDebugDraw(debugDraw);
	
	//createGround(world);
	ground = createBox(world, 4, 1, 4 , 0.5, {type : b2Body.b2_staticBody});
	
	return world;
}		

//Create a ground below the hellow world boxes
function createGround(world) 
{
	var bodyDef = new b2BodyDef();
	
	var fixDef = new b2FixtureDef();
	fixDef.density = 1.0;
	fixDef.friction = 1.0;
	fixDef.restitution = 0.5;
	
	fixDef.shape = new b2PolygonShape;
	
	//mention half the sizes
	fixDef.shape.SetAsBox(4.00 , .5);
	
	//set the position of the center
	bodyDef.position.Set(4.10 , 1);
	
	return world.CreateBody(bodyDef).CreateFixture(fixDef);
}

//Function to create a ball
function createBall(world, x, y, r, options) 
{
	var body_def = new b2BodyDef();
	var fix_def = new b2FixtureDef;
	
	fix_def.density = 1.0;
	fix_def.friction = 0.5;
	fix_def.restitution = 0.5;
	
	var shape = new b2CircleShape(r);
	fix_def.shape = shape;
	
	body_def.position.Set(x , y);
	
	body_def.linearDamping = 0.0;
	body_def.angularDamping = 0.0;
	
	body_def.type = b2Body.b2_dynamicBody;
	body_def.userData = options.user_data;
	
	var b = world.CreateBody( body_def );
	b.CreateFixture(fix_def);
	
	return b;
}

//Create some elements
function createHelloWorld() 
{
	// H
	createBox(world, .5 , 2.2, .1, .2);
	
	createBox(world, .9 , 2.2 , .1, .2);
	createBox(world, .7 , 1.95 , .3, .05);
	createBox(world, .5 , 1.7 , .1 , .2);
	createBox(world, .9 , 1.7 , .1 , .2);
}

//Create standard boxes of given height , width at x,y
function createBox(world, x, y, width, height, options) 
{
	 //default setting
	options = $.extend(true, {
		'density' : 1.0 ,
		'friction' : 1.0 ,
		'restitution' : 0.5 ,
		
		'linearDamping' : 0.0 ,
		'angularDamping' : 0.0 ,
		
		'type' : b2Body.b2_dynamicBody
	}, options);
      
    var body_def = new b2BodyDef();
	var fix_def = new b2FixtureDef();
	
	fix_def.density = options.density;
	fix_def.friction = options.friction;
	fix_def.restitution = options.restitution;
	
	fix_def.shape = new b2PolygonShape();
		
	fix_def.shape.SetAsBox( width , height );
	
	body_def.position.Set(x , y);
	
	body_def.linearDamping = options.linearDamping;
	body_def.angularDamping = options.angularDamping;
	
	body_def.type = options.type;
	body_def.userData = options.user_data;
	
	var b = world.CreateBody( body_def );
	var f = b.CreateFixture(fix_def);
	
	return b;
}

/*
	This method will draw the world again and again
	called by settimeout , self looped
*/
function step() 
{
	var fps = 60;
	var timeStep = 1.0/fps;
	
	//move the world ahead , step ahead man!!
	world.Step(timeStep , 8 , 3);
	world.ClearForces();
	
	draw_world(world, ctx);
}

/*
	Convert coordinates in canvas to box2d world
*/
function get_real(p)
{
	return new b2Vec2(p.x + 0, 6 - p.y);
}

// main entry point
$(function() 
{
	var canvas = $('#canvas');
	ctx = canvas.get(0).getContext('2d');
	
	//first create the world
	world = createWorld();
	
	//get internal dimensions of the canvas
	canvas_width = parseInt(canvas.attr('width'));
	canvas_height = parseInt(canvas.attr('height'));
	
	//create the hello world boxes in the world
	createHelloWorld();
	
	//click event handler on our world
	canvas.click( function(e) 
	{
		var p = get_real(new b2Vec2(e.clientX / scale, e.clientY / scale));
		
		//create shape
		if (Math.random() > 0.5) 
		{
			//Square box
			createBox(world, p.x , p.y , .1 , .1);
		} 
		else 
		{
			//circle
			createBall(world, p.x , p.y, 0.2, {'user_data' : {'fill_color' : 'rgba(204,100,0,0.3)' , 'border_color' : '#555' }});
		}
	});
	
	 window.setInterval(step, 1000 / 60);
});

Next ...

Box2d is a tried and tested library that has been used to develop many popular games like Angry Birds. It is very easy to write a complete game using just box2d and a few graphic sprites. If you wish to build an html5 game with real-world like physics then give box2d a try.

About Silver Moon

A Tech Enthusiast, Blogger, Linux Fan and a Software Developer. Writes about Computer hardware, Linux and Open Source software and coding in Python, Php and Javascript. He can be reached at [email protected].

Leave a Reply

Your email address will not be published. Required fields are marked *