Click here to Skip to main content
15,868,141 members
Articles / Web Development / HTML5
Article

HTML5 Gaming: How to Animate Sprites in Canvas with EaselJS

3 Apr 2012CPOL5 min read 41.3K   27   2
I’ve decided to use EaselJS which was used to write PiratesLoveDaisies, a great HTML5 Tower Defense game. We’re going to see in this tutorial how to use your existing sprite elements and animate them.

This article is in the Product Showcase section for our sponsors at CodeProject. These articles are intended to provide you with information on products and services that we consider useful and of value to developers.

When you write casual games using the HTML5 Canvas element, you need a way to handle your sprites. There are several libraries available to help you write games, including ImpactJS and CraftyJS.

Introduction

On the official EaselJS site, you’ll find interesting samples and some basic documentation. We’ll use the sprites sample as a base, along with resources available in the XNA 4.0 Platformer sample.

If you follow my blog, you know I love playing with this sample. I’ve used it in two previous articles:

The platformer sample has been updated by the XNA team and is available for Xbox 360, PC & Windows Phone 7: App Hub – platformer. Download it to play around with it, then extract the sprites to use with EaselJS.

I’m going to use 2 PNG files as source of sprite sequences.

A running monster, which contains 10 different sprites.

HTML5-Gaming/image001.png

Our monster in idle mode, containing 11 different sprites:

HTML5-Gaming/image002.png

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

Tutorial 1: Building the SpriteSheet and the BitmapSequence

We’ll start by running the monster from one end of the canvas to the other.

The first step is to load the complete sequence contained in the PNG file with this code:

C#
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, you can start the game.

EaselJS exposes a SpriteSheet object to handle the sprite. Thus, by using this code:

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

...I’m indicating that I’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, like in the rats sprite sample on the EaselJS site.

After that, you’ll need to use the BitmapSequence object. It helps animate the sequence and position the sprites on screen.

Let’s review the initializing code of this BitmapSequence:

C#
// 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, you need to decide what you’d like to do in the animation loop. This animation loop is called every xxx milliseconds and lets you update the position of your sprites. For that, EaselJS exposes a Ticker object that provides a centralized tick or heartbeat broadcast at a set interval.

All you have to do is subscribe to the tick event and implement a .tick() method that will be called back.

This code is for instance registering the event:

C#
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:

C#
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: easelJSSpritesTutorial01 to view the complete source code.

But wait! There are 2 problems in this animation!

  1. The animation steps are weird, since the character loops through its different sprites too fast
  2. The character only walks normally from right to left—otherwise it looks like he’s doing the Moonwalk!  

Let’s fix it.

Tutorial 2: Controlling the Animation speed and Flipping the Sprites

The simplest way I’ve found to fix the animation’s speed is by using a modulus operator to avoid drawing/updating my sequence during each tick.

There’s currently an open issue about this on EaselJS 0.3.2: https://github.com/gskinner/EaselJS/issues/60

To make the character walk normally from left to right, we just need to flip each frame.

EaselJS exposes a SpriteSheetUtils object for that and a flip() method.

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

You’re essentially making a derivative sequence named "walk_right" based on the "walk_left" sequence that’s flipped horizontally.

Finally, here is the code that slows down the speed animation and handles which sequence to play based on the character position:

C#
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: easelJSSpritesTutorial02

Tutorial 3: Loading Multiple Sprites and Playing with Multiple Animations

It’s time to load the idle state of the monster.

The idea here is to make the monster travel a single round-trip, then play the idle state.

We’ll have to load multiple PNG files from the web server. It’s very important to wait until all resources are loaded, otherwise you might try to draw non-yet downloaded resources.

Here’s a simple way to do it:

C#
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.

When 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 set the position parameters, you just need the same kind of code previously seen:

C#
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 the left side of the screen, and play the idle animation instead.

Here is the code to stop your monster:

C#
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: easelJSSpritesTutorial03.

That’s all folks! If you’re ready for more, check out HTML5 Gaming: building the core objects & handling collisions with EaselJS.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
David Rousset is a Senior Program Manager working for Microsoft Corp, in charge of driving adoption of HTML5 standards. He was a speaker in several famous web conferences such as Paris Web, CodeMotion, ReasonsTo or jQuery UK. He’s the co-author of the WebGL Babylon.js open-source engine. Read his blog on MSDN or follow him @davrous on Twitter.

Comments and Discussions

 
QuestionAnimations with different frame rates. Pin
Member 971222826-Dec-12 18:31
Member 971222826-Dec-12 18:31 
QuestionThe next 2 tutorials are here Pin
davrous6-Nov-11 0:28
davrous6-Nov-11 0:28 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.