HTML5キャンバスベースのゲームの作成:AngularJSとCreateJSを使用したチュートリアル

公開: 2022-03-11

ゲーム開発は、ソフトウェア開発業界に絶えず挑戦している、より興味深い高度なプログラミング手法の1つです。

ゲームの開発に使用されるプログラミングプラットフォームは数多くあり、それらをプレイするためのデバイスは多数ありますが、Webブラウザーでゲームをプレイする場合、Flashベースの開発が依然として先導しています。

FlashベースのゲームをHTML5Canvasテクノロジーに書き直すと、モバイルブラウザーでもプレイできるようになります。 また、Apache Cordovaを使用すると、熟練したWeb開発者はそれらをクロスプラットフォームのモバイルゲームアプリに簡単にラップできます。

CreateJSの人々は、それ以上のことを始めました。

CreateJSスイートの一部であるEaselJSを使用すると、HTML5Canvasでの描画が簡単になります。 高性能と数千の要素を備えたカスタムデータの視覚化を構築することを想像してみてください。 スケーラブルベクターグラフィック(SVG)は、DOM要素を使用するため、適切な選択ではありません。 約600のDOM要素で、初期レンダリング、再描画、およびアニメーションが高価な操作になると、ブラウザーは圧倒されます。 HTML5 Canvasを使用すると、これらの問題を簡単に回避できます。 キャンバスの描画は紙の上のインクのようなものであり、DOM要素とそれに関連するコストはありません。

つまり、Canvasベースの開発では、要素を分離し、それらにイベントと動作を付加する場合に、より注意を払う必要があります。 EaselJSが助けになります。 個々の要素を処理しているかのようにコーディングして、EaselJSライブラリにマウスオーバー、クリック、および衝突を処理させることができます。

SVGベースのコーディングには1つの大きな利点があります。SVGには古い仕様があり、開発で使用するためにSVGアセットをエクスポートする設計ツールがたくさんあるため、設計者と開発者の協力がうまく機能します。 D3.JSなどの人気のあるライブラリ、およびSnapSVGなどの新しい、より強力なライブラリは、多くのことを実現します。

デザイナーから開発者へのワークフローがSVGを使用する唯一の理由である場合は、AIで作成された形状からコードを生成するAdobe Illustrator(AI)の拡張機能を検討してください。 私たちのコンテキストでは、このような拡張機能はEaselJSコードまたはProcessingJSコードを生成します。どちらもHTML5Canvasベースのライブラリです。

結論として、新しいプロジェクトを開始する場合、SVGを使用する理由はもうありません。

SoundJSはCreateJSスイートの一部です。 HTML5オーディオ仕様のシンプルなAPIを提供します。

PreloadJSは、ビットマップやサウンドファイルなどのアセットをプリロードするために使用されます。 他のCreateJSライブラリと組み合わせて使用​​するとうまく機能します。

EaselJS、SoundJS、およびPreloadJSを使用すると、JavaScriptの忍者にとってゲーム開発が非常に簡単になります。 そのAPIメソッドは、Flashベースのゲーム開発を使用した人なら誰でもよく知っています。

「これはすべて素晴らしいことです。 しかし、一連のゲームをFlashからHTML5に変換する開発者のチームがある場合はどうなるでしょうか。 このスイートでそれを行うことは可能ですか?」

答え: 「はい。ただし、すべての開発者がJediレベルである場合に限ります!」

さまざまなスキルセット開発者のチームがいる場合(これはよくあることですが)、CreateJSを使用して、スケーラブルでモジュール式のコードを期待するのは少し怖いかもしれません。 CreateJSスイートとAngularJSを統合するとどうなりますか? 最良で最も採用されているフロントエンドJSフレームワークを導入することで、このリスクを軽減できますか?

はい。このHTML5Canvasゲームのチュートリアルでは、CreateJSとAngularJSを使用して基本的なゲームを作成する方法を説明します。

CreateJSとAngularJSを使用したHTML5Canvasゲームチュートリアル

種を蒔く

AngularJSは、開発チームが次のことを行えるようにすることで、複雑さを大幅に軽減します。

  1. チームメンバーがゲームのさまざまな側面に集中できるように、コードのモジュール性を追加します。
  2. コードをテスト可能で保守可能な個別の部分に分割します。
  3. コードの再利用を有効にして、1つのファクトリクラスを複数回インスタンス化し、異なるが類似したアセットと動作をロードするために再利用できるようにします。
  4. 複数のチームメンバーがお互いの足を踏むことなく並行して作業できるため、開発をスピードアップします。
  5. 開発者を悪いパターンの使用から保護します(Javascriptは悪名高い悪い部分を持っており、JSLintは私たちに非常に役立つだけです)。
  6. 堅実なテストフレームワークを追加します。

私のように、あなたが「いじくり回す」または触覚的な学習者である場合は、GitHubからコードを取得して、学習を開始する必要があります。 私の提案は、チェックインを調べて、CreateJSコードにAngularJSの良さを追加することの利点を得るために私が取った手順を理解することです。

AngularJSシードプロジェクトの実行

まだ行っていない場合は、このデモを実行する前にnodeJSをインストールする必要があります。

AngularJSシードプロジェクトを作成するか、GitHubからダウンロードした後、 npm installを実行して、すべての依存関係をアプリフォルダーにダウンロードします。

アプリケーションを実行するには、同じフォルダーからnpm startを実行し、ブラウザーでhttp://localhost:8000/app/#/view1に移動します。 ページは下の画像のようになります。

ページの例

EaselJSとAngularJSの出会い

CreateJSライブラリ参照をAngularJSシードプロジェクトに追加します。 CreateJSスクリプトがAngularJSの後に含まれていることを確認してください。

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

次の行を削除して、 view2からapp.jsモジュールを削除します

myApp.view2,

これまでAngularJSを使用したことがなく、AngularJSディレクティブに精通していない場合は、このチュートリアルを確認してください。 AngularJSのディレクティブは、HTMLにいくつかの新しいトリックを教える方法です。 これらはフレームワークで最もよく考えられた機能であり、AngularJSを強力で拡張可能にします。

特殊なDOM機能またはコンポーネントが必要な場合は、オンラインで検索してください。 Angularモジュールのような場所ですでに利用可能である可能性が高いです。

次に行う必要があるのは、EaselJSの例を実装する新しいAngularJSディレクティブを作成することです。 /app/view1/directives/spriteSheetRunner.jsにある新しいファイルに/app/view1/directives/spriteSheetRunner.jsという新しいディレクティブを作成します。

 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
  • 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開発サービスは、すべてのアセットを1か所でロードおよび管理するための効果的なメカニズムを提供します。 アセットの変更はサービスの個々のインスタンスに伝播されるため、コードの保守がはるかに簡単になります。

/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ファクトリを使用します。

Factoryは、他の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ファイルを作成します。

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

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をディレクティブから移動すると、ディレクティブのサイズが91行から65行に20%減少したことに注意してください。

さらに、ファクトリクラスごとに個別にテストを記述して、メンテナンスを簡素化できます。

注:テストはこの投稿では取り上げられていないトピックですが、ここから始めることをお勧めします。

矢印キーの相互作用

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からダウンロードできます。

  • app / Assets / jump.mp3
  • app / Assets / 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のベストプラクティスとヒント

スコアとライフインジケーターの追加

ゲームのスコアとライフ(ハート)インジケーターをHTML5Canvasゲームに追加しましょう。 スコアは左上隅に数字で表示され、右上隅にあるハート記号はライフカウントを示します。

ハートをレンダリングするために外部フォントライブラリを使用するので、 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()メソッドの最後にこれらの3行を追加します。

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

アプリケーションを実行すると、スコアと寿命のインジケーターが表示されます。

スコアとライフインジケーター

HTML5ゲームプログラミングチュートリアルのこの時点ではまだゲームの幅と高さをハードコーディングしているため、ページの右側に追加の空白が引き続き表示されます。

ゲーム幅の調整

AngularJSには便利なメソッドとサービスが満載です。 それらの1つは$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フレームワークを使用したモバイルアプリの作成に関する他のモバイルアプリ開発チュートリアルを読むことをお勧めします。 イオンシードアプリを作成し、このプロジェクトからすべてのコードをコピーして、1時間以内にモバイルデバイスでゲームを開始できるはずです。

ここで取り上げていないのは、衝突検出だけです。 それについてもっと学ぶために、私はこの記事を読みました。

要約

このゲーム開発チュートリアルの過程で、AngularJSとCreateJSがHTML5ベースのゲーム開発の勝利のデュオであることに気づいたと思います。 あなたはすべての基本を持っており、これら2つのプラットフォームを組み合わせることの利点を認識していると確信しています。

この記事のコードはGitHubからダウンロードして、自由に使用、共有、および独自のものにすることができます。

関連:開発者が犯す最も一般的なAngularJSの間違いトップ18