Box2D, Web workers, and Page Visibility API

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!








UPDATE: The below code is not optimal. While this is interesting, I encourage you immediately follow up with the next post on a better way to do it.

(If you're not familiar with Web workers, I encourage to read the previous post first before diving into this post.)

Intro

A previous article discussed running Box2D in a Web worker. We found that, by putting the physics simulation into a separate process, we have better performance as we free up the main JavaScript thread to focus on rendering.

But we can do better.

I see it's not visible

It's great that your awesome physics based WebGL game is running at 60FPS. While I'm playing the game, that is. When I tab out to check how to make candied bacon ice cream (go ahead, I'll wait), there's no reason for my browser and CPU to grind away rendering 60FPS or simulating physics. As developers, we can provide for a better experience by pausing everything when the game is not visible.

Because we are good modern HTML5 game developers, we are already using requestAnimationFrame to schedule our draws. One of the benefits of this technique is allowing the browser to pause animation loops if the canvas, window, or tab is not visible. If the user can't see the element, no reason to keep animating. Not only does this save battery, but it effectively pauses the game which is a nice touch.

Or does it?

Workers sneakily keep working

Our Box2D in Web workers demo showed that you can run your physics simulation in a worker. While the render loop, powered by requestAnimationFrame, will pause if the tab is placed into the background, the worker will happily keep working and filling up that message queue with updates.

You can verify this by using Chrome's Task Manager. First, load up the original Box2D in a Web worker demo, then open the Task Manager (wrench icon, tools, task manager). Sort by CPU so you can see the tab running the simulation. Now, put that tab into the background, which pauses the animation and render loops. Notice, however, that CPU doesn't go to zero (on my machine it hovers around 10%)


You might be thinking, "wait a minute, why only 10%? why not more?" Good observation! It turns out that Chrome (and other browsers like Firefox) will clamp your setInterval and setTimeout calls to 1 second if your tab goes to the background. This is a good start, but it's still wasteful to run a physics simulation once a second if the user can't see or interact with it.

Not only is it wasteful, the simulation keeps right on simulating. The user doesn't have any chance to see the results or interact. It would be a bad experience if the user comes back to the tab to see the world completely changed.

Now I know I'm not visible

To fully pause the game, both the render loop and the physics simulation, we need to know if the tab or window is visible to the user. Enter the Page Visibility API!

The Page Visibility API provides a callback which signals to our application when the tab or window is visible to the user. For example, you can pause a video if the user hides the window.

Luckily, the API is very simple and straight forward. The host page, not the worker, but register for the callback:

    document.addEventListener('webkitvisibilitychange', function() {
      if (document.webkitHidden) {
        worker.postMessage({'cmd': 'hidden'});
        console.log('page now hidden, sent msg to worker');
      } else {
        worker.postMessage({'cmd': 'visible'});
        console.log('page now visible, sent msg to worker');
      }
    }, false);

You can see above that the main page listens to the webkitvisibilitychange event. Once fired, it looks at the document.webkitHidden flag. It then sends a message to the worker about the state change.

Over in the worker, we listen for the message and stop or start the loop accordingly:

var intervalId = setInterval(loop, 1000/30);

self.onmessage = function(e) {
    switch (e.data.cmd) {
        case 'visible':
            if (intervalId == null) {
                intervalId = setInterval(loop, 1000/30);
            }
            break;
        case 'hidden':
            clearInterval(intervalId);
            intervalId = null;
            break;
        case 'bodies':
            box.setBodies(e.data.msg);
            break;
    }
};

Demo

Try it out for yourself. Reload this page (or go to the stand alone version of the below example) and switch to another tab before the simulation finishes.

For the first test, load up the Task Manager to see zero CPU after the tab is hidden. For the second test, come back to this tab and notice how every object picks up right where it left off.

(Contrast this behavior with the original Box2D and Web worker demo, you'll see the bodies jump ahead when you return to the tab)



Grab the source code for the example above or check out the stand alone version.

Summary

Using a combination of requestAnimationFrame and Page Visibility API, you can control both the rendering and physics simulations when the game or app is no longer visible. This saves battery and provides a more consistent user experience when the user returns to your game or app.

Next Up

I took a closer look at the worker code and realized it had problems. Read on...

Popular posts from this blog

Lists and arrays in Dart

Converting Array to List in Scala

Null-aware operators in Dart