Box2d is a physics engine. It handles the interrelation of various solid objects. It includes gravity, forces, rotations, collisions and more. It does not include the actual drawing of the objects. However, it does enclude a "debug draw" mode that draws outlines of all the objects for you. (This caused me a large amount of confusion.)
Stop animation (reload page to start again)I decided to start out with the 2.1 release of box2d. Alas, this has been significantly updated from the 2.0 release. (And most of the demos out there are of the old version.) The box2d is originally written in C. The javascript version, however, is based on the flash port. The documentation is on the flash site, and needs to be "mapped" in to the javascript syntax. It also tends to be rather sparse. (I guess the action of "MulVV should be obvious.)
I chose the version that was used by the cocos2d gaming engine. I found a good build of box2djs at github. It is a 1-file version based on the most recent version of box2d. It also does not have a requirement for any external javascript libraries. (The other popular port is based on an older version and requires prototype.js).
This version of box2d.js did have one easily solvable bug with IE. Luckily, it appeared the error was simply related to using prototype.__defineGetter__. Microsoft had a suggestion that seemed to work fairly well for using the defineProperty method instead. I added this code to the start of the box2d.js, and it works like a charm in IE9 and IE10. (I've had horrible experience with the canvas simulation experience in older versions of IE, so I'll wait until later to worry about that.) For now, I just have it added to the full version of the source. However, a minification should not be much of a problem.
For IE, a doctype declaration is also needed to push it to standards mode. <!doctype html> seems to do the trick.
The canvas setup is straightforward. The one caveat is that on a canvas the upper left hand corner is 0,0, while with box2d, the lower left hand corner is 0. I simply inverted the value in the drawings. However, there are plenty of other (cleaner) methods for handling this.
<body onload="init();">
<canvas id="canvas" width="600" height="400" style="background-color:#333333;" ></canvas>
<script src="box/javascript/Box2D/box2d.js"> </script>
<script type="text/javascript">
...
The syntax for initializing a world is different from most of the 2.0 demos I've found. Now the world is of "unlimited" size. You must bound it with fixed objects, or take care to remove unused objects to help reduce endless calculations.
I set some convenience values for the initialization. The gravity vector controls what direction things will fall. Give at an x value and they will fall to the left or right. A negative y value will cause them to fall up. I also use a "deletionbuffer" variable. After the center of any object moves more than 4 pixels off the screen, I remove it.
// Define the canvas
var canvaselem = document.getElementById("canvas");
var context = canvaselem.getContext("2d");
var canvaswidth = canvaselem.width-0;
var canvasheight = canvaselem.height-0;
// Define the world
var gravity = new b2Vec2(0, -10);
var doSleep = true;
var world = new b2World(gravity, doSleep);
var deletionBuffer = 4;
This code is fairly similar to most examples out there. In this example, I only set a ceiling and a floor, leaving the sides unbounded. (That is why I need to delete things that float away.) The SetAsBox method is a quick way to create rectangular polygons. The boxes are created to span the entire horizontal size of the canvas. After this, I call methods for creating 3 initial shapes.
Finally, the interval is set. I am bad and use a global variable for the interval so that I can clear it later. When debugging, it is helpful to clear the interval after one iteration, or to slow it. Otherwise, a whole stream of console.logs can crash the debugger.
function init() {
//create ground
var fixDef = new b2FixtureDef;
fixDef.density = .5;
fixDef.friction = 0.4;
fixDef.restitution = 0.2;
var bodyDef = new b2BodyDef;
bodyDef.type = b2Body.b2_staticBody;
fixDef.shape = new b2PolygonShape;
fixDef.shape.SetAsBox(canvaswidth/2,2);
bodyDef.position.Set(canvaswidth/2, 0);
world.CreateBody(bodyDef).CreateFixture(fixDef);
bodyDef.position.Set(canvaswidth/2, canvasheight -2);
world.CreateBody(bodyDef).CreateFixture(fixDef);
// Start dropping some shapes
addTriangle();
addCircle();
addImageCircle();
// The refresh rate of the display. Change the number to make it go faster
z = window.setInterval(update2, (1000 / 60));
There are a number of good examples of drawing shapes in box2d.js. To create a shepe, you create bodydef, set the type to dynamic body, and give it an initial x and y position. The bodyDef can also have a "useData" object attached to it. Since this is initially defined to null, it is easiest to create a new object and then attach it.
Then you create a fixDef. The fixDef has general properties, such as density and friction. The fixture also has a shape "shape" object member that defines that actual shape of the object. For circles, all you need is a radius. For polygons, you must define the vertices. Once that is done, the object can be added to the world with the appropriate fixture.
Once you have drawn the shape, you can also apply an Impulse. To have an impact, this vector must be stronger than the gravitational vector in the system. If the second parameter is something other then the world center, you will likely induce spinning
function addTriangle() {
//create simple triangle
var bodyDef = new b2BodyDef;
var fixDef = new b2FixtureDef;
fixDef.density = Math.random();
fixDef.friction = Math.random();
fixDef.restitution = 0.2;
bodyDef.type = b2Body.b2_dynamicBody;
fixDef.shape = new b2PolygonShape;
var scale = Math.random() * 60;
fixDef.shape.SetAsArray([
new b2Vec2(scale*0.866 , scale*0.5),
new b2Vec2(scale*-0.866, scale*0.5),
new b2Vec2(0, scale*-1),
]);
bodyDef.position.x = (canvaswidth-scale*2)*Math.random()+scale*2;
bodyDef.position.y = canvasheight - (scale*Math.random() +scale);
world.CreateBody(bodyDef).CreateFixture(fixDef);
}
function addImageCircle() {
// create a fixed circle - this will have an image in it
// create basic circle
var bodyDef = new b2BodyDef;
var fixDef = new b2FixtureDef;
fixDef.density = .5;
fixDef.friction = 0.1;
fixDef.restitution = 0.2;
bodyDef.type = b2Body.b2_dynamicBody;
scale = Math.floor(Math.random()*40) + 8;
fixDef.shape = new b2CircleShape(scale);
bodyDef.position.x = scale*2;
bodyDef.position.y = scale*2;
var data = { imgsrc: "/jh.png",
imgsize: 16,
bodysize: scale
}
bodyDef.userData = data;
var body = world.CreateBody(bodyDef).CreateFixture(fixDef);
body.GetBody().ApplyImpulse(
new b2Vec2(100000,100000),
body.GetBody().GetWorldCenter()
);
}
The shapes on the canvas do not necesarily need to be drawn at the same time as those in box2d. In this example, they are not drawn until the world is redrawn. Instead, after each update of box2d, all dynamic bodies are found and redrawn.
Cycling through the list is fairly well documented. After cycling, we only process the dynamic bodies, with different actions depending on the shapes. Shapes outside the range are deleted. In this case, the whole canvas is cleared and redrawn after each cycle. However, if individual canvas elements were used, you could probably skip the redraw for sleeping objects.
var node = world.GetBodyList();
while (node) {
var b = node;
node = node.GetNext();
// Destroy objects that have floated off the screen
var position = b.GetPosition();
if (position.x < -deletionBuffer || position.x >(canvaswidth+4)) {
world.DestroyBody(b);
continue;
}
// Draw the dynamic objects
if (b.GetType() == b2Body.b2_dynamicBody) {
// Canvas Y coordinates start at opposite location, so we flip
var flipy = canvasheight - position.y;
var fl = b.GetFixtureList();
if (!fl) {
continue;
}
var shape = fl.GetShape();
var shapeType = shape.GetType();
The regular circle is the easiest, since it is a solid color, with no visible rotation. We simply get the coordinates and radius and draw it:
else if (shapeType == b2Shape.e_circleShape) {
context.strokeStyle = "#CCCCCC";
context.fillStyle = "#FFFF00";
context.beginPath();
context.arc(position.x,flipy,shape.GetRadius(),0,Math.PI*2,true);
context.closePath();
context.stroke();
context.fill();
}
The triangles need to be rotated. I found many examples with the older verison of box2d, but the syntax had changed slightly with this version. The math routines can be used to transform the vertices based on the rotation. Then the lines can be drawn from these locations. (The debug draw function in the source seems to use an alternate method for doing this.)
else if (shapeType == b2Shape.e_polygonShape) {
var vert = shape.GetVertices();
context.beginPath();
// Handle the possible rotation of the polygon and draw it
b2Math.MulMV(b.m_xf.R,vert[0]);
var tV = b2Math.AddVV(position, b2Math.MulMV(b.m_xf.R, vert[0]));
context.moveTo(tV.x, canvasheight-tV.y);
for (var i = 0; i < vert.length; i++) {
var v = b2Math.AddVV(position, b2Math.MulMV(b.m_xf.R, vert[i]));
context.lineTo(v.x, canvasheight - v.y);
}
context.lineTo(tV.x, canvasheight - tV.y);
context.closePath();
context.strokeStyle = "#CCCCCC";
context.fillStyle = "#00FFAA";
context.stroke();
context.fill();
}
Using an image for the object can be a little more tricky. Many of the examples out there use an individual canvas element for each image. However, it is possible to have several rotated images within one canvas. (Performance may come in to consideration.)
To do the rotation, first, the canvas needs to be transformed to the center of the image (conveniently the box2d coordinates). Then the canvas needs to be rotated. If the image needs to be reduced or enlarged, that can be done here also. Finally, the image needs to be added to the canvas. After that, the canvas can be restored to its original position. (I use the canvas save and restore. However, I have read that doing reverse rotate, etc. can be more effecient.)
if (b.m_userData && b.m_userData.imgsrc) {
// This "image" body destroys polygons that it contacts
// Draw the image on the object
var size = b.m_userData.imgsize;
var imgObj = new Image(size,size);
imgObj.src = b.m_userData.imgsrc;
context.save();
// Translate to the center of the object, then flip and scale appropriately
context.translate(position.x,flipy);
context.rotate(b.GetAngle());
var s2 = -1*(size/2);
var scale = b.m_userData.bodysize/-s2;
context.scale(scale,scale);
context.drawImage(imgObj,s2,s2);
context.restore();
}
If an "image circle", hits a polygon, the polygon is removed from the display. To do this, I iterate over the ContactList from the circle. This is a C-style linked list, so the approporiate iteration is needed.
var edge = b.GetContactList();
while (edge) {
var other = edge.other;
if (other.GetType() == b2Body.b2_dynamicBody) {
var othershape = other.GetFixtureList().GetShape();
if (othershape.GetType() == b2Shape.e_polygonShape) {
world.DestroyBody(other);
break;
}
}
edge = edge.next;
}
The meat of the code is the "update2" function. This is called at the regular interval. The Step function is called to update all the box2d objects. Then the canvas is cleared and the objects are processed to redraw everything. This function also removes objects that have strayed out of view, or have been "consumed" by a collision. Finally, new images are randomly added to the display
Typically mouse and/or keyboard events will be set to only interacting with the animation. These are fairly standard javascript, and a number of examples can be found in google. This demo is known to work in Chrome, Firefox, Internet Explorer 9 and Internet Explorer 10. It should probably also work in Opera and Safari and other modern browsers. Since it does put a lot of objects on the screen, it can get sluggish after it is run for a while. (You can tweak the object size, gravity and random creation to alter the impacts.) My main goal here was to figure out how to easily link the images to the box2d. Now the fun can begin.
var b2Vec2 = Box2D.Common.Math.b2Vec2, b2BodyDef = Box2D.Dynamics.b2BodyDef, b2Body = Box2D.Dynamics.b2Body, b2FixtureDef = Box2D.Dynamics.b2FixtureDef, b2World = Box2D.Dynamics.b2World, b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape, b2Shape = Box2D.Collision.Shapes.b2Shape, b2CircleShape = Box2D.Collision.Shapes.b2CircleShape; var b2Math = Box2D.Common.Math.b2Math;Home