Box2D with Complex and Concave Objects, 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!






Intro

Previously in this Box2D for the JavaScript developer series, I covered how to build and simulate arbitrary polygons. Not entirely arbitrary, as I mentioned that you can only build convex objects. Turns out I was both right and wrong. Read on to learn how to build more complex and even concave bodies.


Example

The below screenshot has an example of a complex object. The green square + triangle is also concave.


Below is the live version:



The stand alone example for the above demo is available, as well as the source code for review.

Explanation

I'll start using more exact terminology now that we've covered the basics of getting objects onto the screen and interacting with each other. It's important to understand the specifics now that we're getting more complex.

A Body is the rigid chunk of matter.

A Shape is a geometrical object, such as a polygon, circle, square, etc. A Shape must be convex.

A Fixture binds a shape to a body.

Up to this point, we've had a one-to-one relationship between Body, Shape, and Fixture. Turns out that Box2D Bodies can have multiple shapes!  If you extrapolate what that means, you can see that we can build Bodies with complex shapes.


Code

With this more specific understanding and terminology, let's use it to build complex bodies (see, I used the right term there :)

I've extended our JSON shorthand object definitions to support one or more polygons for a body. (This is non-standard Box2D code, just something I put together to make it easier to play around.)

var initialState = {"0": {id: 0, x: 10, y: 5, radius: 2},
                    "1": {id: 1, x: 5, y: 5, polys: [
                      [{x: 0, y: 0}, {x: 1, y: 0}, {x: 0, y:2}] // triangle
                    ]},
                    "2": {id: 2, x: 9, y: 4, halfHeight: 1.5, halfWidth: 0.9},
                    "3": {id: 3, x: 4.5, y: 3, polys: [
                      [{x: 0, y: -2}, {x: 2, y: 0}, {x: 0, y:2}, {x:-0.5, y: 1.5}] // odd shape
                    ]},
                    "4": {id: 4, x: 10, y: 10, polys: [
                        [{x: -1, y: -1}, {x: 1, y: -1}, {x: 1, y: 1}, {x: -1, y: 1}], // box
                        [{x: 1, y: -1.5}, {x: 2, y: 0}, {x: 1, y: 1.5}]  // arrow
                    ], color: "green"}
};

Notice the last definition has two polygon definitions. The first has four points for a box, and the second has three points for a triangle. Just like arbitrary shapes, the points are defined relative to the position of the body as specified by the x,y coordinates.

To construct the Box2D objects from the above definition, I refactored the original code a bit to handle more than one Fixture and Shape per Body.

bTest.prototype.setBodies = function(bodyEntities) {
    this.bodyDef.type = b2Body.b2_dynamicBody;
    
    for(var id in bodyEntities) {
        var entity = bodyEntities[id];
        
        this.bodyDef.position.x = entity.x;
        this.bodyDef.position.y = entity.y;
        this.bodyDef.userData = entity.id;
        var body = this.world.CreateBody(this.bodyDef);
        
        if (entity.radius) {
            this.fixDef.shape = new b2CircleShape(entity.radius);
            body.CreateFixture(this.fixDef);
        } else if (entity.polys) {
            for (var j = 0; j < entity.polys.length; j++) {
                var points = entity.polys[j];
                var vecs = [];
                for (var i = 0; i < points.length; i++) {
                    var vec = new b2Vec2();
                    vec.Set(points[i].x, points[i].y);
                    vecs[i] = vec;
                }
                this.fixDef.shape = new b2PolygonShape;
                this.fixDef.shape.SetAsArray(vecs, vecs.length);
                body.CreateFixture(this.fixDef);
            }
        } else {
            this.fixDef.shape = new b2PolygonShape;
            this.fixDef.shape.SetAsBox(entity.halfWidth, entity.halfHeight);
            body.CreateFixture(this.fixDef);
        }
    }
    this.ready = true;
}

The HTML5 Canvas code didn't change too much. As you can see below, the canvas is translated and rotated once, and then all of the shapes are drawn with a series of moveTo() and lineTo() calls.

PolygonEntity.prototype.draw = function(ctx) {
  ctx.save();
  ctx.translate(this.x * SCALE, this.y * SCALE);
  ctx.rotate(this.angle);
  ctx.translate(-(this.x) * SCALE, -(this.y) * SCALE);
  ctx.fillStyle = this.color;

  for (var i = 0; i < this.polys.length; i++) {
    var points = this.polys[i];
    ctx.beginPath();
    ctx.moveTo((this.x + points[0].x) * SCALE, (this.y + points[0].y) * SCALE);
    for (var j = 1; j < points.length; j++) {
       ctx.lineTo((points[j].x + this.x) * SCALE, (points[j].y + this.y) * SCALE);
    }
    ctx.lineTo((this.x + points[0].x) * SCALE, (this.y + points[0].y) * SCALE);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();
  }

  ctx.restore();
  
  Entity.prototype.draw.call(this, ctx);
}

Summary

Box2D supports bodies with complex and even concave shapes by binding more than one Fixture and Shape to a Body.

Next Up

Did this help clear up shapes and bodies? What would you like to see next? Please leave comments below and let me know.

Popular posts from this blog

Lists and arrays in Dart

Converting Array to List in Scala

Null-aware operators in Dart