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.
Pour ceux qui pratiquent la langue de Molière, vous trouverez une version française ici : Jeux HTML5: construction des objets principaux & gestion des collisions avec EaselJS
This article is the 2nd 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
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 ourMonster.js file defining our enemies object:
(function (window) { function Monster(monsterName, imgMonster, x_end) { this.initialize(monsterName, imgMonster, x_end); } Monster.prototype = new 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 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] } }); 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 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.
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 futur 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 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 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] } }); 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 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.
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.
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.
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 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 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); Ticker.addListener(window); // Best Framerate targeted (60 FPS) Ticker.useRAF = true; 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.
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.4. For those of you who read the version 0.3.2, here are the main changes for this tutorial to be aware of: