By David Rousset
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.
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.
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:
– Windows Phone 7 Platformer Starter Kit for XNA Studio 4.0
– Silverlight 4 XNA Platformer Level Editor for Windows Phone 7
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.
Our monster in idle mode, containing 11 different sprites:
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:
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:
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 64×64 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:
// 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:
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: 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.
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.
// {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:
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:
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:
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:
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.
About the Author
David Rousset is a Developer Evangelist at Microsoft, specializing in HTML5 and web development. Follow him @Silversurfeur on Twitter.
This article was reprinted with permission from Microsoft Corporation. This site does business with Microsoft Corporation.