2010-06-30

Emitting Physics Events with Box2d and Node

Node tries to make as much application logic as possible asynchronous and non-blocking. Most objects communicate by emitting "events" to which other objects can subscribe with listeners. The listeners handle the emitted events asynchronously. All if this is pretty well documented and examples can be found in the node source code itself, most of its included modules, and many applications that make use of the framework.

In sticking with this pattern for nodehockey, I wanted to make the physics simulation asynchronous with sending state updates to the clients. Here's the code (refactored for brevity):
var sys = require("sys"),
    events = require("events"),
    b2d    = require("./path/to/box2dnode");

var PhysicsSim = function () {
    events.EventEmitter.call(this);
    this.runIntervalId = null;

    // Initialize the simulator 
    var worldAABB = new b2d.b2AABB();
    worldAABB.lowerBound.Set(-1000, -1000);
    worldAABB.upperBound.Set( 1000,  1000);
    var gravity = new b2d.b2Vec2(0.0, -9.8);
    var doSleep = true;
    this.world = new b2d.b2World(worldAABB, gravity, doSleep);

    // Initialize physics entities
    // ...
}
sys.inherits(PhysicsSim, events.EventEmitter);
PhysicsSim.prototype.getState() = function () {
    // Get whatever state information we should return to clients
    // ...
    return state;
}
A new class is defined to handle all the physics simulation. The class is responsible for initializing the world (see the Box2D documentation for more on this) and setting up the entities to be simulated. It includes a method for retrieving the current state of the simulated world.

The important bit is setting the class up to be an event emitter, accomplished by the first line in the function (to call the "parent" constructor) and the sys.inherits call which will add all the prototype properties and functions. This will allow the simulator to broadcast events to which other objects can subscribe:
PhysicsSim.prototype.run = function () {
    this.pause();
    this.runIntervalId = setInterval(function (sim, t, i) {
            sim.world.Step(t, i);
            sim.emit("step", sim.getState());
        }, 50, this, 1.0/60.0, 10);

    this.emit("run");
}
PhysicsSim.prototype.pause = function () {
    if (this.runIntervalId != null) {
        clearInterval(this.runIntervalId);
        this.runIntervalId = null;
    }
    this.emit("pause");
}
Three events are defined here. Starting with the last two, "run" is broadcast whenever the simulation begins or is un-paused, and "pause" is broadcast whenever we halt the simulation.

The more interesting event is the "step" event, which is broadcast when the simulation runs another timestep. An interval timer is set up to run the simulation every 50 milliseconds. The simulator is passed in, as well as a timestep and a simulation interval.

In Box2D, the timestep determines how much "in world" time passes with each step. The smaller the value, the more incremental the calculation of the next state. In the above code, the timestep is 1/60th of a second.

The second argument is the simulation interval (the name is a bit misleading.) This is the number of passes in each step that the simulator will take when calculating collisions and movement. Every time a body in the simulated world moves, it has the chance to affect the other bodies. Since this can cascade infinitely within a step, the interval limits how many times the calculations will actually be run. Note that Box2D is smart enough to stop calculating if it doesn't need the full number of passes.

The interval's wait time (50 milliseconds) does not have to match the simulator's timestep. Increasing or decreasing the wait time will speed up or slow down the rate at which the simulator calculates the next step completely independently of how much "in world" time passes with each step, determined by the timestep. This can be useful for adding slow-motion or fast-forward effects to simulations and games.

Once the step is calculated, a "step" event is emitted with the new game state. This is the signal that will be used to let all the clients know that there is a new state they must handle. The following code demonstrates catching these events:
var sim = new PhysicsSim();
sim.addListener("run", function () {
        sys.puts("Simulation running");
    })
    .addListener("pause", function () {
        sys.puts("Simulation paused");
    })
    .addListener("step", function (state) {
        sys.puts("New simulation step: " + sys.inspect(state));
    });
sim.run();
setTimeout(function () {
    sim.pause();
}, 1000);
Running this code in a terminal will show the simulator starting, the first 20 steps of the simulation, then the simulator pausing.

Any number of listeners can be subscribed to these events, even after the simulation is running. Here's an example using websockets:
var ws = require("./path/to/websocket/library");
var sim = new PhysicsSim();
sim.run();

ws.createServer(function (connection) {
    connection.ready = false;
    connection.addListener("connect", function () {
            connection.ready = true;
        });
    sim.addListener("step", function (state) {
            if (connection.ready) {
                connection.write(JSON.stringify(state));
            }
        });
}).listen(8080);
When run, a simulator is created and begins stepping through the simulation. At this point, it is broadcasting "step" events, though there are no listeners.

A websocket server is created and starts listening for connections on port 8080. When it receives a new connection, it attaches a new listener to the simulator for that connection to receive step events, JSON serialize the state that is broadcast and send it to the websocket client.

The "connect" listener on the websocket connection is used to prevent sending step events to the client before the connection is fully initialized. Since all the event handling takes place asynchronously, it is very likely that a step event could be received and sent through the websocket before the socket is done connecting. The connect event on the websocket doesn't fire until the connection is complete, so at that point we can signal the step listener to start sending states to the client.

Since the simulator runs independently of the client connections, clients can connect, disconnect and reconnect at any time, and immediately see the same state as all other clients. Nothing needs to save the simulator state to replay to clients that connect at later points.

If a client does want to see what it has missed, a listener could be added to the step event that would save each state, then replay those states to the client faster than new states are broadcast. Once all the states are replayed, the client could then begin to receive states at the normal pace. This is an exercise left for the reader.

No comments:

Post a Comment