65.9K
CodeProject 正在变化。 阅读更多。
Home

Master Chief, CreateJS & TypeScript

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (46投票s)

2014年4月7日

CPOL

9分钟阅读

viewsIcon

67725

downloadIcon

1308

使用CreateJS和TypeScript创建一个简单的HTML5游戏。

引言

在本文中,我将解释如何使用CreateJSTypeScript。示例项目是一个简单的横版卷轴游戏;其中Master Chief试图避开“可爱的”Asuka Kazama机器人。Asuka Kazamaandroids。游戏没有关卡,也没有管道。 :cool

TypeScript

TypeScript是JavaScript的一个超集,它为语言增加了可选的静态类型和基于类的面向对象编程。它编译为JavaScript,生成的JavaScript输出与TypeScript输入非常匹配。在这方面,“每个JavaScript程序也是一个TypeScript程序。”如果您还不熟悉TypeScript,请查看以下资源以快速上手,

CreateJS

CreateJS是一套JavaScript库和工具,可以轻松构建丰富且交互式的HTML5应用程序。这些库可以独立工作,也可以根据需要进行混合搭配。

CreateJS套件由四个主要库组成,

  • EaselJS:提供了一个完整的、分层的显示列表,一个核心的交互模型,以及使HTML5Canvas元素更容易使用的辅助类,
  • 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对象的completeprogress事件的事件处理程序也在构造函数中指定。当整个队列加载完成后,将触发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的帧率,以及Tickertick事件的事件处理程序。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时调用。精灵的动画在keydownkeyup事件处理程序中更改。

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库。libraryOlaf 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;
}

为了检查BulletSprite之间的碰撞,我可以这样做,

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();
                }
            }
        }
    }
}

ndgmrcheckPixelCollision()方法在没有碰撞时返回null,在发生碰撞时返回一个包含交集大小和位置的对象。

结论

我必须承认,这是我第一次尝试开发HTML5应用程序,TypeScript和CreateJS的结合使这次经历是可以忍受且有价值的。CreateJS拥有非常好的文档和示例,所以如果您对该库有更多兴趣,请查看他们的website和GitHubrepository

历史

  • 2014年4月7日:初始发布
© . All rights reserved.