製作基於 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 錯誤