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 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,
<!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 LoadQueue
object 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 Main
is 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