个性化阅读
专注于IT技术分析

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

本文概述

游戏开发是更有趣的高级编程技术之一, 它不断挑战软件开发行业。

有许多用于开发游戏的编程平台, 并且有很多设备可以玩这些游戏, 但是, 在网络浏览器中玩游戏时, 基于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的唯一原因, 请考虑使用Adobe 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 Seed项目

如果尚未执行此演示, 则需要先安装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文件夹中。

  • app / assets / spritesheet_grant.png
  • app / assets / ground.png
  • app / assets / hill1.png
  • app / assets / hill2.png
  • app / assets / 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类的实例化(在我们的例子中是sprite), 我们将使用AngularJS工厂。

就像任何其他AngularJS模块一样, Factory在应用程序中注册。要创建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文件。

  • app / 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);
}]);
  • app / 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);
}]);
  • app / 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);
}]);

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

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

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移出指令将指令大小从91行减少了20%至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);
               }
           }
       }
   }]);

相关:srcmini开发人员的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;
}]);

扩展主要游戏指令的width和height属性, 就是这样!

<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错误

赞(0)
未经允许不得转载:srcmini » 制作基于HTML5 Canvas的游戏:使用AngularJS和CreateJS的教程

评论 抢沙发

评论前必须登录!