Building a Game Mainloop in JavaScript

Posted: by Joe under Game Development

HTML5 is being touted as the next technology for browser games, but even common and straight forward parts like a ‘mainloop’ can be difficult to write in JavaScript.

This is a thorough explanation of how to go about writing your own, based upon our experiences of building Play My Code. Novice programmers please note: this article applies to JavaScript coding and not to coding using PMC, where we have solved this problem for you already.

Why?

Most games update in one of two ways. The first is to wait for input, like a mouse click, and then update or draw the game based on this. Under this system, the game only updates when a user interacts with it. This works well for card and puzzle games, where it’s normal for nothing to happen for fairly long periods of time.

However most action games, such as a first-person shooters, have to repeatedly update and redraw multiples times a second as they normally contain elements which move and react independently to player input. Essentially:

    while ( true ) {
        updateGame();
        drawGame();
    }

This is often referred to as a game’s ‘mainloop’.

Getting back to JavaScript

The first thing to note is that you cannot do the above in JavaScript, because the script must end in order for the browser to update the page. This includes displaying anything you’ve drawn to the canvas, and allowing the user to interact with the page (and in many cases, the rest of the browser).

So the first step is to break down your mainloop into a function, which will then be called repeatedly.

    var mainloop = function() {
        updateGame();
        drawGame();
    };
    while ( true ) {
        mainloop();
    }

Remove the while loop

Again, to stop the page from locking up, we remove the while loop. The easiest way to achieve this is to use setInterval. Given a function, it will re-call it repeatedly, until we tell it to stop.

    var mainloop = function() {
        updateGame();
        drawGame();
    };
    setInterval( mainloop, ONE_FRAME_TIME );

ONE_FRAME_TIME represents how often the function gets called, and ideally this should be run at 60 frames per second (which matches most refresh rates).

The time is in milliseconds, so to work out the time for one frame we divide 1 second (1000 milliseconds) by 60 frames.

    var ONE_FRAME_TIME = 1000 / 60 ;
    var mainloop = function() {
        updateGame();
        drawGame();
    };
    setInterval( mainloop, ONE_FRAME_TIME );

It works out to be about around 16.666 milliseconds.

More Efficient Looping

However, setInterval is not the most efficient way to build a mainloop. Some modern browsers provide a specialist function that can be used called ‘requestAnimationFrame’. You pass in your function, and the browser will call it later for you.

    var mainloop = function() {
        updateGame();
        drawGame();
    };
    window.requestAnimationFrame( mainloop );

It is more efficient because it is built with animation in mind. The browser will try to call it at the right time- so your changes are shown as soon as possible- and the timing code behind it tries to be more accurate then setInterval, to give a more stable frame rate.

However this is an experimental feature, and is not fully supported in all browsers. So you need to ensure you use the ‘requestAnimationFrame’ version supported by the host browser. You can do this using:

    var mainloop = function() {
        updateGame();
        drawGame();
    };

    var animFrame = window.requestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame    ||
            window.oRequestAnimationFrame      ||
            window.msRequestAnimationFrame     ||
            null ;
    animFrame( mainloop );

It will test for, and return, the first ‘requestAnimationFrame’ version it can find, using each of the browser name prefixes. If they fail, then null is returned. I will solve this issue later.

This Only Calls it Once!

The above code only calls ‘mainloop’ once, not multiple times. To turn it back into a proper loop, we need to call ‘mainloop’ multiple times using ‘animFrame’.

We can achieve this by wrapping it up within a closure, which recursively calls itself.

    var mainloop = function() {
        updateGame();
        drawGame();
    };

    var animFrame = window.requestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame    ||
            window.oRequestAnimationFrame      ||
            window.msRequestAnimationFrame     ||
            null ;

    var recursiveAnim = function() {
        mainloop();
        animFrame( recursiveAnim );
    };

    // start the mainloop
    animFrame( recursiveAnim );

What if animFrame === null ?

If ‘requestAnimationFrame’ is not supported (such as in IE 9), then ‘animFrame’ will be null. In this case, we fall back onto the ‘setInterval’ used earlier.

    var mainloop = function() {
        updateGame();
        drawGame();
    };

    var animFrame = window.requestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame    ||
            window.oRequestAnimationFrame      ||
            window.msRequestAnimationFrame     ||
            null ;

    if ( animFrame !== null ) {
        var recursiveAnim = function() {
            mainloop();
            animFrame( recursiveAnim );
        };

        // start the mainloop
        animFrame( recursiveAnim );
    } else {
        var ONE_FRAME_TIME = 1000.0 / 60.0 ;
        setInterval( mainloop, ONE_FRAME_TIME );
    }

Alternatively, you could also use the technique suggested by Paul Irish, which creates a function that essentially looks like requestAnimationFrame.

WebKit Improvements

That’s all you need for a cross browser mainloop, but you can improve it further. WebKit browsers feature a second parameter, where you pass in the canvas you are going to be drawing to. This allows the browser to only update that one area.

    var mainloop = function() {
        updateGame();
        drawGame();
    };

    var animFrame = window.requestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame    ||
            window.oRequestAnimationFrame      ||
            window.msRequestAnimationFrame     ||
            null ;

    if ( animFrame !== null ) {
        var canvas = $('canvas').get(0);

        var recursiveAnim = function() {
            mainloop();
            animFrame( recursiveAnim, canvas );
        };

        // start the mainloop
        animFrame( recursiveAnim, canvas );
    } else {
        var ONE_FRAME_TIME = 1000.0 / 60.0 ;
        setInterval( mainloop, ONE_FRAME_TIME );
    }

I’ve used jQuery above to grab my canvas element, mainly to keep my example as short as possible. You can replace that with your own JavaScript code.

In Firefox and other non-WebKit browsers, passing in the canvas as the second parameter is simply ignored. It either optimises, or does nothing.

Firefox improvements

There is a second way to call the mainloop in FireFox, by attaching it to the ‘mozBeforePaint’ event. In my personal experience, I find this gives a minor speed-up (about 10 to 60 milliseconds).

So if we are using Firefox, we should use that instead of the recursive code above. To detect for Firefox, again I am just using jQuery.

    var mainloop = function() {
        updateGame();
        drawGame();
    };

    var animFrame = window.requestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame    ||
            window.oRequestAnimationFrame      ||
            window.msRequestAnimationFrame     ||
            null ;

    if ( animFrame !== null ) {
        var canvas = $('canvas').get(0);

        if ( $.browser.mozilla ) {
            var recursiveAnim = function() {
                mainloop();
                animFrame();
            };

            // setup for multiple calls
            window.addEventListener("MozBeforePaint", recursiveAnim, false);

            // start the mainloop
            animFrame();
        } else {
            var recursiveAnim = function() {
                mainloop();
                animFrame( recursiveAnim, canvas );
            };

            // start the mainloop
            animFrame( recursiveAnim, canvas );
        }
    } else {
        var ONE_FRAME_TIME = 1000.0 / 60.0 ;
        setInterval( mainloop, ONE_FRAME_TIME );
    }

Now, in Firefox, when ‘animFrame’ is called it tells the browser to repaint. It then then runs the ‘MozBeforePaint’ event, where our mainloop sits.

Alternatively

This is what it takes to achieve a proper mainloop across browsers. Cross-browser issues like these are what Play My Code helps to solve, by handling these details for you under the hood. So, alternatively you could just build your game here, and save yourself a great deal of time and trouble.

4 Responses to Building a Game Mainloop in JavaScript

Nice tutorial. I suggest to add an example supporting deltas for simulation time. Google "coke and code" tutorials to take one example.

Nick Dunn

Great stuff! Is there a way to control the frame rate with requestAnimationFrame? I understand it's intrinsically tied to the refresh rate of the machine's graphics processor. But if one user has a very fast processor, and the code is using requestAnimationFrame, does that mean they'll get a far higher frame rate, in comparison to someone on an older machine that's falling back to setTimeout at 60fps (or maybe even struggling that!). Maybe I'm not of the actual question I'm asking yet. But it seems to be the final example above would yield very different frame rates depending on the machine. Would it be down to the game logic to normalise this, regardless of how many times the mainloop is called?

Joe

As far as I know, there is no way to manually set the frame rate using requestAnimationFrame. But if you want it to be lower, then you can chose to silently do nothing, and so skip a frame, when your mainloop function is called. But I would advise against that, because even on a fast machine, the browser will be restricting the frame rate for you. With setTimeout you can yield much higher refresh rates by using 'setTimeout(1)', and there are alternatives to setTimeout/setInterval, which will allow you to loop at even higher speeds then that. But there is no need to run faster, because the user will not see the update (because of their refresh rate). The real question for me is, if I have a very high refresh rate, such as 75, will the fps be higher with requestAnimationFrame? I do not the answer to that question, but I would guess that this behavior is browser specific. In regards to normalizing game speed, you need to use 'delta time' to achieve this. This was left out because it's a topic big enough to warrant it's own blog post.

  • Pingback: Come scrivere un “main loop” di un gioco in JavaScript | Edit - Il blog di HTML.it

  • Leave a Response

    Your email address will not be published. Required fields are marked *