Master Chief, CreateJS & TypeScript

TypeScript

TypeScript is a superset of JavaScript that adds optional static typing and class-based object oriented programming to the language. It compiles to JavaScript with the resultant JavaScript output closely matching the TypeScript input. In this regard, "Every JavaScript program is also a TypeScript program." If you aren't yet conversant with TypeScript check out the following resources to quickly get up to speed,

CreateJS

CreateJS is a suite of JavaScript libraries and tools that make it easy to build rich and interactive HTML5 applications. The libraries are designed to work independently or they can be mixed and matched to suit one's needs.
The CreateJS suite is composed of four main libraries,
  • EaselJS: provides a full, hierarchical display list, a core interaction model, and helper classes that make it easier to work with the HTML5 Canvas element,
  • TweenJS: provides support for tweening of numerical object properties and CSS style properties. It was developed to support EaselJS, but is not dependent on it,
  • SoundJS: provides consistent cross-browser audio support in HTML5. It enables developers to query for capabilities, then specify and prioritize what APIs, plugins, and features to leverage for specific devices and browsers,
  • PreloadJS: makes it easy to preload assets like images and sounds.
CreateJS is free and open-source and is officially sponsored by Adobe, Microsoft, AOL and Mozilla. In my project I will only be making use of three CreateJS libraries: EaselJS, SoundJS, and PreloadJS.

Getting Started

Sprite Sheets

As I mentioned in the introduction of this article, the main character in my simple game is Master Chief, from the popular first-person shooter; Halo. For my project a copy of the Master Chief sprite sheet from Halo Zero suffices as a suitable asset.
While the sprite sheet in its original state is okay, it contains more sprites than I need for my simple game. Another thing, and the most significant issue, is that the sprites are non-uniform ie. their height and width vary. For EaselJS to appropriately make use of such a sprite sheet I will need to provide it with data containing the x and y offset of each sprite; their width, height and image index. To generate an appropriate sprite sheet, and associated data, I used darkFunction Editor; a free and open source 2D sprite editor that enables fast definition of spritesheets.
After opening the sprite sheet in darkFunction I selected the sprites I needed, a simple affair done by double clicking on an image. I took great care to adjust the height of my selections so that each selection has a similar height. (This helps to prevent an issue where EaselJS shifts upwards any sprite whose height is near to or less than half the height of the tallest sprite). I then used a feature in darkFunction, that enables optimal packing of sprites, to create a more compact sprite sheet from my selections.
After saving the new sprite sheet the editor allows you to save the sprite sheet data. The data is contained in a .sprites file, which is actually just an XML file. The following is the data generated for my new sprite sheet,
<?xml version="1.0"?>
<!-- Generated by darkFunction Editor (www.darkfunction.com) -->
<img name="MasterChiefSpriteSheet.png" w="475" h="369">
  <definitions>
    <dir name="/">
      <spr name="stand" x="0" y="123" w="80" h="123"/>
      <spr name="fire" x="0" y="0" w="106" h="123"/>
      <spr name="run1" x="0" y="246" w="73" h="123"/>
      <spr name="run2" x="409" y="0" w="66" h="123"/>
      <spr name="run3" x="106" y="0" w="71" h="123"/>
      <spr name="run4" x="177" y="0" w="80" h="123"/>
      <spr name="run5" x="257" y="0" w="82" h="123"/>
      <spr name="run6" x="339" y="0" w="70" h="123"/>
      <spr name="run7" x="106" y="246" w="66" h="123"/>
      <spr name="run8" x="106" y="123" w="71" h="123"/>
      <spr name="run9" x="177" y="123" w="80" h="123"/>
      <spr name="run10" x="257" y="123" w="81" h="123"/>
      <spr name="jump1" x="409" y="123" w="66" h="123"/>
      <spr name="jump2" x="338" y="123" w="71" h="123"/>
      <spr name="crouch1" x="177" y="246" w="68" h="123"/>
      <spr name="crouch2" x="245" y="246" w="74" h="123"/>
      <spr name="crouch3" x="319" y="246" w="67" h="123"/>
      <spr name="crouch4" x="386" y="246" w="66" h="123"/>
    </dir>
  </definitions>
</img>
Notice that the h attribute of every <spr> element is the same. For the Asuka sprite sheet, and its corresponding data file, I also used a similar process. The .sprites files are quite invaluable for this project but their .sprites extension is not really helpful, and will make it impossible to parse the files. I therefore changed the extensions to .xml.

Sounds

The project would be a bit bland without some audio effects, which will be made use of with the help of SoundJS. The gunshot and explosion sounds are from SoundBible, which offers royalty free sound effects. The background music is from the YouTube Audio Library which contains a collection of free music tracks that can be filtered based on various criteria.

Type Definitions

The MasterChief project uses local copies of the necessary CreateJS libraries which I downloaded from the CreateJS GitHub repository. The libraries are in a folder named js.
Remember that "Every JavaScript program is also a TypeScript program" and while this is the case the CreateJS libraries are unusable with TypeScript without the TypeScript type definitions of the libraries. Type definitions enable the TypeScript compiler to be aware of the public api of an existing JavaScript library. Fortunately you can get the type definitions for CreateJS libraries via NuGet or on the GitHub repository of DefinitelyTyped.
Using Visual Studio's NuGet Package Manager I searched for and installed the EaselJS, PreloadJS, and SoundJS type definitions.
Installing the type definitions for EaselJS also installs the CreateJS and TweenJS type definitions. The definitions are placed in a folder named Scripts and have a .d.ts extension.
NB: The type definitions for PreloadJS and TweenJS both contain ambient declarations for a class namedSamplePlugin. This situation will generate a compile time error so I commented the ambient declaration in the TweenJS type definition.

MasterChief

The HTML markup for index.html is a simple affair,
<!DOCTYPE html>

<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>MasterChief</title>
    <link rel="stylesheet" href="app.css" type="text/css" />
    <!-- CreateJS libs -->
    <script src="js/preloadjs-0.4.1.min.js"></script>
    <script src="js/easeljs-0.7.1.min.js"></script> 
    <script src="js/soundjs-0.5.2.min.js"></script>
    <!-- indiegmr collision detection lib -->
    <script src="js/ndgmr.Collision.js"></script>
    <!-- TypeScript compiler generated scripts --> 
    <script src="ts/utils/SpriteSheet.js"></script>
    <script src="ts/Ground.js"></script> 
    <script src="ts/MasterChief.js"></script>
    <script src="ts/AsukaKamikaze.js"></script> 
    <script src="ts/Bullet.js"></script>
    <script src="ts/Explosion.js"></script> 
    <script src="ts/Main.js"></script>
</head>
<body>
    <canvas id="gameCanvas" width="800" height="380"></canvas>
</body>
</html>
In the script tags the CreateJS libs and TypeScript generated JavaScript files are loaded. I also load a collision detection lib which I will cover later. The canvas element is where the action takes place and its id attribute is set with the value gameCanvas. When the window is loaded an object of type Main is created and passed thecanvas element as a parameter.
window.addEventListener('load', () => {
    var canvas = <HTMLCanvasElement> document.getElementById('gameCanvas');
    canvas.style.background = '#000';
    var main = new Main(canvas);
})
This event listener is specified in a TypeScript file named Main.ts. The Main class contains the following variables,
private canvas: HTMLCanvasElement;
private stage: createjs.Stage;
private manifest: any[];
private queue: createjs.LoadQueue;

private message: createjs.Text;
private score: createjs.Text;
private background: createjs.Bitmap;
private ground: Ground;
private masterChief: MasterChief;
private groundImg: HTMLImageElement;
private explosionImg: HTMLImageElement;
private bulletImg: HTMLImageElement;
private asukaImg: HTMLImageElement;
private asukaDoc: XMLDocument;

private asukas: AsukaKamikaze[] = []
private bullets: Bullet[] = [];    
private explosions: Explosion[] = [];

private canFire: boolean = true;
private isGameOver: boolean = false;

private asukaInterval: number;
private points: number = 0;
Several of the variables are of types defined in the CreateJS libs. For class Main to make use of these types I have to first specify a reference to the CreateJS type definitions.
/// <reference path="Scripts/typings/createjs/createjs.d.ts"/>
/// <reference path="Scripts/typings/easeljs/easeljs.d.ts"/>
/// <reference path="Scripts/typings/preloadjs/preloadjs.d.ts"/>
/// <reference path="Scripts/typings/soundjs/soundjs.d.ts"/>
/// <reference path="Scripts/typings/ndgmr/ndgmr.Collision.d.ts"/>

class Main {
...
In the constructor of class Main I instantiate a Stage object. The stage is where display objects like sprites, bitmaps, and text will be placed.
constructor(canvas: HTMLCanvasElement) {
    this.canvas = canvas;
    this.stage = new createjs.Stage(canvas);

    this.message = new createjs.Text('', 'bold 30px Segoe UI', '#e66000');
    this.message.textAlign = 'center';
    this.message.x = canvas.width / 2;
    this.message.y = canvas.height / 2;
    this.stage.addChild(this.message);       

    this.manifest =
    [
        { src: 'assets/images/AsukaKamikazeSpriteSheet.png', id: 'asuka' },
        { src: 'assets/images/Background.png', id: 'background' },
        { src: 'assets/images/Bullet.png', id: 'bullet' },
        { src: 'assets/images/ExplosionSpriteSheet.png', id: 'explosion' },
        { src: 'assets/images/ground.png', id: 'ground' },
        { src: 'assets/images/MasterChiefSpriteSheet.png', id: 'masterChief' },
        { src: 'assets/data/AsukaKamikazeSpriteSheet.xml', id: 'asukaData' },
        { src: 'assets/data/MasterChiefSpriteSheet.xml', id: 'chiefData' },
        { src: 'assets/sounds/Glock_17.mp3', id: 'glock' },
        { src: 'assets/sounds/Echinoderm_Regeneration.mp3', id: 'music' },
        { src: 'assets/sounds/Bomb_Exploding.mp3', id: 'bomb' },
    ];

    this.queue = new createjs.LoadQueue();
    this.queue.installPlugin(createjs.Sound);
    this.queue.on('complete', (e: createjs.Event) => { this.onComplete(e) });
    this.queue.on('progress', (e: createjs.Event) => { this.loading(e) });
    this.queue.loadManifest(this.manifest);
}
In the constructor I also create a Text object and use PreloadJS to load several files. The queue is a load manager and loads the queue of files specified in the manifest, using the loadManifest() method. In order to enable preloading of the audio files I register the SoundJS Sound class as a plugin using theinstallPlugin() method. The event handlers for the complete and progress events of the LoadQueueobject are also specified in the constructor. The complete event will be fired when the entire queue has been loaded while the progress event is fired when the overall loading progress changes.
The event handler for the progress event displays the file loading progress.
private loading(e: createjs.Event) {
    this.message.text = 'Loading: ' + Math.round(e.progress * 100) + '%';
    this.stage.update();
}
In order for the changes to the Text object to be displayed the update() method of the stage is called. Theupdate() method redraws the stage.
The onComplete() method will be called once all the files have been loaded.
private onComplete(e: createjs.Event) {
    this.stage.removeChild(this.message);
    
    var backgroundImg = <HTMLImageElement> this.queue.getResult('background')
    this.background = new createjs.Bitmap(backgroundImg);
    
    var groundImg = <HTMLImageElement> this.queue.getResult('ground');
    this.ground = new Ground(groundImg, this.canvas);
    
    var chiefImg = <HTMLImageElement> this.queue.getResult('masterChief');
    var chiefDoc = <XMLDocument> this.queue.getResult('chiefData');
    this.masterChief = new MasterChief(chiefImg, chiefDoc);
    this.masterChief.x = 180;
    this.masterChief.y = this.ground.y - this.masterChief.getBounds().height;

    this.score = new createjs.Text('Score: 0', 'Bold 15px Arial', '#000');
    this.score.textAlign = 'left';
    this.score.shadow = new createjs.Shadow("#000", 3, 4, 8);
    this.score.x = 10;
    this.score.y = 10;        
    // Add elements to stage.
    this.stage.addChild(this.background, this.ground, this.masterChief, this.score);

    this.explosionImg = <HTMLImageElement> this.queue.getResult('explosion');
    this.bulletImg = <HTMLImageElement> this.queue.getResult('bullet');
    this.asukaImg = <HTMLImageElement> this.queue.getResult('asuka');
    this.asukaDoc = <XMLDocument> this.queue.getResult('asukaData');

    createjs.Ticker.setFPS(30);
    createjs.Ticker.on('tick', (e: createjs.TickerEvent) => { this.tick(e) });

    document.addEventListener('keydown', (e: KeyboardEvent) => { this.keyDown(e) });
    document.addEventListener('keyup', (e: KeyboardEvent) => { this.keyUp(e) });

    createjs.Sound.play('music', createjs.Sound.INTERRUPT_NONE, 0, 0, -1, 0.5);

    this.asukaInterval = setInterval(() => { this.createAsuka() }, 6000);
}
After the files have been loaded I create a Bitmap that will serve as the background, using the getResult()method of the LoadQueue object to get the necessary file.

Ground

The Ground object will serve as its name suggests. The Ground class inherits from the CreateJS Shape class.
/// <reference path="Scripts/typings/createjs/createjs.d.ts"/>
/// <reference path="Scripts/typings/easeljs/easeljs.d.ts"/>

class Ground extends createjs.Shape {

    private img: HTMLImageElement;    

    constructor(img: HTMLImageElement, canvas: HTMLCanvasElement) {
        super(new createjs.Graphics());
        this.graphics.beginBitmapFill(img);
        this.graphics.drawRect(0, 0, canvas.width + img.width, img.height);
        this.y = canvas.height - img.height;
        this.img = img;
    }    

    public tick(ds: number) {        
        this.x = (this.x - ds * 150) % this.img.width;
    }
}
The CreateJS Shape class enables the display of vector art and contains a graphics property, of type Graphics, which defines the graphic instance to display. The CreateJS Graphics class exposes several vector drawing methods.

MasterChief

The MasterChief object is passed an image, which is the spritesheet I made in darkFunction, and the xml file containing the data for the sprite sheet. The MasterChief class inherits from the CreateJS Sprite class.
/// <reference path="Scripts/typings/createjs/createjs.d.ts"/>
/// <reference path="Scripts/typings/easeljs/easeljs.d.ts"/>

class MasterChief extends createjs.Sprite {
    constructor(img: HTMLImageElement, doc: XMLDocument) {
        super(new createjs.SpriteSheet({
            images: [img],
            frames: utils.SpriteSheet.getData(doc),
            animations:
            {
                stand: 0,
                fire:
                {
                    frames: 1,
                    next: 'stand',
                    speed: 0.8
                },
                run: [2, 11, true, 0.5],
                crouch: 15
            }
        }), 'stand');        
    } 
}
The CreateJS Sprite class is used to display a frame or sequence of frames. The constructor of the Sprite class is passed a SpriteSheet instance as a parameter and the frame number or animation that will be played initially. The parameters passed to the SpriteSheet constructor define the image/s to be used, the position of individual frames, and the animations for a SpriteSheet instance. A MasterChief object has four animations with the stand animation as the default animation to play.
To set the frames property of the SpriteSheet's data object I've written a utility class that contains a static method called getData(), which parses the XML document and returns an array.
module utils {

    export class SpriteSheet {
                
        static getData(doc: XMLDocument): any[] {           
            var sprites = doc.getElementsByTagName('spr');
            var frames = [];
            for (var i = 0; i < sprites.length; i++) {
                var x = parseInt(sprites.item(i).attributes.getNamedItem('x').value);
                var y = parseInt(sprites.item(i).attributes.getNamedItem('y').value);
                var w = parseInt(sprites.item(i).attributes.getNamedItem('w').value);
                var h = parseInt(sprites.item(i).attributes.getNamedItem('h').value);

                frames.push([x, y, w, h]);
            }

            return frames;
        }

    } 

}
In the onComplete() method I also set the framerate of the Ticker and an event handler for the Ticker'stick event. The Ticker provides a heartbeat broadcast at a set interval and its tick event handler will serve as the game loop.
private tick(e: createjs.TickerEvent) {
    var ds = e.delta / 1000;
              
    if (this.masterChief.currentAnimation == 'run' && !this.isGameOver) {
        this.ground.tick(ds);
    }

    this.moveBullets(ds);
    this.moveAsukas(ds);

    this.checkBulletAsukaCollision();
    this.checkAsukaMasterChiefCollision();

    this.stageCleanup();

    this.stage.update(e);
}
The parameter passed to the tick() method indicates the amount of time that has elapsed since the previous tick. The Ground object's tick() method is only called when the MasterChief object's animation changes torun. The sprite's animation is changed in the keydown and keyup event handlers.
private keyDown(e: KeyboardEvent) {
    var key = e.keyCode;
    switch (key) {
        case 39: // Right
            if (this.masterChief.currentAnimation != 'run' && !this.isGameOver) {
                this.masterChief.gotoAndPlay('run');
            }
            break;
        case 32: // Spacebar
            if (this.canFire && !this.isGameOver) {
                this.masterChief.gotoAndPlay('fire');
                this.createBullet();
                createjs.Sound.play('glock');
                this.canFire = false;
            }
            break;
        case 40: // Down
            if (this.masterChief.currentAnimation != 'crouch' && !this.isGameOver) {
                this.masterChief.gotoAndStop('crouch');
            }
            break;
        case 38: // Up
            if (this.masterChief.currentAnimation != 'stand' && !this.isGameOver) {
                this.masterChief.gotoAndStop('stand');
            }
            break;
        case 13: // Enter
            if (this.isGameOver) {
                this.stage.removeChild(this.message);
                this.masterChief.visible = true;
                this.asukaInterval = setInterval(() => { this.createAsuka() }, 6000);
                this.isGameOver = false;
                this.points = 0;
                this.score.text = '0';
            }
            break;
    }
}

private keyUp(e: KeyboardEvent) {
    var key = e.keyCode;
    if (key == 39) {
        this.masterChief.gotoAndPlay('stand');
    }
    else if (key == 32) {
        this.canFire = true;
    }
}

Bullets

The Bullet class is a simple class that inherits from the CreateJS Bitmap class.
/// <reference path="Scripts/typings/createjs/createjs.d.ts"/>
/// <reference path="Scripts/typings/easeljs/easeljs.d.ts"/>

class Bullet extends createjs.Bitmap {
    constructor(img: HTMLImageElement) {
        super(img);
    }   

    public tick(ds: number) {        
        this.x += ds * 1000;
    }
}
Bullet objects are created when the user presses the spacebar and the createBullet() method in class Mainis called.
private createBullet() {
    var bullet = new Bullet(this.bulletImg);
    bullet.alpha = 0.3;
    bullet.x = this.masterChief.x + this.masterChief.getbounds().width - 5;
    bullet.y = this.masterChief.y + 32;
    this.bullets.push(bullet);
    this.stage.addChild(bullet);
}

Asukas

The Asuka class inherits from the CreateJS Sprite class.
/// <reference path="Scripts/typings/createjs/createjs.d.ts"/>
/// <reference path="Scripts/typings/easeljs/easeljs.d.ts"/>

class AsukaKamikaze extends createjs.Sprite {
    private hitCount: number = 0;
    
    constructor(img: HTMLImageElement, doc: XMLDocument) {
        super(new createjs.SpriteSheet({
            images: [img],
            frames: utils.SpriteSheet.getData(doc),
            animations:
            {
                run: [0, 5, true, 0.4],
                hit: [6, 8, 'dead', 0.2],
                dead: 9
            }
        }), 'run');
    }    

    public set HitCount(value: number) {
        this.hitCount = value;        
    }
    
    public get HitCount(): number {
        return this.hitCount;
    }

    private VELOCITY: number = 200;

    public tick(ds: number) {        
        this.x -= ds * this.VELOCITY;
    }     
       
} 

Collision Detection

In one of the <script> tags in the HTML markup for index.html I load a JavaScript library that I use for collision detection. The library, written by Olaf Horstmann, provides pixel perfect and bounding box collision detection for EaselJS Bitmaps. To make use of the library I've written its type definitions in a file named ndgmr.Collision.d.ts.
declare module ndgmr {
    export function checkRectCollision(bitmap1: any, bitmap2: any): any;
    export function checkPixelCollision(bitmap1: any, bitmap2: any, alphaThreshold: number, getRect?: any): any;
}
To check for collision between a Bullet and a Sprite I can then do,
private checkBulletAsukaCollision() {
    for (var a in this.asukas) {
        var asuka = this.asukas[a];
        for (var b in this.bullets) {
            var bullet = this.bullets[b];
            var collision = ndgmr.checkPixelCollision(asuka, bullet, 0);
            if (collision) {
                this.removeElement(bullet, this.bullets);
                asuka.HitCount += 1;
                if (asuka.HitCount == 5) {
                    asuka.gotoAndPlay('hit');
                    this.points += 1;
                    this.score.text = this.points.toString();
                }
            }
        }
    }
}
The ndgmr checkPixelCollision() method returns null if there is no collision or, in case of a collision, an object with the size and position of the intersection.

Conclusion

I have to confess that this is my first attempt at a HTML5 application and the combination of TypeScript and CreateJS made the experience tolerable and worthwhile.

No comments:

Post a Comment

Genuine websites to earn money.

If you are interested in PTC sites then this article is for you. I have personally tried many of the sites and found that the best thing ...