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.