Partager via


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.

PlatformerTutorial2 

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. Clignement d'œil

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?). Clignement d'œil

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! Sourire

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:

  1. BitmapSequence is not available anymore in 0.4 and has been replaced by BitmapAnimation
  2. You can now slow down the animation loop of the sprites natively while building the SpriteSheet object
  3. 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).
  4. 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.
  5. 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 understand

  • Anonymous
    April 12, 2012
    great tutorial!

  • Anonymous
    May 22, 2012
    The comment has been removed

  • Anonymous
    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, David

  • Anonymous
    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, David

  • Anonymous
    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.0

  • Anonymous
    July 24, 2013
    The comment has been removed

  • Anonymous
    August 02, 2013
    marvelouse

  • Anonymous
    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