HTML5 browsers and
HTML5 for Windows 8 metro style apps are now serious candidates for developing
modern games.
With the canvas, you
have access to a hardware
accelerated
space where you can draw the content of your game and with some tips and tricks
you will be able to achieve a splendid 60 frame per second render. This
notion of fluidity is really important in games because the smoother the game
is the better the feeling of the player is.
The goal of this
article is to give you some keys on how to get the maximum power from HTML 5
canvas. This effect is mainly inspired by some Commodore AMIGA code I wrote
when I was a young demomaker back in the 80’s .
Now, it only uses canvas
and Javascript (where original code was only based on 68000 assembler):
<insert tunnel.zip package here like this page>
The complete code is
available there: http://www.catuhe.com/msdn/canvas/tunnel.zip
The aim of this
article is not to explain how the tunnel is developed but how you can start
from a given code and optimize it to achieve real time performance.
Using an
Off-Screen Canvas to Read Picture Data
The first point I
want to talk about is how you can use a canvas to help you read picture data.
Indeed, on every game, you need graphics for your sprites or your background.
The canvas has a really helpful method to draw an image: drawImage. This
function can be used to draw a sprite in the canvas because you can define a
source and a destination rectangle.
But sometimes it is
not enough. For example, it is not sufficient when you want to apply some
effects on the source image. Or when the source image is not a simple bitmap
but a more complex resource for your game (for instance, a map where you need
to read data from).
In these cases, you
need to access internal data of the picture. But the Image tag do not
have a way to read its content. And this is where the canvas can help you!
Indeed, every time
you need to read the content of a picture, you can use an off-screen canvas.
The main idea here is to load a picture and when the picture is loaded, you
just have to render it in a canvas (not included in the DOM). You can then get
every pixel of the source image by reading pixel of the canvas (which is really
simple).
The code for this
technique is the following (used in the 2D tunnel effect to read the tunnel’s
texture data):
var loadTexture = function (name, then) {
var texture = new Image();
var textureData;
var textureWidth;
var textureHeight;
var result = {};
texture.addEventListener('load', function () {
var textureCanvas = document.createElement('canvas');
textureCanvas.width = this.width; textureCanvas.height = this.height;
result.width = this.width;
result.height = this.height;
var textureContext = textureCanvas.getContext('2d');
textureContext.drawImage(this, 0, 0);
result.data = textureContext.getImageData(0, 0, this.width, this.height).data;
then();
}, false);
texture.src = name;
return result;
};
To use this code,
you have to take in account that the load of the texture is asynchronous and so
you have to use the then parameter to transmit a function to continue
your code:
var texture = loadTexture("soft.png", function () {
QueueNewFrame();
});
Using the
Hardware Scaling Feature
Modern browsers and
Windows 8 support hardware accelerated canvas. It means that, for
instance, you can use the GPU to rescale the content of the canvas.
In the case of the
2D tunnel effect, the algorithm requires to process every pixel of the canvas.
So for instance for a 1024x768 canvas you have to process 786432 pixels. And to
be fluid you have to do that 60 times per second which corresponds to 47185920
pixels per second !
It is obvious that
every solution that helps you reducing the pixel count will drastically improve
the overall performance.
And once again, the
canvas has a solution! The following code shows you how to use the hardware
acceleration to rescale the internal working buffer of a canvas to the external
size of the DOM object:
canvas.width = 300;
canvas.style.width = window.innerWidth + 'px';
canvas.height = 200;
canvas.style.height = window.innerHeight + 'px';
It is worth noting
the difference between the size of the DOM objet (canvas.style.width and
canvas.style.height) and the size of the working buffer of the canvas (canvas.width
and canvas.height).
When there is a
difference between these two sizes, hardware is used to scale the
working buffer and in our case it is a excellent thing: we can work on a
smaller resolution and let the GPU rescales the result to fit the DOM
object (with a beautiful and free filter to blur the result).
In this case, the
render is done in 300x200 and the GPU will scale it to the size of your window.
This feature is
widely supported across all modern browsers so you can count on it.
Optimize
Your Rendering Loop
When you are writing
a game, you must have a rendering loop where you draw all the components of
your game (background, sprites, score, etc..). This loop is the backbone of
your code and must be over-optimized to be sure that your game is fast and
fluid.
RequestAnimationFrame
One interesting
feature introduced by HTML 5 is the function window.requestAnimationFrame.
Instead of using window.setInterval to create a timer that calls
your rendering loop every (1000/16) milliseconds (to achieve a good 60 fps),
you can delegate this responsibility to the browser with requestAnimationFrame.
Calling this method indicates that you want to be called by the browser as soon
as possible to update graphics related stuff.
The browser will
include your request inside its own rendering schedule and will synchronize you
with its rendering and animations code (CSS, transitions, etc…). This solution
is also interesting because your code won’t be called if the window is not
displayed (minimized, fully occluded, etc.)
This can help
performance because the browser can optimize concurrent rendering (for example
if your rendering loop is too slow) and by the way produce more fluid
animations.
The code is pretty
obvious (please note the usage of the vendor specific prefixes):
var intervalID = -1;
var QueueNewFrame = function () {
if (window.requestAnimationFrame)
window.requestAnimationFrame(renderingLoop);
else if (window.msRequestAnimationFrame)
window.msRequestAnimationFrame(renderingLoop);
else if (window.webkitRequestAnimationFrame)
window.webkitRequestAnimationFrame(renderingLoop);
else if (window.mozRequestAnimationFrame)
window.mozRequestAnimationFrame(renderingLoop);
else if (window.oRequestAnimationFrame)
window.oRequestAnimationFrame(renderingLoop);
else {
QueueNewFrame = function () {
};
intervalID = window.setInterval(renderingLoop, 16.7);
}
};
To use this
function, you just have to call it at the end of your rendering loop to
register the next frame:
var renderingLoop = function () {
...
QueueNewFrame();
};
Accessing
the DOM (Document Object Model)
To optimize your
rendering loop, you have to follow at least one golden rule: DO NOT ACCESS THE
DOM. Even if modern browsers are optimized on this point, reading DOM object
properties is still to slow for a rendering loop.
For example, in my
code, I used the Internet Explorer 10 profiler (available in the F12
developer bar) and the result is obvious:
As you can see
accessing the canvas width and height takes a lot of time in my rendering loop!
The initial code
was:
var renderingLoop = function () {
for (var y = -canvas.height / 2; y < canvas.height / 2; y++) {
for (var x = -canvas.width / 2; x < canvas.width / 2; x++) {
...
}
}
};
You can remove the
canvas.width and canvas.height properties with 2 variables previously filled
with the good value:
var renderingLoop = function () {
var index = 0;
for (var y = -canvasHeight / 2; y < canvasHeight / 2; y++) {
for (var x = -canvasWidth / 2; x < canvasWidth / 2; x++) {
...
}
}
};
Simple, isn't ? It
may be sometimes hard to realize but believe me it is worth trying!
Pre-compute
According to the
profiler, the Math.atan2 function is a bit slow. In fact, this
operation is not hardly coded inside the CPU so the JavaScript runtime must add
some code to compute the result.
In a general way, if
you can pre-compute some long running code it is always a good idea. Here,
before running my rendering loop, I compute the result of Math.atan2:
var atans = [];
var index = 0;
for (var y = -canvasHeight / 2; y < canvasHeight / 2; y++) {
for (var x = -canvasWidth / 2; x < canvasWidth / 2; x++) {
atans[index++] = Math.atan2(y, x) / Math.PI;
}
}
The atans
array can then be used inside the rendering loop to clearly boost the
performance.
Avoid using Math.round,
Math.floor and parseInt
The last relevant
point is the usage of parseInt:
When you use a
canvas, you need to reference pixels with integer coordinates (x and y).
Indeed, all your computation are made using floating point numbers and you need
to convert them to integer at the end of the day.
JavaScript provides Math.round,
Math.floor or even parseInt to convert number to integer. But this
function makes some extra works (for instance to check ranges or to check if
the value is effectively a number. parseInt even first converts its
parameter to string!). And inside my rendering loop, I need to have a quick way
to perform this conversion.
Remembering my old
assembler code, I used a small trick: Instead of using parseInt, you
just have to shift your number to the right with a value of 0. The runtime will
move the floating value from a floating register to an integer register and use
an hardware conversion. Shifting this value to the right with a 0 value will
let it unchanged and so you can get back your value casted to integer.
The original code
was:
u = parseInt((u < 0) ? texture.width + (u % texture.width) : (u >= texture.width) ? u % texture.width : u);
And the new one is
the following:
u = ((u < 0) ? texture.width + (u % texture.width) : (u >= texture.width) ? u % texture.width : u) >> 0;
Of course this
solution requires that you are sure the value is a correct number
Final Result
Applying all the
optimizations gives you the following report:
You can see that now
the code seems to be well optimized with only essential functions.
Starting from the
original render of the tunnel (without any optimization):
<insert tunnel.zip package here like this page>
And after applying
all of these optimizations:
<insert tunnel.zip package here like this page>
We can resume the
impact of each optimization with the following chart which gives you the
framerate measured on my own computer
Going
Further
With these key
points in mind, you are ready to produce real time fast and fluid games for modern browsers like IE10 or metro apps for Windows
8!