HTML5 Gaming: building the core objects & handling collisions with EaselJS
We’ve seen in the previous article how to animate our sprites using EaselJS: HTML5 Gaming: animating sprites in Canvas with EaselJS
We’re now going to see how to create some of our game objects like ennemies and our platformer hero. We will also see how to implement a simple collision mechanism between them. This time, these tutorials will be mainly based on this sample: EaselJS Game sample
You’ll find a live working sample at the end of this article. It’s the base of a simple game.
This article is the 2nd of a series 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
Building the Monster object
A monster object has 2 states:
1 - Running along all the width of the screen
2 – Being idle once one of the side is reached during a certain amount of time before running again
It’s very stupid. But if you touch it, you’re dead. This time I’ve merged the sprites coming from the XNA Platformer sample defining the running & the idle sequence inside a unique PNG file. For instance, here is the PNG file for MonsterC:
Our Monster object is defined inside Monster.js and takes the BitmapAnimation object as its prototype which has to be used indeed for such scenarios. It contains everything we need: a tick() method, some hit testing mechanism for our collisions and a way to handle our sprites into several animations.
We just need to add some specific logic of our monster like the timing part to handle the idle state and we’re done. Here is the code of our Monster.js file defining our enemies object:
(function (window) {
function Monster(monsterName, imgMonster, x_end) {
this.initialize(monsterName, imgMonster, x_end);
}
Monster.prototype = new createjs.BitmapAnimation();
// public properties:
Monster.prototype.IDLEWAITTIME = 40;
Monster.prototype.bounds = 0; //visual radial size
Monster.prototype.hit = 0; //average radial disparity
// constructor:
Monster.prototype.BitmapAnimation_initialize = Monster.prototype.initialize; //unique to avoid overiding base class
// variable members to handle the idle state
// and the time to wait before walking again
this.isInIdleMode = false;
this.idleWaitTicker = 0;
var quaterFrameSize;
Monster.prototype.initialize = function (monsterName, imgMonster, x_end) {
var localSpriteSheet = new createjs.SpriteSheet({
images: [imgMonster], //image to use
frames: {width: 64, height: 64, regX: 32, regY: 32},
animations: {
walk: [0, 9, "walk", 4],
idle: [10, 20, "idle", 4]
}
});
createjs.SpriteSheetUtils.addFlippedFrames(localSpriteSheet, true, false, false);
this.BitmapAnimation_initialize(localSpriteSheet);
this.x_end = x_end;
quaterFrameSize = this.spriteSheet.getFrame(0).rect.width / 4;
// start playing the first sequence:
this.gotoAndPlay("walk_h"); //animate
// set up a shadow. Note that shadows are ridiculously expensive. You could display hundreds
// of animated monster if you disabled the shadow.
this.shadow = new createjs.Shadow("#000", 3, 2, 2);
this.name = monsterName;
// 1 = right & -1 = left
this.direction = 1;
// velocity
this.vX = 1;
this.vY = 0;
// starting directly at the first frame of the walk_h sequence
this.currentFrame = 21;
}
Monster.prototype.tick = function () {
if (!this.isInIdleMode) {
// Moving the sprite based on the direction & the speed
this.x += this.vX * this.direction;
this.y += this.vY * this.direction;
// Hit testing the screen width, otherwise our sprite would disappear
if (this.x >= this.x_end - (quaterFrameSize + 1) || this.x < (quaterFrameSize + 1)) {
this.gotoAndPlay("idle");
this.idleWaitTicker = this.IDLEWAITTIME;
this.isInIdleMode = true;
}
}
else {
this.idleWaitTicker--;
if (this.idleWaitTicker == 0) {
this.isInIdleMode = false;
// Hit testing the screen width, otherwise our sprite would disappear
if (this.x >= this.x_end - (quaterFrameSize + 1)) {
// We've reached the right side of our screen
// We need to walk left now to go back to our initial position
this.direction = -1;
this.gotoAndPlay("walk");
}
if (this.x < (quaterFrameSize + 1)) {
// We've reached the left side of our screen
// We need to walk right now
this.direction = 1;
this.gotoAndPlay("walk_h");
}
}
}
}
Monster.prototype.hitPoint = function (tX, tY) {
return this.hitRadius(tX, tY, 0);
}
Monster.prototype.hitRadius = function (tX, tY, tHit) {
//early returns speed it up
if (tX - tHit > this.x + this.hit) { return; }
if (tX + tHit < this.x - this.hit) { return; }
if (tY - tHit > this.y + this.hit) { return; }
if (tY + tHit < this.y - this.hit) { return; }
//now do the circle distance test
return this.hit + tHit > Math.sqrt(Math.pow(Math.abs(this.x - tX), 2) + Math.pow(Math.abs(this.y - tY), 2));
}
window.Monster = Monster;
} (window));
The collision part is handled via the hitPoint() and hitRadius() functions. The hit testing is done via circle which is a bit less accurate than a boxing mode.
Building the Player object
The logic of the player object is a bit different than the monsters. The x & y position are normally controlled by the user moving the character with the keyboard. Our hero has more animations than the monsters as he can die, jump, move, celebrate and be in the idle mode.
Here is the PNG associated to him:
In this tutorial, we’ll keep it simple. We will only handle the walk, idle & die sequence. Still, let’s load all the animations for a future potential usage. Here is the code available in the Player.js file. Reading the code and its comments should provide enough details:
(function (window) {
function Player(imgPlayer, x_start, x_end) {
this.initialize(imgPlayer, x_start, x_end);
}
Player.prototype = new createjs.BitmapAnimation();
// public properties:
Player.prototype.bounds = 0;
Player.prototype.hit = 0;
Player.prototype.alive = true;
// constructor:
Player.prototype.BitmapAnimation_initialize = Player.prototype.initialize; //unique to avoid overiding base class
var quaterFrameSize;
Player.prototype.initialize = function (imgPlayer, x_end) {
var localSpriteSheet = new createjs.SpriteSheet({
images: [imgPlayer], //image to use
frames: { width:64, height:64, regX:32, regY: 32 },
animations: {
walk: [0, 9, "walk", 4],
die: [10, 21, false, 4],
jump: [22, 32],
celebrate: [33, 43],
idle: [44, 44]
}
});
createjs.SpriteSheetUtils.addFlippedFrames(localSpriteSheet, true, false, false);
this.BitmapAnimation_initialize(localSpriteSheet);
this.x_end = x_end;
quaterFrameSize = this.spriteSheet.getFrame(0).rect.width / 4;
// start playing the first sequence:
this.gotoAndPlay("idle"); //animate
this.isInIdleMode = true;
// set up a shadow. Note that shadows are ridiculously expensive. You could display hundreds
// of animated monster if you disabled the shadow.
this.shadow = new createjs.Shadow("#000", 3, 2, 2);
this.name = "Hero";
// 1 = right & -1 = left
this.direction = 1;
// velocity
this.vX = 1;
this.vY = 0;
// starting directly at the first frame of the walk_h sequence
this.currentFrame = 66;
//Size of the Bounds for the collision's tests
this.bounds = 28;
this.hit = this.bounds;
}
Player.prototype.tick = function () {
if (this.alive && !this.isInIdleMode) {
// Hit testing the screen width, otherwise our sprite would disappear
// The player is blocked at each side but we keep the walk_right or walk_animation running
if ((this.x + this.direction > quaterFrameSize) && (this.x + (this.direction * 2) < this.x_end - quaterFrameSize + 1)) {
// Moving the sprite based on the direction & the speed
this.x += this.vX * this.direction;
this.y += this.vY * this.direction;
}
}
}
window.Player = Player;
} (window));
The player will be remotely controlled into the main page.
Building the Content Manager object
Usually, the first step of a HTML5 game is to download all the needed resources before starting the game. In my case, you’ll find a very basic ContentManager available in the ContentManager.js file.
Note: the CreateJS suite now expose a library named PreloadJS that is far more advanced that the basic method exposed below.
Here is the code:
// Used to download all needed resources from our
// webserver
function ContentManager() {
// Method called back once all elements
// have been downloaded
var ondownloadcompleted;
// Number of elements to download
var NUM_ELEMENTS_TO_DOWNLOAD = 15;
// setting the callback method
this.SetDownloadCompleted = function (callbackMethod) {
ondownloadcompleted = callbackMethod;
};
// We have 4 type of enemies, 1 hero & 1 type of tile
this.imgMonsterA = new Image();
this.imgMonsterB = new Image();
this.imgMonsterC = new Image();
this.imgMonsterD = new Image();
this.imgTile = new Image();
this.imgPlayer = new Image();
// the background can be created with 3 different layers
// those 3 layers exist in 3 versions
this.imgBackgroundLayers = new Array();
var numImagesLoaded = 0;
// public method to launch the download process
this.StartDownload = function () {
SetDownloadParameters(this.imgPlayer, "img/Player.png", handleImageLoad, handleImageError);
SetDownloadParameters(this.imgMonsterA, "img/MonsterA.png", handleImageLoad, handleImageError);
SetDownloadParameters(this.imgMonsterB, "img/MonsterB.png", handleImageLoad, handleImageError);
SetDownloadParameters(this.imgMonsterC, "img/MonsterC.png", handleImageLoad, handleImageError);
SetDownloadParameters(this.imgMonsterD, "img/MonsterD.png", handleImageLoad, handleImageError);
SetDownloadParameters(this.imgTile, "img/Tiles/BlockA0.png", handleImageLoad, handleImageError);
// download the 3 layers * 3 versions
for (var i = 0; i < 3; i++) {
this.imgBackgroundLayers[i] = new Array();
for (var j = 0; j < 3; j++) {
this.imgBackgroundLayers[i][j] = new Image();
SetDownloadParameters(this.imgBackgroundLayers[i][j], "img/Backgrounds/Layer" + i
+ "_" + j + ".png", handleImageLoad, handleImageError);
}
}
}
function SetDownloadParameters(imgElement, url, loadedHandler, errorHandler) {
imgElement.src = url;
imgElement.onload = loadedHandler;
imgElement.onerror = errorHandler;
}
// our global handler
function handleImageLoad(e) {
numImagesLoaded++
// If all elements have been downloaded
if (numImagesLoaded == NUM_ELEMENTS_TO_DOWNLOAD) {
numImagesLoaded = 0;
// we're calling back the method set by SetDownloadCompleted
ondownloadcompleted();
}
}
//called if there is an error loading the image (usually due to a 404)
function handleImageError(e) {
console.log("Error Loading Image : " + e.target.src);
}
}
It lacks several things to be a good content manager: a download progress indicator, a better error handler, localStorage usage, a more generic code, etc. But I’ve tried to build a basic & easy to understand game.
Setting up all the pieces inside the main page
Now that we have the core parts of our game, we can start to use them to build a very basic platformer game. Let’s review each part of our main page hosting our game. In the init() method, we’re creating the stage and we’re using the ContentManager object to download our PNG files:
function init() {
//find canvas and load images, wait for last image to load
canvas = document.getElementById("testCanvas");
// create a new stage and point it at our canvas:
stage = new createjs.Stage(canvas);
// grab canvas width and height for later calculations:
screen_width = canvas.width;
screen_height = canvas.height;
contentManager = new ContentManager();
contentManager.SetDownloadCompleted(startGame);
contentManager.StartDownload();
}
Once done, the startGame() function is called. It first uses the CreateAndAddRandomBackground() function which create a random background based on 3 different layers. Then, it creates our Hero and set its Y position in a random place. Just under the hero, we’re building a very basic platform where our hero will be able to walk on to. Finally, we’re building 4 Monster() objects inside the Monsters array and we add them also to the stage.
function startGame() {
// Random number to set the Y position
// of our Hero & Enemies
var randomY;
CreateAndAddRandomBackground();
// Our hero can be moved with the arrow keys (left, right)
document.onkeydown = handleKeyDown;
document.onkeyup = handleKeyUp;
// Creating the Hero
randomY = 32 + (Math.floor(Math.random() * 7) * 64);
Hero = new Player(contentManager.imgPlayer, screen_width);
Hero.x = 400;
Hero.y = randomY;
//Tile where the hero & the ennemies will be able to walk on to
bmpSeqTile = new createjs.Bitmap(contentManager.imgTile);
bmpSeqTile.regX = bmpSeqTile.frameWidth / 2 | 0;
bmpSeqTile.regY = bmpSeqTile.frameHeight / 2 | 0;
// Taking the same tile all over the width of the game
for (var i = 0; i < 20; i++) {
// clone the original tile, so we don't need to set shared properties:
var bmpSeqTileCloned = bmpSeqTile.clone();
// set display properties:
bmpSeqTileCloned.x = 0 + (i * 40);
bmpSeqTileCloned.y = randomY + 32;
// add to the display list:
stage.addChild(bmpSeqTileCloned);
}
// Our Monsters collection
Monsters = new Array();
// Creating the first type of monster
randomY = 32 + (Math.floor(Math.random() * 7) * 64);
Monsters[0] = new Monster("MonsterA", contentManager.imgMonsterA, screen_width);
Monsters[0].x = 20;
Monsters[0].y = randomY;
// Creating the second type of monster
randomY = 32 + (Math.floor(Math.random() * 7) * 64);
Monsters[1] = new Monster("MonsterB", contentManager.imgMonsterB, screen_width);
Monsters[1].x = 750;
Monsters[1].y = randomY;
// Creating the third type of monster
randomY = 32 + (Math.floor(Math.random() * 7) * 64);
Monsters[2] = new Monster("MonsterC", contentManager.imgMonsterC, screen_width);
Monsters[2].x = 100;
Monsters[2].y = randomY;
// Creating the forth type of monster
randomY = 32 + (Math.floor(Math.random() * 7) * 64);
Monsters[3] = new Monster("MonsterD", contentManager.imgMonsterD, screen_width);
Monsters[3].x = 650;
Monsters[3].y = randomY;
// Adding all the monsters to the stage
for (var i=0; i<Monsters.length;i++){
stage.addChild(Monsters[i]);
}
stage.addChild(Hero);
// we want to do some work before we update the canvas,
// otherwise we could use Ticker.addListener(stage);
createjs.Ticker.addListener(window);
// Best Framerate targeted (60 FPS)
createjs.Ticker.useRAF = true;
createjs.Ticker.setFPS(60);
}
And the end, there are 2 obvious keyboard handler that simply play the walk_left or walk_right animation of our hero based on the arrows keys. And finally, the core logic of our game is contained in a few line of code inside the tick() method of course:
function tick() {
// looping inside the Monsters collection
for (monster in Monsters) {
var m = Monsters[monster];
// Calling explicitly each tick method
// to launch the update logic of each monster
m.tick();
// If the Hero is still alive and if he's too near
// from one of the monster...
if (Hero.alive && m.hitRadius(Hero.x, Hero.y, Hero.hit)) {
//...he must die unfortunately!
Hero.alive = false;
// Playing the proper animation based on
// the current direction of our hero
if (Hero.direction == 1) {
Hero.gotoAndPlay("die_h");
}
else {
Hero.gotoAndPlay("die");
}
continue;
}
}
// Update logic of the hero
Hero.tick();
// update the stage:
stage.update();
}
We’re just checking during each tick if one of the monsters is not currently hitting our hero based on their collision parameters. If one monster is too near of our hero, our poor hero must die.
Play with the live sample
You can now play with the live sample just below. Every time you’ll press the start button a new background will be generated and each character (enemies & hero) will be placed at a different position. You can also move right or left using the keyboard. By the way, don’t panic. As you can’t jump, there is currently no way to win in this game. This is a 100% looser game (first of genre?).
Note: as there is no progress bar, you need to wait a bit before playing after pressing the “Start” button.
You can play it also via this link: easelJSCoreObjectsAndCollision
Next part will be to handle the jump sequence using a simple physics engine, loading the music & sound effects and finally loading the levels. But the core is here if you’d like to create your own simple game, you now have all the cards in your hand!
But if you’d like to review the full game with all its source code, jump to the next article : HTML5 Platformer: the complete port of the XNA game to <canvas> with EaselJS
David
Note : this tutorial has originally been written for EaselJS 0.3.2 in July 2010 and has been updated for EaselJS 0.6 on 04/03/2013. For those of you who read the version 0.3.2, here are the main changes for this tutorial to be aware of:
- BitmapSequence is not available anymore in 0.4 and has been replaced by BitmapAnimation
- You can now slow down the animation loop of the sprites natively while building the SpriteSheet object
- EaselJS 0.4 can now use requestAnimationFrame for more efficient animations on supported browsers (like IE10+, Firefox 4.0+ & Chrome via the appropriate vendors’ prefixes).
- You have to explicitly call the tick() method of each object in a global handler rather than having a global Ticker automatically calling your tick implementation.
- Since EaselJS 0.4.2 and in 0.5, you need to add the createjs namespace before every EaselJS objects by default. Otherwise, simply affect the createjs namespace to the window object.
Comments
Anonymous
August 19, 2011
Brilliant thanks! Can't wait for the next installment.Anonymous
October 10, 2011
Cool, very easy to understandAnonymous
April 12, 2012
great tutorial!Anonymous
May 22, 2012
The comment has been removedAnonymous
May 22, 2012
Hi pau1, It is just to save the original constructor of BitmapAnimation object to be able to call it later on. Otherwise, it would be overriden. Bye, DavidAnonymous
May 22, 2012
Another great job.Anonymous
August 23, 2012
For some reason the demo does not work on my iPad with iOS 5.1.1 (though some other EaselJS demos around the web does work). Any ideas?Anonymous
August 30, 2012
Hi Lorenzo, I've updated the turorials & samples to EaselJS 0.5. They should now work again on iPads. Bye, DavidAnonymous
September 25, 2012
I don't play it. Do you give me code?Anonymous
July 21, 2013
Not sure why, but the demo is not working for me. I am trying from my ipad 2.0Anonymous
July 24, 2013
The comment has been removedAnonymous
August 02, 2013
marvelouseAnonymous
July 08, 2015
For those of you still checking out this tutorial but trying to use EaselJs v.0.8.1: github.com/.../HTML5-Game-Tutorial-by-David-Rousset-P2-Easeljs-v.0.8.1.git