Search This Blog

Box2D and Web workers for JavaScript developers

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!

This is the fourth post in a series on Box2D for the JavaScript developer.

Intro

Our previous post on Box2D frame rates, world step rates, and adaptive rates showed how changing the FPS and world step frequency has an effect on performance and perception. The rendering and physics simulations were running in the same loop, which is typical for web programming.

However, nothing in the simulation requires it to be tied so closely to the render loop. What if we could separate the two loops (physics update loop and render loop) and run them on different threads?

Enter Web workers

Yay! for HTML5 because modern browsers can in fact spawn what is essentially a new process for long running, computationally intensive tasks. Web workers are designed to allow JavaScript to run independently of the main event loop for the page. This can keep your UI responsive for the user as the browser, in a separate process, is crunching on expensive algorithms which would otherwise lock up the browser.

Browser Support

Thanks to the awesome caniuse.com we can see that support for Web workers is pretty good, even the new Internet Explorer 10 will include it. For our purposes, those users without Web workers can either get Chrome Frame or you as the developer can gracefully degrade the application experience by placing the Box2D simulation into the render loop.

For users with modern browsers, we can provide an enhanced experience by using more than one CPU core.

Web worker details

Workers shouldn't be considered threads, as they are generally more heavy and should be considered more like processes. For example, you should only spawn a few workers per page.

Creating a new worker is straight forward:

var worker = new Worker('worker.js');

Notice how you need to supply a path to a script to be run by the worker. While you can't just point a worker to a function on your page, you can use the Blob Builder to construct JavaScript from the host page to hand to the worker. Generally, though, you will write your worker code as a separate file.

Restrictions

A worker can't access the DOM of your page. It can't add elements, respond to user events, and it can't write to console.log(. Best to think of a worker as a server for your client app.

Communicating with a worker

Use postMessage() and onmessage to send and receive messages between the host page and the worker. This should look familiar, it's how you communicate between different documents and iframes.

Notice event.data which contains the object sent from the worker. I've noticed the event data can contain any object, not just simple strings or numbers. Data sent from worker to host page will be serialized and copied from producer to consumer.

To send a message, use postMessage():

postMessage(msg);

To be clear, the data sent via postMessage() will be copied and serialized. This implies the consumer or receiver of the event will get a copy of the message.

This event queue is unbounded, in Chrome at least. Your producer should be aware of its message sending rate to ensure it doesn't get too far ahead of the consumer. Unfortunately, the Web worker spec doesn't have the API to manage the queue. If your producer is generating a lot of messages (as we will below) you will need to add throttling.

Debugging workers

It is unfortunately that workers can't access console.log(). It is more difficult to debug a worker, but not impossible. Luckily, the Chrome Developer Tools can debug a worker, although it is not automatic.

In the Scripts tag in the Dev Tools, you'll see a Worker inspectors drop down on the right. By clicking the Debug checkbox, the Dev Tools will place the worker inside an iframe of the host page (recent versions of Chrome >= 15 should not be debugging workers natively). You can then step through the worker and debug it like normal.

Box2D functions best if it can run at a consistent rate. By placing Box2D into a Web worker, we can shelter it from the browser's UI thread and let it run its loop independently.

Our strategy will be to use the host page to build the world and to perform all rendering via requestAnimationFrame. The worker will run the Box2D simulation at its own rate, and will post updates for all the objects in the world back to the host page to render.

This separation of concerns lets the Box2D world update at its optimal rate, while the rendering of the world can run as fast as possible.

Creating the world

We start by creating a simple object hierarchy of Entities, both Circles and Rectangles. We need these entities because the host application doesn't know anything about Box2D, nor should it.

Note that after the bodies are configured, the ready flag is set to true. This flag is required because the worker has already started its loop, and we want to signal when the bodies are set so the simulation can begin.

Looping

The worker starts the loop immediately, before any message is received:

As you can see, the loop is managed by setInterval to govern the frequency, which we have set to 30Hz.

Different frequencies

If you recall, the render loop on the host page is managed with requestAnimationFrame which should be running at 60Hz. This means we are rendering twice as fast as we are updating our physics simulation. The render loop simply draws whatever it knows about at the time of the frame.

Updating the canvas

The physics simulation is running in the worker, and updating the state of all the bodies. After each update, it needs to send a message to the host page with the new state of the objects.

The sendUpdate function builds a temporary data structure to hold the x, y, and angle properties of all the objects. It only wants to inform the host page of bodies with IDs set in the UserData field. There is no need to send information about static bodies like the ground, as they don't change.

Note: a future enhancement will only send updated state for objects that are not at rest. If the object is sleeping, there's no need to send an update to the host page.

Remember that the world object is copied and serialized as it's sent via postMessage.

Updating entities

The host page receives the update messages from the worker and updates the state of all the entities.

Yay! for polymorphism, as we let the implementation of Entity draw itself to the canvas.

Example

Now that we've walked through the code, let's see it in action:

Reload the page a few times to see it in action. Remember that the physics loop is running via a worker, and updating the host page at 30Hz with the new coordinates and angles for each of the entities. The rendering is governed by requestAnimationFrame on the host page, running at 60Hz.

Dangers

This is all well and good, but there is an added difficulty with this configuration. The Web worker, or producer, can generate too many messages for the host page, or consumer, to deal with. Although the queue is an unbounded vector in Chrome, at some point, if message sending rate isn't closely monitored, the browser will run out of memory.

The trick is to send messages just fast enough for the consumer to use them. Sending too many and you'll eventually run out of memory. Send too few and the perceived animation performance will suffer. If the consumer falls too far behind, it will want to catch up by pulling messages from the queue and throwing them out.

Web workers do not have APIs to manage the queue, so it's up to the application to build throttling logic and coordination between producers and consumers.

Summary

We've learned that we can separate our rendering and physics simulations by using Web workers. Placing Box2D into a worker allows it to run in a separate process, thus speeding up the rendering loop in the host page.

Future improvements include sending less updates from the worker when objects are asleep, and not rendering if all objects are asleep. Also, a necessary improvement would include update throttling so that the producer doesn't send more than the consumer can handle.

To learn more about JavaScript development for HTML5 games, I encourage you to join us at New Game.

New Game is the first conference in North America dedicated to HTML5 game development. Key developers and engineers from Google, Opera, Mozilla, Gamesalad, Spil, Subsonic, Mandreel, and others will present sessions on bringing modern games to the modern web browser. Keynotes from EA and Zynga will inspire and amaze. New Game is a conference by developers and for developers. Register today!

Popular posts from this blog

Now, this has to have a built-in somewhere in Scala , because it just seems too common. So, how to convert an Array to a List in Scala? Why do I need this? I needed to drop to Java for some functionality, which in this case returns an Array. I wanted to get that Array into a List to practice my functional programming skillz. **Update**: I figured out how to convert Arrays to Lists the Scala way. Turns out it's a piece of cake. val myList = List.fromArray(Array("one", "two", "three")) or val myList = Array("one","two","three").elements.toList The call to elements returns an Iterator , and from there you can convert to a List via toList . Nice. Because my first version wasn't actually tail recursive, what follows is a true tail recursive solution, if I were to implement this by hand. The above, built in mechanism is much better, though. object ArrayUtil { def toList[a](array: Array[a]): List[a] = { d

In which I port a snazzy little JavaScript audio web app to Dart , discover a bug, and high-five type annotations. Here's what I learned. [As it says in the header of this blog, I'm a seasoned Dart developer. However, I certainly don't write Dart every day (I wish!). Don't interpret this post as "Hi, I'm new to Dart". Instead, interpret this post as "I'm applying what I've been documenting."] This post analyzes two versions of the same app, both the original (JavaScript) version and the Dart version. The original version is a proxy for any small JavaScript app, there's nothing particularly special about the original version, which is why it made for a good example. This post discusses the differences between the two implementations: file organization, dependencies and modules, shims, classes, type annotations, event handling, calling multiple methods, asynchronous programming, animation, and interop with JavaScript libraries. F

In which the virtues of automated mechanical arboreal pruning are extolled over quaint manual labor, as applied to web development build processes. The setup Ever notice how the primary bit of marketing for many traditional web programming libraries is their download size? Why is that? Check this out: Why does size matter so much for these libraries? Your first instinct is probably, "because the more bytes you shuttle across the wire, the slower the app starts up." Yes, this is true. I'd also say you're wrong. The primary reason that size matters for these libraries is because traditional web development has no intelligent or automated way to prune unused code so you can ship only the code that is used over the wire. The web is full of links, yet web dev has no linker The web development workflow is missing a linking step. A linker's job is to combine distinct project files into a single executable. A smart linker will only incl