Вы находитесь на странице: 1из 15

Master Chief, CreateJS & TypeScript

Meshack Musundi, 14 Aug 2014


4.99 (46 votes)

Rate this: vote 1vote 2vote 3vote 4vote 5


Using CreateJS and TypeScript to create a simple HTML5 game
 Download MasterChief.zip - 3.5 MB

Introduction
In this article I'll explain how you can go about using CreateJS with TypeScript. The sample project
is a simple side-scrolling game; where Master Chief tries to avoid getting Kamikazed by
'lovely' Asuka Kazama androids. The game has no levels, and no pipes. :cool:

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,

 Say Hello to TypeScript,


 TypeScript Handbook

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,

Hide Copy Code


<?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
named SamplePlugin. 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,

Hide Copy Code


<!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 the canvas element as a parameter.

Hide Copy Code


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,

Hide Copy Code


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.

Hide Copy Code


/// <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.

Hide Shrink Copy Code


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 the installPlugin() 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.

Hide Copy Code


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. The update() method redraws the stage.

The onComplete() method will be called once all the files have been loaded.

Hide Shrink Copy Code


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.

Hide Copy Code


/// <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.

Hide Copy Code


/// <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.

Hide Copy Code


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's tick event. The Ticker provides a heartbeat broadcast at a set interval and
its tick event handler will serve as the game loop.

Hide Copy Code


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 to run. The sprite's animation is changed in
the keydown and keyup event handlers.

Hide Shrink Copy Code


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.

Hide Copy Code


/// <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.

Hide Copy Code


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.

Hide Shrink Copy Code


/// <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.

Hide Copy Code


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,

Hide Copy Code


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. CreateJS has really good
documentation and samples so if you are interested in more details regarding the library then do
take a look at their website and GitHub repository.

History
 7th April 2014: Initial post

License
This article, along with any associated source code and files, is licensed under The Code Project
Open License (CPOL)

Вам также может понравиться