Master Chief, CreateJS & TypeScript






4.99/5 (46投票s)
使用CreateJS和TypeScript创建一个简单的HTML5游戏。
引言
在本文中,我将解释如何使用CreateJS和TypeScript。示例项目是一个简单的横版卷轴游戏;其中Master Chief试图避开“可爱的”Asuka Kazama机器人。Asuka Kazamaandroids。游戏没有关卡,也没有管道。 :cool
TypeScript
TypeScript是JavaScript的一个超集,它为语言增加了可选的静态类型和基于类的面向对象编程。它编译为JavaScript,生成的JavaScript输出与TypeScript输入非常匹配。在这方面,“每个JavaScript程序也是一个TypeScript程序。”如果您还不熟悉TypeScript,请查看以下资源以快速上手,
CreateJS
CreateJS是一套JavaScript库和工具,可以轻松构建丰富且交互式的HTML5应用程序。这些库可以独立工作,也可以根据需要进行混合搭配。
CreateJS套件由四个主要库组成,
- EaselJS:提供了一个完整的、分层的显示列表,一个核心的交互模型,以及使HTML5
Canvas
元素更容易使用的辅助类, - TweenJS:支持数值对象属性和CSS样式属性的缓动。它旨在支持EaselJS,但并不依赖于它,
- SoundJS:在HTML5中提供一致的跨浏览器音频支持。它使开发人员能够查询功能,然后指定和优先选择用于特定设备和浏览器的API、插件和功能,
- PreloadJS:可以轻松预加载图像和声音等资源。
CreateJS是免费且开源的,并由Adobe、Microsoft、AOL和Mozilla官方赞助。在我的项目中,我将只使用三个CreateJS库:EaselJS、SoundJS和PreloadJS。
入门
精灵图集
正如我在文章开头提到的,我的简单游戏中的主要角色是Master Chief,来自流行的第一人称射击游戏;Halo。对于我的项目,来自Halo Zero的Master Chief精灵图集副本足以作为合适的资产。
虽然精灵图集在其原始状态下还可以,但它包含了我简单游戏不需要的精灵。另一件事,也是最重要的问题是,精灵是不均匀的,即它们的宽度和高度各不相同。为了让EaselJS能够恰当地使用这样的精灵图集,我需要提供包含每个精灵的x和y偏移量;它们的宽度、高度和图像索引的数据。为了生成合适的精灵图集和相关数据,我使用了darkFunction Editor;一个免费开源的2D精灵编辑器,可以快速定义精灵图集。
在darkFunction中打开精灵图集后,我选择了我需要的精灵,这很简单,只需双击图像即可。我非常小心地调整了我的选择的高度,以使每个选择具有相似的高度。(这有助于防止EaselJS将任何高度接近或小于最高精灵高度一半的精灵向上移动的问题)。然后,我使用了darkFunction中一个能够优化打包精灵的功能,从而从我的选择中创建了一个更紧凑的精灵图集。
保存新的精灵图集后,编辑器允许您保存精灵图集数据。数据包含在一个.sprites文件中,实际上它只是一个XML文件。以下是我为新精灵图集生成的数据,
<?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>
请注意,每个<spr>
元素的h
属性都相同。对于Asuka精灵图集及其相应的数据文件,我也使用了类似的过程。.sprites文件对这个项目非常有价值,但它们的.sprites扩展名并没有真正帮助,并且会使文件无法解析。因此,我将扩展名更改为.xml。
音效
没有一些音频效果,项目会有点乏味,我们将借助SoundJS来使用它们。枪声和爆炸声来自SoundBible,它提供免版税的声音效果。背景音乐来自YouTube的Audio Library,其中包含一系列免费音乐曲目,可以根据各种标准进行过滤。
类型定义
MasterChief项目使用我从CreateJS GitHubrepository下载的必要CreateJS库的本地副本。这些库位于一个名为js的文件夹中。
请记住,“每个JavaScript程序也是一个TypeScript程序”,虽然如此,但没有TypeScript类型定义,CreateJS库就无法与TypeScript一起使用。类型定义使TypeScript编译器能够了解现有JavaScript库的公共API。幸运的是,您可以通过NuGet或DefinitelyTyped的GitHub存储库获取CreateJS库的类型定义。
使用Visual Studio的NuGet包管理器,我搜索并安装了EaselJS、PreloadJS和SoundJS的类型定义。
安装EaselJS的类型定义还会安装CreateJS和TweenJS的类型定义。这些定义位于一个名为Scripts的文件夹中,并带有.d.ts扩展名。
注意:PreloadJS和TweenJS的类型定义都包含一个名为SamplePlugin
的类的环境声明。这种情况将产生编译时错误,所以我注释掉了TweenJS类型定义中的环境声明。
MasterChief
index.html的HTML标记很简单,
<!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>
在script
标签中加载CreateJS库和TypeScript生成的JavaScript文件。我还加载了一个将在稍后介绍的碰撞检测库。canvas
元素是发生动作的地方,它的id
属性设置为gameCanvas
。当窗口加载时,将创建一个类型为Main
的对象,并将canvas
元素作为参数传递。
window.addEventListener('load', () => {
var canvas = <HTMLCanvasElement> document.getElementById('gameCanvas');
canvas.style.background = '#000';
var main = new Main(canvas);
})
此事件监听器在名为Main.ts的TypeScript文件中指定。Main
类包含以下变量,
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;
其中一些变量的类型定义在CreateJS库中。为了让类Main
能够使用这些类型,我必须首先指定一个对CreateJS类型定义的引用。
/// <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 {
...
在类Main
的构造函数中,我实例化一个Stage
对象。舞台是显示对象(如精灵、位图和文本)将被放置的地方。
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);
}
在构造函数中,我还创建了一个Text
对象,并使用PreloadJS加载了几个文件。queue
是一个加载管理器,它使用loadManifest()
方法加载manifest
中指定的文件队列。为了启用音频文件的预加载,我使用installPlugin()
方法将SoundJS的Sound
类注册为一个插件。LoadQueue
对象的complete
和progress
事件的事件处理程序也在构造函数中指定。当整个队列加载完成后,将触发complete
事件,而当整体加载进度发生变化时,将触发progress
事件。
progress
事件的事件处理程序显示文件加载进度。
private loading(e: createjs.Event) {
this.message.text = 'Loading: ' + Math.round(e.progress * 100) + '%';
this.stage.update();
}
为了显示对Text
对象的更改,需要调用舞台的update()
方法。update()
方法会重绘舞台。
当所有文件都加载完成后,将调用onComplete()
方法。
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);
}
文件加载完成后,我创建一个Bitmap
作为背景,使用LoadQueue
对象的getResult()
方法获取必要的文件。
地线
Ground
对象将起到其名称所暗示的作用。Ground
类继承自CreateJS的Shape
类。
/// <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;
}
}
CreateJS的Shape
类允许显示矢量图,并包含一个graphics
属性,类型为Graphics
,它定义了要显示的图形实例。CreateJS的Graphics
类公开了多种矢量绘图方法。
MasterChief
MasterChief
对象被传递了一个图像,这是我在darkFunction中制作的精灵图集,以及包含精灵图集数据的xml文件。MasterChief
类继承自CreateJS的Sprite
类。
/// <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');
}
}
CreateJS的Sprite
类用于显示一个帧或一系列帧。Sprite
类的构造函数以SpriteSheet
实例和初始播放的帧号或动画作为参数。传递给SpriteSheet
构造函数的参数定义了要使用的图像、单个帧的位置以及SpriteSheet
实例的动画。MasterChief
对象有四种动画,其中stand
动画是默认播放的动画。
为了设置SpriteSheet
数据对象的frames
属性,我编写了一个实用类,其中包含一个名为getData()
的静态方法,该方法解析XML文档并返回一个数组。
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;
}
}
}
在onComplete()
方法中,我还设置了Ticker
的帧率,以及Ticker
的tick
事件的事件处理程序。Ticker
以设定的间隔提供心跳广播,其tick
事件处理程序将作为游戏循环。
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);
}
传递给tick()
方法的参数表示自上一个tick以来经过的时间量。Ground
对象的tick()
方法仅在MasterChief
对象的动画更改为run
时调用。精灵的动画在keydown
和keyup
事件处理程序中更改。
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;
}
}
子弹
Bullet
类是一个简单的类,继承自CreateJS的Bitmap
类。
/// <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;
}
}
当用户按下空格键并调用Main
类中的createBullet()
方法时,会创建Bullet
对象。
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
Asuka
类继承自CreateJS的Sprite
类。
/// <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;
}
}
碰撞检测
在index.html的HTML标记中的一个<script>
标签中,我加载了一个用于碰撞检测的JavaScript库。library由Olaf Horstmann编写,为EaselJS Bitmaps
提供像素级精确和边界框碰撞检测。为了使用该库,我将其类型定义写在一个名为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;
}
为了检查Bullet
和Sprite
之间的碰撞,我可以这样做,
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();
}
}
}
}
}
ndgmr
的checkPixelCollision()
方法在没有碰撞时返回null,在发生碰撞时返回一个包含交集大小和位置的对象。
结论
我必须承认,这是我第一次尝试开发HTML5应用程序,TypeScript和CreateJS的结合使这次经历是可以忍受且有价值的。CreateJS拥有非常好的文档和示例,所以如果您对该库有更多兴趣,请查看他们的website和GitHubrepository。
历史
- 2014年4月7日:初始发布