制作基于 HTML5 Canvas 的游戏:使用 AngularJS 和 CreateJS 的教程

已发表: 2022-03-11

游戏开发是不断挑战软件开发行业的更有趣、更先进的编程技术之一。

用于开发游戏的编程平台有很多,可以在上面玩游戏的设备也很多,但在 Web 浏览器中玩游戏时,基于 Flash 的开发仍然处于领先地位。

将基于 Flash 的游戏重写为 HTML5 Canvas 技术可以让我们在移动浏览器上玩它们。 而且,借助 Apache Cordova,熟练的 Web 开发人员可以轻松地将它们封装到跨平台的移动游戏应用程序中。

CreateJS 的人们开始这样做,甚至更多。

EaselJS是 CreateJS 套件的一部分,使在 HTML5 Canvas 上绘图变得简单。 想象一下构建具有高性能和数千个元素的自定义数据可视化。 可缩放矢量图形 (SVG) 不是正确的选择,因为它使用 DOM 元素。 当大约 600 个 DOM 元素、初始渲染、重新绘制和动画成为昂贵的操作时,浏览器就会不堪重负。 使用 HTML5 Canvas,我们可以轻松解决这些问题; 画布绘图就像纸上的墨水,没有 DOM 元素及其相关成本。

这意味着基于 Canvas 的开发在分离元素以及将事件和行为附加到它们时需要更多关注。 EaselJS 来救援; 我们可以像处理单个元素一样进行编码,让 EaselJS 库处理您的鼠标悬停、点击和碰撞。

基于 SVG 的编码有一个很大的优势:SVG 有一个旧的规范,并且有很多设计工具可以导出 SVG 资产以供开发使用,因此设计人员和开发人员之间的合作很好。 流行的库(例如 D3.JS)和更新的、功能更强大的库(例如 SnapSVG)带来了很多东西。

如果设计人员到开发人员的工作流程是您使用 SVG 的唯一原因,请考虑使用 Adob​​e Illustrator (AI) 的扩展程序,该扩展程序可以从 AI 中创建的形状生成代码。 在我们的上下文中,此类扩展会生成 EaselJS 代码或 ProcessingJS 代码,它们都是基于 HTML5 Canvas 的库

最重要的是,如果您正在开始一个新项目,则没有理由再使用 SVG!

SoundJS是 CreateJS 套件的一部分; 它为 HTML5 音频规范提供了一个简单的 API。

PreloadJS用于预加载位图、声音文件等资产。 它与其他 CreateJS 库结合使用效果很好。

EaselJS、SoundJS 和 PreloadJS 让任何 JavaScript 忍者的游戏开发都变得超级简单。 任何使用基于 Flash 的游戏开发的人都熟悉它的 API 方法。

“这一切都很棒。 但是,如果我们有一个开发团队将一堆游戏从 Flash 转换为 HTML5 会怎样? 这套房能做到吗?”

答案是:“是的,但前提是您的所有开发人员都处于绝地级别!”。

如果您有一个由不同技能的开发人员组成的团队(通常情况如此),那么使用 CreateJS 并期望可扩展和模块化的代码可能会有点吓人。 如果我们将 CreateJS 套件与 AngularJS 结合在一起会怎样? 我们能否通过引入最好和最常用的前端 JS 框架来减轻这种风险?

的,这个 HTML5 Canvas 游戏教程将教您如何使用 CreateJS 和 AngularJS 创建一个基本游戏!

使用 CreateJS 和 AngularJS 的 HTML5 Canvas 游戏教程

播种

AngularJS 通过为您的开发团队提供以下功能来显着降低复杂性:

  1. 添加代码模块化,使团队成员可以专注于游戏的不同方面。
  2. 将代码分解成单独的可测试和可维护的部分。
  3. 启用代码重用,这样一个工厂类可以被多次实例化,并重用于加载不同但相似的资产和行为。
  4. 加快开发速度,因为多个团队成员可以并行工作,而不会互相干扰。
  5. 保护开发人员不使用不良模式(Javascript 带有臭名昭著的不良部分,而 JSLint 只能帮助我们这么多)。
  6. 添加一个可靠的测试框架。

如果像我一样,您是“修补匠”或触觉学习者,您应该从 GitHub 获取代码并开始学习。 我的建议是查看我的签到并了解我为获得将 AngularJS 优点添加到 CreateJS 代码中的好处而采取的步骤。

运行你的 AngularJS 种子项目

如果您还没有这样做,则需要先安装 nodeJS,然后才能运行此演示。

创建 AngularJS 种子项目或从 GitHub 下载后,运行npm install将所有依赖项下载到您的应用程序文件夹。

要运行您的应用程序,请从同一文件夹执行npm start并在浏览器中导航到http://localhost:8000/app/#/view1 。 您的页面应如下图所示。

页面示例

EaselJS 遇到 AngularJS

将 CreateJS 库引用添加到您的 AngularJS 种子项目。 确保在 AngularJS 之后包含 CreateJS 脚本。

<script src="http://code.createjs.com/createjs-2014.12.12.min.js"></script>

接下来,清理应用程序:

  • 从您的应用文件夹中删除 view2 文件夹
  • 通过删除下面显示的代码,从 index.html 中删除菜单和 AngularJS 版本信息:
 <ul class="menu"> <li><a href="#/view1">view1</a></li> <li><a href="#/view2">view2</a></li> </ul> … <div>Angular seed app: v<span app-version></span></div> … <script src="view2/view2.js"></script>

通过删除以下行从app.js中删除view2模块

myApp.view2,

如果您以前没有使用过 AngularJS,并且不熟悉 AngularJS 指令,请查看本教程。 AngularJS 中的指令是一种教 HTML 一些新技巧的方法。 它们是框架中经过深思熟虑的特性,使 AngularJS 强大且可扩展。

每当您需要专门的 DOM 功能或组件时,请在线搜索; 很有可能它已经在 Angular 模块等地方可用。

我们需要做的下一件事是创建一个新的 AngularJS 指令,它将实现 EaselJS 中的示例。 在位于/app/view1/directives/spriteSheetRunner.js的新文件中创建一个名为 spriteSheetRunner 的新指令。

 angular.module('myApp.directives', []) .directive('spriteSheetRunner', function () { "use strict"; return { restrict : 'EAC', replace : true, scope :{ }, template: "<canvas width='960' height='400'></canvas>", link: function (scope, element, attribute) { var w, h, loader, manifest, sky, grant, ground, hill, hill2; drawGame(); function drawGame() { //drawing the game canvas from scratch here //In future we can pass stages as param and load indexes from arrays of background elements etc if (scope.stage) { scope.stage.autoClear = true; scope.stage.removeAllChildren(); scope.stage.update(); } else { scope.stage = new createjs.Stage(element[0]); } w = scope.stage.canvas.width; h = scope.stage.canvas.height; manifest = [ {src: "spritesheet_grant.png", id: "grant"}, {src: "sky.png", id: "sky"}, {src: "ground.png", id: "ground"}, {src: "hill1.png", id: "hill"}, {src: "hill2.png", id: "hill2"} ]; loader = new createjs.LoadQueue(false); loader.addEventListener("complete", handleComplete); loader.loadManifest(manifest, true, "/app/assets/"); } function handleComplete() { sky = new createjs.Shape(); sky.graphics.beginBitmapFill(loader.getResult("sky")).drawRect(0, 0, w, h); var groundImg = loader.getResult("ground"); ground = new createjs.Shape(); ground.graphics.beginBitmapFill(groundImg).drawRect(0, 0, w groundImg.width, groundImg.height); ground.tileW = groundImg.width; ground.y = h - groundImg.height; hill = new createjs.Bitmap(loader.getResult("hill")); hill.setTransform(Math.random() * w, h - hill.image.height * 4 - groundImg.height, 4, 4); hill.alpha = 0.5; hill2 = new createjs.Bitmap(loader.getResult("hill2")); hill2.setTransform(Math.random() * w, h - hill2.image.height * 3 - groundImg.height, 3, 3); var spriteSheet = new createjs.SpriteSheet({ framerate: 30, "images": [loader.getResult("grant")], "frames": {"regX": 82, "height": 292, "count": 64, "regY": 0, "width": 165}, // define two animations, run (loops, 1.5x speed) and jump (returns to run): "animations": { "run": [0, 25, "run", 1.5], "jump": [26, 63, "run"] } }); grant = new createjs.Sprite(spriteSheet, "run"); grant.y = 35; scope.stage.addChild(sky, hill, hill2, ground, grant); scope.stage.addEventListener("stagemousedown", handleJumpStart); createjs.Ticker.timingMode = createjs.Ticker.RAF; createjs.Ticker.addEventListener("tick", tick); } function handleJumpStart() { grant.gotoAndPlay("jump"); } function tick(event) { var deltaS = event.delta / 1000; var position = grant.x 150 * deltaS; var grantW = grant.getBounds().width * grant.scaleX; grant.x = (position >= w grantW) ? -grantW : position; ground.x = (ground.x - deltaS * 150) % ground.tileW; hill.x = (hill.x - deltaS * 30); if (hill.x hill.image.width * hill.scaleX <= 0) { hill.x = w; } hill2.x = (hill2.x - deltaS * 45); if (hill2.x hill2.image.width * hill2.scaleX <= 0) { hill2.x = w; } scope.stage.update(event); } } } });

创建指令后,通过更新/app/app.js向应用程序添加依赖项,如下所示:

 'use strict'; // Declare app level module which depends on views, and components angular.module('myApp',[ 'ngRoute', 'myApp.view1', 'myApp.version', 'myApp.services', 'myApp.uiClasses', 'myApp.directives']) .config(['$routeProvider', function($routeProvider) { $routeProvider.otherwise({redirectTo: '/view1'}); }]);

通过添加对spriteSheetRunner.js的引用,在index.html中包含指令代码。

 <script src="view1/directives/spriteSheetRunner.js"></script>

我们几乎准备好了! 将游戏资源复制到您的应用文件夹。 我已经准备好了图片,所以请随意下载它们并保存在您的 app/assets 文件夹中。

  • 应用程序/资产/spritesheet_grant.png
  • 应用程序/资产/ground.png
  • 应用程序/资产/hill1.png
  • 应用程序/资产/hill2.png
  • 应用程序/资产/sky.png

作为最后一步,将我们新创建的指令添加到页面中。 为此,请更改您的app/view/view1.html文件,并将其设为单行:

 <sprite-sheet-runner></sprite-sheet-runner>

启动您的应用程序,您将启动您的跑步者 :)

运动中的跑步者

如果这是您的第一个 AngularJS 或第一个 CreateJS 应用程序,那么庆祝一下,您所做的事情真的很酷!

在服务中预加载资产

AngularJS 中的服务是主要用于共享代码和数据的单例。 我们将使用一项服务在整个应用程序中共享“游戏资产”。 要了解有关 AngularJS 服务的更多信息,请查看 AngularJS 文档。

AngularJS 开发服务提供了一种在一个地方加载和管理所有资产的有效机制。 资产更改会传播到服务的每个单独实例,从而使我们的代码更易于维护。

/app/view1/services文件夹中创建名为loaderSvc.js的新 JS 文件。

 //app/view1/services/loaderSvc.js myServices.service('loaderSvc', function () { var manifest = [ {src: "spritesheet_grant.png", id: "grant"}, {src: "sky.png", id: "sky"}, {src: "ground.png", id: "ground"}, {src: "hill1.png", id: "hill"}, {src: "hill2.png", id: "hill2"} ], loader = new createjs.LoadQueue(true); this.getResult = function (asset) { return loader.getResult(asset); }; this.getLoader = function () { return loader; }; this.loadAssets = function () { loader.loadManifest(manifest, true, "/app/assets/"); }; });

AngularJS 要求我们注册我们正在使用的任何服务。 为此,请更新您的app.js文件以包含对myApp.services的引用。

 'use strict'; // Declare app level module which depends on views, and components angular.module('myApp',[ 'ngRoute', 'myApp.view1', 'myApp.version', 'myApp.services', 'myApp.directives']) .config(['$routeProvider', function($routeProvider) { $routeProvider.otherwise({redirectTo: '/view1'}); }]); var myServices = angular.module('myApp.services', []);

更新app/view1/directives/spriteSheetRunner.js文件中的指令代码,以删除预加载代码并改用服务。

 angular.module('myApp.directives', []) .directive('spriteSheetRunner', ['loaderSvc', function (loaderSvc) { "use strict"; return { restrict : 'EAC', replace : true, scope :{ }, template: "<canvas width='960' height='400'></canvas>", link: function (scope, element, attribute) { var w, h, manifest, sky, grant, ground, hill, hill2; drawGame(); function drawGame() { //drawing the game canvas from scratch here //In future we can pass stages as param and load indexes from arrays of background elements etc if (scope.stage) { scope.stage.autoClear = true; scope.stage.removeAllChildren(); scope.stage.update(); } else { scope.stage = new createjs.Stage(element[0]); } w = scope.stage.canvas.width; h = scope.stage.canvas.height; loaderSvc.getLoader().addEventListener("complete", handleComplete); loaderSvc.loadAssets(); } function handleComplete() { sky = new createjs.Shape(); sky.graphics.beginBitmapFill(loaderSvc.getResult("sky")).drawRect(0, 0, w, h); var groundImg = loaderSvc.getResult("ground"); ground = new createjs.Shape(); ground.graphics.beginBitmapFill(groundImg).drawRect(0, 0, w + groundImg.width, groundImg.height); ground.tileW = groundImg.width; ground.y = h - groundImg.height; hill = new createjs.Bitmap(loaderSvc.getResult("hill")); hill.setTransform(Math.random() * w, h - hill.image.height * 4 - groundImg.height, 4, 4); hill.alpha = 0.5; hill2 = new createjs.Bitmap(loaderSvc.getResult("hill2")); hill2.setTransform(Math.random() * w, h - hill2.image.height * 3 - groundImg.height, 3, 3); var spriteSheet = new createjs.SpriteSheet({ framerate: 30, "images": [loaderSvc.getResult("grant")], "frames": {"regX": 82, "height": 292, "count": 64, "regY": 0, "width": 165}, // define two animations, run (loops, 1.5x speed) and jump (returns to run): "animations": { "run": [0, 25, "run", 1.5], "jump": [26, 63, "run"] } }); grant = new createjs.Sprite(spriteSheet, "run"); grant.y = 35; scope.stage.addChild(sky, hill, hill2, ground, grant); scope.stage.addEventListener("stagemousedown", handleJumpStart); createjs.Ticker.timingMode = createjs.Ticker.RAF; createjs.Ticker.addEventListener("tick", tick); } function handleJumpStart() { grant.gotoAndPlay("jump"); } function tick(event) { var deltaS = event.delta / 1000; var position = grant.x + 150 * deltaS; var grantW = grant.getBounds().width * grant.scaleX; grant.x = (position >= w + grantW) ? -grantW : position; ground.x = (ground.x - deltaS * 150) % ground.tileW; hill.x = (hill.x - deltaS * 30); if (hill.x + hill.image.width * hill.scaleX <= 0) { hill.x = w; } hill2.x = (hill2.x - deltaS * 45); if (hill2.x + hill2.image.width * hill2.scaleX <= 0) { hill2.x = w; } scope.stage.update(event); } } } }]);

创建 UI 元素工厂

在游戏开发中重用和重复精灵是非常重要的。 为了启用 UI 类(在我们的例子中是精灵)的实例化,我们将使用 AngularJS 工厂。

工厂在应用程序中注册,就像任何其他 AngularJS 模块一样。 要创建 uiClasses 工厂,请将您的 app.js 文件修改为如下所示:

 'use strict'; // Declare app level module which depends on views, and components angular.module('myApp',[ 'ngRoute', 'myApp.view1', 'myApp.version', 'myApp.services', 'myApp.uiClasses', 'myApp.directives']) .config(['$routeProvider', function($routeProvider) { $routeProvider.otherwise({redirectTo: '/view1'}); }]); var uiClasses = angular.module('myApp.uiClasses', []); var myServices = angular.module('myApp.services', []);

让我们使用新工厂来创建天空、山丘、地面和我们的跑步者。 为此,请创建如下所列的 JavaScript 文件。

  • 应用程序/view1/uiClasses/sky.js
 uiClasses.factory("Sky", [ 'loaderSvc', function (loaderSvc) { function Sky(obj) { this.sky = new createjs.Shape(); this.sky.graphics.beginBitmapFill(loaderSvc.getResult("sky")).drawRect(0, 0, obj.width, obj.height); } Sky.prototype = { addToStage: function (stage) { stage.addChild(this.sky); }, removeFromStage: function (stage) { stage.removeChild(this.sky); } }; return (Sky); }]);
  • 应用程序/view1/uiClasses/hill.js
 uiClasses.factory("Hill", [ 'loaderSvc', function (loaderSvc) { function Hill(obj) { this.hill = new createjs.Bitmap(loaderSvc.getResult(obj.assetName)); this.hill.setTransform(Math.random() * obj.width, obj.height - this.hill.image.height * obj.scaleFactor - obj.groundHeight, obj.scaleFactor, obj.scaleFactor); } Hill.prototype = { addToStage: function (stage) { stage.addChild(this.hill); }, removeFromStage: function (stage) { stage.removeChild(this.hill); }, setAlpha: function (val) { this.hill.alpha = val; }, getImageWidth: function () { return this.hill.image.width; }, getScaleX: function () { return this.hill.scaleX; }, getX: function () { return this.hill.x; }, getY: function () { return this.hill.y; }, setX: function (val) { this.hill.x = val; }, move: function (x, y) { this.hill.x = this.hill.x + x; this.hill.y = this.hill.y + y; } }; return (Hill); }]);
  • 应用程序/view1/ground.js
 uiClasses.factory("Ground", [ 'loaderSvc', function (loaderSvc) { function Ground(obj) { var groundImg = loaderSvc.getResult("ground"); this.ground = new createjs.Shape(); this.ground.graphics.beginBitmapFill(groundImg).drawRect(0, 0, obj.width + groundImg.width, groundImg.height); this.ground.tileW = groundImg.width; this.ground.y = obj.height - groundImg.height; this.height = groundImg.height; } Ground.prototype = { addToStage: function (stage) { stage.addChild(this.ground); }, removeFromStage: function (stage) { stage.removeChild(this.ground); }, getHeight: function () { return this.height; }, getX: function () { return this.ground.x; }, setX: function (val) { this.ground.x = val; }, getTileWidth: function () { return this.ground.tileW; }, move: function (x, y) { this.ground.x = this.ground.x + x; this.ground.y = this.ground.y + y; } }; return (Ground); }]);
  • app/view1/uiClasses/character.js
 uiClasses.factory("Character", [ 'loaderSvc', function (loaderSvc) { function Character(obj) { var spriteSheet = new createjs.SpriteSheet({ framerate: 30, "images": [loaderSvc.getResult(obj.characterAssetName)], "frames": {"regX": 82, "height": 292, "count": 64, "regY": 0, "width": 165}, // define two animations, run (loops, 1.5x speed) and jump (returns to run): "animations": { "run": [0, 25, "run", 1.5], "jump": [26, 63, "run"] } }); this.grant = new createjs.Sprite(spriteSheet, "run"); this.grant.y = obj.y; } Character.prototype = { addToStage: function (stage) { stage.addChild(this.grant); }, removeFromStage: function (stage) { stage.removeChild(this.grant); }, getWidth: function () { return this.grant.getBounds().width * this.grant.scaleX; }, getX: function () { return this.grant.x; }, setX: function (val) { this.grant.x = val; }, playAnimation: function (animation) { this.grant.gotoAndPlay(animation); } }; return (Character); }]);

不要忘记在index.html中添加所有这些新的 JS 文件。

现在,我们需要更新游戏指令。

 myDirectives.directive('spriteSheetRunner', ['loaderSvc','Sky', 'Ground', 'Hill', 'Character', function (loaderSvc, Sky, Ground, Hill, Character) { "use strict"; return { restrict : 'EAC', replace : true, scope :{ }, template: "<canvas width='960' height='400'></canvas>", link: function (scope, element, attribute) { var w, h, sky, grant, ground, hill, hill2; drawGame(); function drawGame() { //drawing the game canvas from scratch here if (scope.stage) { scope.stage.autoClear = true; scope.stage.removeAllChildren(); scope.stage.update(); } else { scope.stage = new createjs.Stage(element[0]); } w = scope.stage.canvas.width; h = scope.stage.canvas.height; loaderSvc.getLoader().addEventListener("complete", handleComplete); loaderSvc.loadAssets(); } function handleComplete() { sky = new Sky({width:w, height:h}); sky.addToStage(scope.stage); ground = new Ground({width:w, height:h}); hill = new Hill({width:w, height:h, scaleFactor: 4, assetName: 'hill', groundHeight: ground.getHeight()}); hill.setAlpha(0.5); hill.addToStage(scope.stage); hill2 = new Hill({width:w, height:h, scaleFactor: 3, assetName: 'hill2', groundHeight: ground.getHeight()}); hill2.addToStage(scope.stage); ground.addToStage(scope.stage); grant = new Character({characterAssetName: 'grant', y: 34}) grant.addToStage(scope.stage); scope.stage.addEventListener("stagemousedown", handleJumpStart); createjs.Ticker.timingMode = createjs.Ticker.RAF; createjs.Ticker.addEventListener("tick", tick); } function handleJumpStart() { grant.playAnimation("jump"); } function tick(event) { var deltaS = event.delta / 1000; var position = grant.getX() + 150 * deltaS; grant.setX((position >= w + grant.getWidth()) ? -grant.getWidth() : position); ground.setX((ground.getX() - deltaS * 150) % ground.getTileWidth()); hill.move(deltaS * -30, 0); if (hill.getX() + hill.getImageWidth() * hill.getScaleX() <= 0) { hill.setX(w); } hill2.move(deltaS * -45, 0); if (hill2.getX() + hill2.getImageWidth() * hill2.getScaleX() <= 0) { hill2.setX(w); } scope.stage.update(event); } } } }]);

请注意,将uiClasses移出指令将指令大小减少了 20%,从 91 行减少到 65 行。

此外,我们可以为每个工厂类独立编写测试,以简化其维护。

注意:测试是本文未涵盖的主题,但这是一个很好的起点。

方向键交互

在我们的 HTML5 Canvas 游戏教程的这一点上,鼠标点击或点击移动设备会让我们的人跳起来,我们无法阻止他。 让我们添加箭头键控件:

  • 左箭头(暂停游戏)
  • 向上箭头(跳跃)
  • 右箭头(开始运行)

为此,请创建keyDown函数并添加一个事件侦听器作为handleComplete()函数的最后一行。

 function keydown(event) { if (event.keyCode === 38) {//if keyCode is "Up" handleJumpStart(); } if (event.keyCode === 39) {//if keyCode is "Right" if (scope.status === "paused") { createjs.Ticker.addEventListener("tick", tick); scope.status = "running"; } } if (event.keyCode === 37) {//if keyCode is "Left" createjs.Ticker.removeEventListener("tick", tick); scope.status = "paused"; } } window.onkeydown = keydown;

再次尝试运行游戏并检查键盘控制。

让音乐播放

没有音乐,游戏就不好玩,所以让我们播放一些音乐。

我们首先需要将 MP3 文件添加到我们的 app/assets 文件夹中。 您可以从下面提供的 URL 下载它们。

  • 应用程序/资产/jump.mp3
  • 应用程序/资产/runningTrack.mp3

现在,我们需要使用加载器服务预加载这些声音文件。 我们将使用PreloaderJS库的loadQueue 。 更新您的app/view1/services/loaderSvc.js以预加载这些文件。

 myServices.service('loaderSvc', function () { var manifest = [ {src: "spritesheet_grant.png", id: "grant"}, {src: "sky.png", id: "sky"}, {src: "ground.png", id: "ground"}, {src: "hill1.png", id: "hill"}, {src: "hill2.png", id: "hill2"}, {src: "runningTrack.mp3", id: "runningSound"}, {src: "jump.mp3", id: "jumpingSound"} ], loader = new createjs.LoadQueue(true); // need this so it doesn't default to Web Audio createjs.Sound.registerPlugins([createjs.HTMLAudioPlugin]); loader.installPlugin(createjs.Sound); this.getResult = function (asset) { return loader.getResult(asset); }; this.getLoader = function () { return loader; }; this.loadAssets = function () { loader.loadManifest(manifest, true, "/app/assets/"); }; });

修改您的游戏指令以在游戏事件中播放声音。

 myDirectives.directive('spriteSheetRunner', [ 'loaderSvc', 'Sky', 'Ground', 'Hill', 'Character', function (loaderSvc, Sky, Ground, Hill, Character) { "use strict"; return { restrict : 'EAC', replace : true, scope :{ }, template: "<canvas width='960' height='400'></canvas>", link: function (scope, element, attribute) { var w, h, sky, grant, ground, hill, hill2, runningSoundInstance, status; drawGame(); function drawGame() { //drawing the game canvas from scratch here if (scope.stage) { scope.stage.autoClear = true; scope.stage.removeAllChildren(); scope.stage.update(); } else { scope.stage = new createjs.Stage(element[0]); } w = scope.stage.canvas.width; h = scope.stage.canvas.height; loaderSvc.getLoader().addEventListener("complete", handleComplete); loaderSvc.loadAssets(); } function handleComplete() { sky = new Sky({width:w, height:h}); sky.addToStage(scope.stage); ground = new Ground({width:w, height:h}); hill = new Hill({width:w, height:h, scaleFactor: 4, assetName: 'hill', groundHeight: ground.getHeight()}); hill.setAlpha(0.5); hill.addToStage(scope.stage); hill2 = new Hill({width:w, height:h, scaleFactor: 3, assetName: 'hill2', groundHeight: ground.getHeight()}); hill2.addToStage(scope.stage); ground.addToStage(scope.stage); grant = new Character({characterAssetName: 'grant', y: 34}); grant.addToStage(scope.stage); scope.stage.addEventListener("stagemousedown", handleJumpStart); createjs.Ticker.timingMode = createjs.Ticker.RAF; createjs.Ticker.addEventListener("tick", tick); // start playing the running sound looping indefinitely runningSoundInstance = createjs.Sound.play("runningSound", {loop: -1}); scope.status = "running"; window.onkeydown = keydown; } function keydown(event) { if (event.keyCode === 38) {//if keyCode is "Up" handleJumpStart(); } if (event.keyCode === 39) {//if keyCode is "Right" if (scope.status === "paused") { createjs.Ticker.addEventListener("tick", tick); runningSoundInstance = createjs.Sound.play("runningSound", {loop: -1}); scope.status = "running"; } } if (event.keyCode === 37) {//if keyCode is "Left" createjs.Ticker.removeEventListener("tick", tick); createjs.Sound.stop(); scope.status = "paused"; } } function handleJumpStart() { if (scope.status === "running") { createjs.Sound.play("jumpingSound"); grant.playAnimation("jump"); } } function tick(event) { var deltaS = event.delta / 1000; var position = grant.getX() + 150 * deltaS; grant.setX((position >= w + grant.getWidth()) ? -grant.getWidth() : position); ground.setX((ground.getX() - deltaS * 150) % ground.getTileWidth()); hill.move(deltaS * -30, 0); if (hill.getX() + hill.getImageWidth() * hill.getScaleX() <= 0) { hill.setX(w); } hill2.move(deltaS * -45, 0); if (hill2.getX() + hill2.getImageWidth() * hill2.getScaleX() <= 0) { hill2.setX(w); } scope.stage.update(event); } } } }]);
相关: Toptal 开发人员的 AngularJS 最佳实践和技巧

添加分数和生命指标

让我们将游戏得分和生命(心脏)指标添加到 HTML5 Canvas 游戏中。 分数将在左上角显示为数字,右上角的心形符号将指示生命计数。

我们将使用外部字体库来渲染爱心,因此将以下行添加到您的index.html文件头。

 <link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">

标准 AngularJS 绑定将提供实时更新。 将以下代码添加到您的app/view1/view1.html文件中:

 <sprite-sheet-runner score="score" lifes-count="lifesCount"></sprite-sheet-runner> <span class="top-left"><h2>Score: {{score}}</h2></span> <span class="top-right"><h2>Life: <i ng-if="lifesCount > 0" class="fa fa-heart"></i> <i ng-if="lifesCount < 1" class="fa fa-heart-o"></i> <i ng-if="lifesCount > 1" class="fa fa-heart"></i> <i ng-if="lifesCount < 2" class="fa fa-heart-o"></i> <i ng-if="lifesCount > 2" class="fa fa-heart"></i> <i ng-if="lifesCount < 3" class="fa fa-heart-o"></i> </h2></span>

为了正确定位我们的指标,我们需要在app/app.css文件中为左上角和右上角添加 CSS 类。

 .top-left { position: absolute; left: 30px; top: 10px; } .top-right { position: absolute; right: 100px; top: 10px; float: right; }

app/view1/view1.js控制器中初始化 score 和lifesCount变量。

 'use strict'; angular.module('myApp.view1', ['ngRoute']) .config(['$routeProvider', function($routeProvider) { $routeProvider.when('/view1', { templateUrl: 'view1/view1.html', controller: 'View1Ctrl' }); }]) .controller('View1Ctrl', ['$scope', function($scope) { $scope.score = 0; $scope.lifesCount = 3; }]);

为确保指标正确更新,请修改您的主游戏指令以使用范围变量。

 ... replace : true, scope :{ score: '=score', lifesCount: '=lifesCount' }, template: ...

要测试范围绑定,请将这三行添加到handleComplete()方法的末尾。

 scope.score = 10; scope.lifesCount = 2; scope.$apply();

当您运行应用程序时,您应该会看到分数和寿命指标。

分数和生活指标

页面右侧的额外空白将继续存在,因为在我们的 HTML5 游戏编程教程中,此时我们仍在对游戏的宽度和高度进行硬编码。

调整游戏宽度

AngularJS 包含有用的方法和服务。 其中之一是 $window,它提供了一个 innerWidth 属性,我们将使用它来计算元素的位置。

修改您的app/view1/view1.js以注入$window服务。

 'use strict'; angular.module('myApp.view1', ['ngRoute']) .config(['$routeProvider', function($routeProvider) { $routeProvider.when('/view1', { templateUrl: 'view1/view1.html', controller: 'View1Ctrl' }); }]) .controller('View1Ctrl', ['$scope', '$window', function($scope, $window) { $scope.windowWidth = $window.innerWidth; $scope.gameHeight = 400; $scope.score = 0; $scope.lifesCount = 3; }]);

使用宽度和高度属性扩展主游戏指令,就是这样!

 <sprite-sheet-runner width="windowWidth" height="gameHeight" score="score" lifes-count="lifesCount"> </sprite-sheet-runner>
 ... scope :{ width: '=width', height: '=height', score: '=score', lifesCount: '=lifesCount' }, ... drawGame(); element[0].width = scope.width; element[0].height = scope.height; w = scope.width; h = scope.height; function drawGame() { ...

现在您可以让游戏自行调整到浏览器窗口的宽度。

如果你想将它移植到移动应用程序中,我建议阅读我的其他移动应用程序开发教程,了解使用 Ionic 框架创建移动应用程序。 您应该能够创建一个离子种子应用程序,复制该项目中的所有代码,并在不到一个小时的时间内开始在您的移动设备上玩游戏。

我在这里唯一没有涉及的是碰撞检测。 要了解更多信息,我阅读了这篇文章。

包起来

我相信,通过本游戏开发教程的课程,您意识到 AngularJS 和 CreateJS 是基于 HTML5 的游戏开发的双赢组合。 你已经掌握了所有的基础知识,我相信你已经认识到结合这两个平台的好处。

您可以从 GitHub 下载本文的代码,随意使用、分享和制作您自己的代码。

相关:开发人员犯的 18 个最常见的 AngularJS 错误