Unleash the power of HTML 5 Canvas for gaming - Eternal Coding - HTML5 / Windows / Kinect / 3D development - Site Home - MSDN Blogs

Unleash the power of HTML 5 Canvas for gaming


 

Unleash the power of HTML 5 Canvas for gaming

  • Comments 18

image

HTML 5 browsers and HTML 5 for Windows 8 Metro are now serious candidates for developing modern games.   

With the canvas, you have access to an 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.  

I will use a sample in the following chapters to help me demonstrate the concepts I introduce. The sample is a 2D tunnel effect I wrote for the Coding4Fun session I presented for the TechDays 2012 in France (http://video.fr.msn.com/watch/video/techdays-2012-session-technique-coding4fun/zqy7cm8l).

This effect is mainly inspired by some Commodore AMIGA code I wrote when I was a young demomaker back in the 80’s Sourire.

Now, it only uses canvas and Javascript (where original code was only based on 68000 assembler):


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 = {};

    // on load
    texture.addEventListener('load', function () {
        var textureCanvas = document.createElement('canvas'); // off-screen canvas

        // Setting the canvas to right size
        textureCanvas.width = this.width; //<-- "this" is the image
        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);

    // Loading
    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:

// Texture
var texture = loadTexture("soft.png", function () {
    // Launching the render
    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:

// Setting hardware scaling
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:

image

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.

image

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:

// precompute arctangent
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:

image

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 Sourire

Final result

Applying all the optimizations gives you the following report:

image

 

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):

And after applying all of these optimizations:

We can resume the impact of each optimization with the following chart which gives you the framerate measured on my own computer

image

Going further

With these key points in mind, you are ready to produce real time fast and fluid games for modern browsers or Windows 8!

Leave a Comment
  • Please add 7 and 7 and type the answer here:
  • Post
  • Super Useful, Thanks..!

  • i am getting 2 fps.. that is 2 frames per second on an iPhone 4s.   Not good

  • @Rich: not really adapted :)

  • Your code snippets are unreadable; they all show up as a single line (bunch of spans within a pre which is not resizing for some reason).

  • Oups, it's now fixed. Thank you for pointing me this out :)

  • Nice pointers. Will help a lot.

  • Works better/faster under Firefox than IE 9. How about IE 10?

  • 1 fps on windows phone 7...

  • These examples only get you so far, before you find a roadblock.  I was following the IE Test drive examples and was happy to see in the Galactic demo (ie.microsoft.com/.../Default.html) that 3D geometry could be rendered to the screen.  I decided to use the same THREE.js library in my own project, but then found out how IE falls short of the goal of WebGL.  If you see what other browsers are doing with technologies like these (mrdoob.github.com/three.js), it makes you wonder when IE will catch up.

  • Hi,

    In the black boxes, when I click on "Click To Start" nothing happens. I am using Internet Explorer 8.

    Do I need a newer browser?

    Thanks,

    Jack

  • Yes you need at least IE9

  • New iPad - 2 fps

  • This demo is not intended to run on small devices (the rendering resolution of the canvas is too high)

  • My results (without any optimization/with all optimizations)

    Firefox 12......5 fps/33 fps

    Chrome 18....5 fps/19 fps  

    IE 9................1 fps/11 fps

    Safari 5..........1 fps/5 fps

  • Well, I ran this on

    Safari 5.1.5 (7534.55.3).............10 fps/57fps

    Non Optimized / Optimized

    Running this on an iMac with 2.8 GHz Intel Core i7, 12 GB, Version 10.7.3 :)

Page 1 of 2 (18 items) 12