Sprite Animations: Boss Kitty

This is a continuation of a tutorial begun in Sprite Animations: Vampire Kitty Lives.

That article ended with the promise we would make some improvements.

requestAnimFrame

setTimeout is good, and it works well in just about every browser, but there’s an even better method, requestAnimFrame.

requestAnimFrame basically acts as a setTimeout, but the browser knows you’re rendering a frame so it can optimize the draw cycle, as well as how that interacts with the rest of the page reflow. It will even detect if the tab is visible and not bother drawing it if it’s hidden, which saves battery (and yes, web games cycling at 60fps will burn battery). Under the hood, the browsers also get the opportunity to optimize in other mysterious ways they don’t tell us much about. In my experience with heavier frame loads (hundreds of sprites especially) there can be substantial gains in performance; especially on recent browser builds.

One caveat I’d add is that in some cases setTimeout will outperform requestAnimFrame, notably on mobile. Test it out and config your app based on the device.

The call to use requestAnimFrame is distinct across different browsers so the standard shim (thanks to Paul Irish) to detect this is:

window.requestAnimFrame = (function(){
    return  window.requestAnimationFrame       ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame    ||
            window.oRequestAnimationFrame      ||
            window.msRequestAnimationFrame     ||
            function( callback ){
              window.setTimeout(callback, 1000 / 60);
            };
})();

There’s also a built-in fall back to plain old setTimeout if requestAnimFrame support is not available.

You then need to modify the update method to repeatedly make the request:

function update() {
    requestAnimFrame(update);
    redraw();
    frame++;
    if (frame >= 6) frame = 0;
}

Calling the requestAnimFrame before you actually carry out the render/update tends to provide a more consistent result.

On a side note, when I first started using requestAnimFrame I searched around for how it would be timed, but couldn’t find anyting. That’s because it isn’t. There’s no equivalent to setting the MS delay you’ll find with setTimeout, which means you can’t actually control the frame rate. Just do your work, and let the browser take care of the rest.

Another thing to watch out for is if you are using requestAnimFrame from within your own closure, then you’ll need to do a native wrapping to call it, such as:

my.requestAnimFrame = (function () {
    var func = window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        window.oRequestAnimationFrame ||
        window.msRequestAnimationFrame ||
        function (callback, element)
        {
            window.setTimeout(callback, 1000 / this.fps);
        };

    // apply to our window global to avoid illegal invocations (it's a native) return function (callback, element) {
        func.apply(window, [callback, element]);
    };
})();

Time-based Animation

Next we need to solve the speed at which poor kitty has been running. Right now the animation frame advances according to the frame rate, which is going to jump around on different devices. That’s bad; if you’re moving a character and animating at the same time, things are going to look weird and inconsistent across different frame rates. You can try to control the frame rate but in the end basing animation on real timing is going to make for a better all round experience.

You’ll also find that timing in general in games is going to apply to everything you do: firing rate, turning speed, accerlation, jumping, they’ll all be better handled using proper timing.

To advance kitty at a regulated speed we need to track how much time has passed, and then advance the frames according to the time allocated to each one. The basics of this is:

  1. Set an animation speed in terms of frames per second. (msPerFrame)
  2. As you cycle the game, figure out how much time has passed since the last frame (delta).
  3. If enough time has passed to move the animation frame forward, then advance the frame and set the accumulated delta to 0.
  4. If enough time hasn’t passed, remember (accumulate) the delta time (acDelta).

Here’s this in our code:

var frame = 0;
var lastUpdateTime = 0;
var acDelta = 0;
var msPerFrame = 100;

function update() {
    requestAnimFrame(update);

    var delta = Date.now() - lastUpdateTime;
    if (acDelta > msPerFrame)
    {
        acDelta = 0;
        redraw();
        frame++;
        if (frame >= 6) frame = 0;
    } else
    {
        acDelta += delta;
    }

    lastUpdateTime = Date.now();
}

If you load this up, our little kitty has calmed down to a more reasonable speed.

Scaling and Rotating

You can also use the 2D canvas to perform a variety of operations on the image as it’s rendered, such as rotation and scaling.

For example, let’s make some kittens by scaling the image down by half. You can do this by adding a ctx.scale(0.5, 0.5) to the draw call:

function redraw()
{
    ctx.fillStyle = '#000000';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    if (imageReady)
    {
        ctx.save();
        ctx.scale(0.5,0.5);
        ctx.drawImage(img, frame*96, 0, 96, 54,
                      canvas.width/2 - 48, canvas.height/2 - 48, 96, 54);
        ctx.restore();
    }
}

Since the scaling is changing, you’ll notice I also added a ctx.save() before the scale call, then a ctx.restore() at the end. Without this, the calls to scale will accumulate and poor kitty will quickly shrink into oblivion (try it, it’s fun).

Scaling also works using negative values in order to reverse an image. If you change the scale values from (0.5, 0.5) to (-1, 1) the cat image will be flipped horizontally, so he’ll run in the opposite direction. Notice that translate is used to flip the starting X position to offset the reversal of the image.

function redraw() {
    ctx.fillStyle = '#000000';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    if (imageReady) {
        ctx.save();
        ctx.translate(img.width, 0);
        ctx.scale(-1, 1);
        ctx.drawImage(img, frame*96, 0, 96, 54,
                      canvas.width/2 - 48, canvas.height/2 - 48, 96, 54);
        ctx.restore();
    }
}

You can use rotate to do (duh) rotation. Here’s kitty climbing the walls:

ctx.rotate( 270*Math.PI/180 );

ctx.drawImage(img, frame*96, 0, 96, 54,
               -(canvas.width/2 - 48), (canvas.height/2 - 48), 96, 54);

In this case, by rotating the context, the coordinates are rotated as well, not just the image, so the drawImage call offset for this by making the inverting the x position of where the kitty will be drawn.

Such a talented kitty (though vampires are supposed to be able to climb walls, right?)

The scaling and rotation is cool. Man, I can do anything! Well, not really. It’s awesome, but it’s also slow and will have a pretty dramatic impact on rendering performance. In a production game there’s another trick to handling this, and a bunch of other rendering performance issues you might encounter: prerendering.

Prerendering

Prerendering is just taking images that you would have rendered during your regular draw cycle and assembling them or manipulating them before hand. You do the expensive rendering operation once, then draw the prerendered result in the regular draw cycle.

In HTML5, you need to draw on a separate invisible canvas, and then instead of drawing an image, you draw the other canvas in its place.

Here’s an example of a function that prerenders the kitty as a reversed image.

var reverseCanvas = null;

function prerender() {
    reverseCanvas = document.createElement('canvas');
    reverseCanvas.width = img.width;
    reverseCanvas.height = img.height;
    var rctx = reverseCanvas.getContext("2d");
    rctx.save();
    rctx.translate(img.width, 0);
    rctx.scale(-1, 1);
    rctx.drawImage(img, 0, 0);
    rctx.restore();
}

Notice a canvas object is created, but not added to the DOM, so it won’t be displayed. The height and width is set to the original spritesheet, and then the original image is drawn using the render buffer’s 2D context.

To setup the prerender you can call it from the loaded function.

function loaded() {
    imageReady = true;
    prerender();
    requestAnimFrame(update);
}

Then when you make the regular redraw call, use the reverseCanvas, instead of the original:

function redraw() {
    ctx.fillStyle = '#000000';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    if (imageReady) {
        ctx.save();
        ctx.drawImage(reverseCanvas, frame*96, 0, 96, 96, 
                      (canvas.width/2 - 48), (canvas.height/2 - 48), 96, 96);
        ctx.restore();
    }
}

Unfortunately, when we reversed the image the animation now plays backwards as well, so you’ll need to reverse the animation sequence as well:

function update() {
    requestAnimFrame(update);
    var delta = Date.now() - lastUpdateTime;
    if (acDelta > msPerFrame) {
        acDelta = 0;
        redraw();
        frame--;
        if (frame < 0) frame = 5;
    } else {
        acDelta += delta;
    }
    lastUpdateTime = Date.now();
}

If you need to, you can convert the canvas into an image by setting its source to use a data url containing the encoded image data. Canvas has a method to do this, so it’s as easy as:

newImage = new Image();

newImage.src = reverseCanvas.toDataURL("image/png");

Another nice image manipulation is to play with the actual pixel data. The HTML5 canvas elements exposes the image data as an array of pixels in RGBA format. You can gain access to the data array form a context using:

var imageData = ctx.getImageData(0, 0, width, height);

Which will return an ImageData structure containing width, height and data members. The data element is the array of pixels we’re after.

The data array is made up of all the pixels, with each pixel being represented by 4 entries, red, green, blue and the alpha level, all ranging from 0 to 255. Thus an image which is 512 wide by 512 high will result in an array that has 1048576 elements in it – 512×512 equals 262,144 pixels, multiplied by 4 entries per pixel.

Using this data array, here’s an example where the specific red component of image is increased, whilst the red and blue components are reduced, thus creating our level 2 monster, the hell-spawn-demon-kitty.

function prerender() {
    reverseCanvas = document.createElement('canvas');
    reverseCanvas.width = img.width;
    reverseCanvas.height = img.height;
    var rctx = reverseCanvas.getContext("2d");
    rctx.save();
    rctx.translate(img.width, 0);
    rctx.scale(-1, 1);
    rctx.drawImage(img, 0, 0);
    // modify the colors
    var imageData = rctx.getImageData(0, 0, reverseCanvas.width, reverseCanvas.height);
    for (var i=0, il = imageData.data.length; i < il; i+=4) {
        if (imageData.data[i] != 0) imageData.data[i] = imageData.data[i] + 100;    // red
        if (imageData.data[i+1] != 0) imageData.data[i+1] = imageData.data[i+1] - 50; // green
        if (imageData.data[i+1] != 0) imageData.data[i+2] = imageData.data[i+2] - 50; // blue
    }
    rctx.putImageData(imageData, 0, 0);
    rctx.restore();
}

The for loop is iterating over the data array in steps of four, each time modifying the three primary colors. The 4th channel, alpha, is left as is, but if you like you can use this to vary the transparency of certain pixels. (Note: in the JSFiddle example below, we use a dataURL for the image data, specifically to avoid cross-domain issues with direct pixel manipulation. You won’t need to do that on your own server.)

Here’s our level 2 boss kitty:

Since manipulating an image using the pixel array requires iterating over all the elements – in the case of hell kitty, that’s over a million times – you should keep things pretty optimized: precalulate as much as possible, don’t create variables/objects and skip pixels as much as possible.

Conclusion

The combination of canvas drawing, scaling, rotating, translating and pixel manipulation, along with the performance option of using prerendering gives a range of powers to make cool, dynamic games.

As an example, I used these techniques in one of Playcraft’s demo games recently, a 2D 4-way scrolling space shooter. The artists produced only a single frame of each ship (player and enemy fighters), which I would then rotate and prerender according to how many degrees, and thus how smooth, we wanted the ships to turn. I could adjust the number of angles based on the type of ship at run time – by default, player ships rendered with 36 turning angles (very smooth), whereas enemy and opponent ships at only 16 angles (choppy). I also added an option to let players on more powerful computers choose to increase the smoothness angles to 72 all round (super smooth). In addition, I dynamically recolor the emblems and markings on the ships (the cool big stripes along the wings) according to the team you’re on. This again saves on rendering and resources, but also allows the ship colors to be dynamically adjusted based on a user selected team color.

For more information on what you can do with canvas check out the Canvas Element API.

This article originally appeared on BuildNewGames.com, a collaboration by the teams at Bocoup and Internet Explorer.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://www.geckoandfly.com Ngan Tengyuen

    more like Simba Kitty

  • http://goldendaymusic.com Mark

    Yeah Simba Kitty :)