HTML5 Zone is brought to you in partnership with:

Relations techniques avec les développeurs chez DPE David has posted 11 posts at DZone. You can read more from them at their website. View Full User Profile

HTML5 Gaming: animating sprites in Canvas with EaselJS

10.17.2011
| 8313 views |
  • submit to reddit

When you want to write casual games using the HTML5 Canvas element, you’ll need to find a way to handle your sprites. There are several libraries available to help you writing games such as ImpactJS, CraftyJS and so on. On my side, I’ve decided to use EaselJS which has been used to write PiratesLoveDaisies: an HTML5 Tower Defense game. We’re going to see in this tutorial how to use your existing sprite elements and animate them.

Pour ceux qui pratiquent la langue de Molière, vous trouverez une version française ici : Jeux HTML5: animation de sprites dans l’élément Canvas grâce à EaselJS

This article is the first of a serie of 3:

- HTML5 Gaming: animating sprites in Canvas with EaselJS
- HTML5 Gaming: building the core objects & handling collisions with EaselJS
- HTML5 Platformer: the complete port of the XNA game to <canvas> with EaselJS

Introduction

On the official EaselJS site, you’ll find some interesting samples and some basic documentation. We will use the sprites sample as a base. We will use also the resources available in the XNA 4.0 Platformer sample. For those of you who are following my blog, you may remember that I love playing with this sample. Here are the previous attached articles:

- Windows Phone 7 Platformer Starter Kit for XNA Studio 4.0
- Silverlight 4 XNA Platformer Level Editor for Windows Phone 7

The platformer sample have been updated in the meantime by our XNA team and is available here for Xbox 360, PC & Windows Phone 7: App Hub – platformer . You can download it to play with it and extract the sprites to use them with EaselJS.

In this article, we’re going to use these 2 PNG files as source of our sprite sequences:

Our monster running:

which contains 10 different sprites.

Our monster in idle mode:

which contains 11 different sprites.

Note: these samples don’t work properly in Firefox 5.0 due apparently to a bug in their canvas implementation. It has been tested ok in IE9/IE10/Chrome 12/Opera 11 & Firefox Aurora 7.0.  

Tutorial 1: building the SpriteSheet and the BitmapSequence

We’ll start by moving the running monster between the beginning and the end of the width of our canvas.

First step is to load the complete sequence contained in the PNG file via this code:

var imgMonsterARun = new Image();

function init() {
    //find canvas and load images, wait for last image to load
    canvas = document.getElementById("testCanvas");

    imgMonsterARun.onload = handleImageLoad;
    imgMonsterARun.onerror = handleImageError;
    imgMonsterARun.src = "img/MonsterARun.png";
}

This code will be called first to initialize our game’s content. Once loaded, we can start the game. EaselJS expose a SpriteSheet object to handle the sprite. Thus, by using this code:

var spriteSheet  = new SpriteSheet(
        imgMonsterARun, //image to use
        64, //width of each sprite
        64, //height of each sprite
        {    
            walk_left: [0, 9]
        });

We’re indicating that we’d like to create a new sequence named “walk_left” that will be made of the imgMonsterARun image. This image will be split into 10 frames with a size of 64x64 pixels. This is the core object to load our sprite and create our sequences. There could be several sequences created from the same PNG file if you want to, like in the rats sprite sample of the EaselJS site.

After that, we need to use the BitmapSequence object. It helps us animating our sequence and positioning our sprites on the screen. Let’s review the initializing code of this BitmapSequence:

// create a BitmapSequence instance to display and play back the sprite sheet:
bmpSeq = new BitmapSequence(spriteSheet);
    
// set the registration point (the point it will be positioned and rotated around)
// to the center of the frame dimensions:
bmpSeq.regX = bmpSeq.spriteSheet.frameWidth/2|0;
bmpSeq.regY = bmpSeq.spriteSheet.frameHeight / 2 | 0;

// start playing the first sequence:
bmpSeq.gotoAndPlay("walk_left");     //animate
    
// set up a shadow. Note that shadows are ridiculously expensive. You could display hundreds
// of animated rats if you disabled the shadow.
bmpSeq.shadow = new Shadow("#454", 0, 5, 4);

bmpSeq.name = "monster1";
bmpSeq.speed = 1;
bmpSeq.direction = 90;
bmpSeq.vX = 3|0.5;
bmpSeq.vY = 0;
bmpSeq.x = 16;
bmpSeq.y = 32;
        
// have each monster start at a specific frame
bmpSeq.currentFrame = 0;
stage.addChild(bmpSeq);

The constructor of the BitmapSequence object simply needs the SpriteSheet element as a parameter. We’re then giving a name to the sequence, setting some parameters like the speed and the initial position of our first frame. Finally, we add this sequence to the display list by using the Stage object and its addChild() method.

Next step is now to decide what we’d like to do in our animation loop. This animation loop is called every xxx milliseconds and let you update the position of your sprites. For that, EaselJS exposes a Ticker object which provides a centralized tick or heartbeat broadcast at a set interval. All you’ve got to do is to subscribe to the tick event and implement a .tick() method that will be called back. This code is for instance registering the event:

Ticker.addListener(window);
// Best Framerate targeted (60 FPS)
Ticker.setInterval(17);

And here is the code that will be called every 17ms (when possible) to update the position of our monster:

function tick() {
    // Hit testing the screen width, otherwise our sprite would disappear
    if (bmpSeq.x >= screen_width - 16) {
        // We've reached the right side of our screen
        // We need to walk left now to go back to our initial position
        bmpSeq.direction = -90;
    }

    if (bmpSeq.x < 16) {
        // We've reached the left side of our screen
        // We need to walk right now
        bmpSeq.direction = 90;
    }

    // Moving the sprite based on the direction & the speed
    if (bmpSeq.direction == 90) {
        bmpSeq.x += bmpSeq.vX;
        bmpSeq.y += bmpSeq.vY;
    }
    else {
        bmpSeq.x -= bmpSeq.vX;
        bmpSeq.y -= bmpSeq.vY;
    }

    // update the stage:
    stage.update();
}

You can test the final result here:

You can also browse this sample here: easelJSSpritesTutorial01 if you’d like to view the complete source code.

But wait! There are 2 problems in this animation:

1 – the animation steps are weird as the character seems to loop through its different sprites sequence too fast
2 – the character can only walk normally from right to left otherwise it looks like it tries to achieve a kind of Moonwalk dance. Clignement d'œil

Let’s see how to fix that in the second tutorial.

Tutorial 2: controlling the animation speed and flipping the sprites

To fix the animation’s speed, the simplest way I’ve found is to use a modulus operator to avoid drawing/updating my sequence during each tick. There is currently an opened issue on this topic: https://github.com/gskinner/EaselJS/issues/60 with EaselJS 0.3.2.

To add new animation frames to let the character walking normally from left to right, we need to flip each frame. EaselJS exposes a SpriteSheetUtils object for that and a flip() method. Here is the code that uses them:

// {nameOfFlippedSequence:["derivativeSequence", flipHorizontally, flipVertically, optionNameOfNextSequence]}
spriteSheet = SpriteSheetUtils.flip(
spriteSheet,
{
    walk_right: ["walk_left", true, false, null]
});

We’re creating a derivative sequence named “walk_right based on the “walk_left” sequence that we flip horizontally. At last, here is the code that slows down the speed animation and which handles which sequence to play based on the character position:

function tick() {
    // To slow down the animation loop of the sprite, we're not redrawing during each tick
    // With a Modulo 4, we're dividing the speed by 4
    var speedControl = Ticker.getTicks() % 4;

    if (speedControl == 0) {
        // Hit testing the screen width, otherwise our sprite would disappear
        if (bmpSeq.x >= screen_width - 16) {
            // We've reached the right side of our screen
            // We need to walk left now to go back to our initial position
            bmpSeq.direction = -90;
            bmpSeq.gotoAndPlay("walk_left")
        }

        if (bmpSeq.x < 16) {
            // We've reached the left side of our screen
            // We need to walk right now
            bmpSeq.direction = 90;
            bmpSeq.gotoAndPlay("walk_right");
        }

        // Moving the sprite based on the direction & the speed
        if (bmpSeq.direction == 90) {
            bmpSeq.x += bmpSeq.vX;
            bmpSeq.y += bmpSeq.vY;
        }
        else {
            bmpSeq.x -= bmpSeq.vX;
            bmpSeq.y -= bmpSeq.vY;
        }

        // update the stage:
        stage.update();
    }
}

You can test the final result here:

You can also browse this sample here: easelJSSpritesTutorial02 if you’d like to view the complete source code.

Tutorial 3: loading multiple sprites and playing with multiple animations

It’s time now to load the idle state of the monster. The idea here is to play the animation state to achieve a single round-trip. Once achieved, we will play the idle state.

We will then now load multiple PNG files from the web server. It’s then very important to wait until all resources are loaded otherwise you may try to draw non-yet downloaded resources. Here is a very simple way to do it:

var numberOfImagesLoaded = 0;

var imgMonsterARun = new Image();
var imgMonsterAIdle = new Image();

function init() {
    //find canvas and load images, wait for last image to load
    canvas = document.getElementById("testCanvas");

    imgMonsterARun.onload = handleImageLoad;
    imgMonsterARun.onerror = handleImageError;
    imgMonsterARun.src = "img/MonsterARun.png";

    imgMonsterAIdle.onload = handleImageLoad;
    imgMonsterAIdle.onerror = handleImageError;
    imgMonsterAIdle.src = "img/MonsterAIdle.png";
}

function handleImageLoad(e) {
    numberOfImagesLoaded++;

    // We're not starting the game until all images are loaded
    // Otherwise, you may start to draw without the resource and raise 
    // this DOM Exception: INVALID_STATE_ERR (11) on the drawImage method
    if (numberOfImagesLoaded == 2) {
        numberOfImagesLoaded = 0;
        startGame();
    }
}

This code is very simple. For instance, it doesn’t handle the errors properly by trying to re-download the image in case of a first failure. If you’re building a game, you will need to write your own content download manager if the JS library you’re using doesn’t implement it.

To add the idle sequence and setting the position parameters, we just need to use the same kind of code previously seen:

var spriteSheetIdle = new SpriteSheet(
        imgMonsterAIdle, //image to use
        64, //width of each sprite
        64, //height of each sprite
        {    
            idle: [0, 10]
        });

bmpSeqIdle = new BitmapSequence(spriteSheetIdle);

bmpSeqIdle.name = "monsteridle1";
bmpSeqIdle.speed = 1;
bmpSeqIdle.direction = 0;
bmpSeqIdle.x = 16;
bmpSeqIdle.y = 32;

Now, in the tick() method, we need to stop the walking animation once we’ve reached back the left side of the screen and to play the idle animation instead. Here is the code which does that:

if (bmpSeq.x < 16) {
    // We've reached the left side of our screen
    // We need to walk right now
    bmpSeq.direction = 90;
    bmpSeq.gotoAndStop("walk_left");
    stage.removeChild(bmpSeq);
    bmpSeqIdle.gotoAndPlay("idle");
    stage.addChild(bmpSeqIdle);
}

You can test the final result here:

You can also browse this sample here: easelJSSpritesTutorial03 if you’d like to view the complete source code.

That’s all folks! But if you want to go further, you can read the next part here: HTML5 Gaming: building the core objects & handling collisions with EaselJS

David

References
Published at DZone with permission of its author, David Rousset. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Nabeel Manara replied on Fri, 2012/01/27 - 10:29am

Hello, really thanks for this useful information. I'm trying to follow your instructions in order to leater build a casual rolgame. SOme things seems to doesn't work (I think something Im doing wrong)

var imgJugador = new Image();

imgJugador.onload = handleImageLoad;

imgJugador.onerror = handleImageError;

If I try to execute this 2 lines doesn't work and if I try to use the SpriteSheetUtils.flip method I can not execute it.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.