Erstellen eines HTML5 Canvas-basierten Spiels: Ein Tutorial mit AngularJS und CreateJS

Veröffentlicht: 2022-03-11

Die Spieleentwicklung ist eine der interessanteren, fortschrittlicheren Programmiertechniken, die die Softwareentwicklungsbranche ständig herausfordert.

Es gibt viele Programmierplattformen, die zum Entwickeln von Spielen verwendet werden, und es gibt eine Vielzahl von Geräten, auf denen sie gespielt werden können, aber wenn es darum geht, Spiele in einem Webbrowser zu spielen, ist die Flash-basierte Entwicklung immer noch führend.

Durch das Umschreiben von Flash-basierten Spielen in die HTML5-Canvas-Technologie könnten wir sie auch auf mobilen Browsern spielen. Und mit Apache Cordova könnten erfahrene Webentwickler sie leicht in plattformübergreifende mobile Spiele-Apps packen.

Die Leute bei CreateJS haben sich vorgenommen, dies und mehr zu tun.

EaselJS , Teil der Suite von CreateJS, macht das Zeichnen auf HTML5 Canvas einfach. Stellen Sie sich vor, Sie erstellen benutzerdefinierte Datenvisualisierungen mit hoher Leistung und Tausenden von Elementen. Scalable Vector Graphic (SVG) ist nicht die richtige Wahl, da sie DOM-Elemente verwendet. Browser werden überfordert, wenn bei etwa 600 DOM-Elementen anfängliche Renderings, Neuzeichnungen und Animationen zu teuren Vorgängen werden. Mit HTML5 Canvas können wir diese Probleme leicht umgehen; Leinwandzeichnungen sind wie Tinte auf Papier, keine DOM-Elemente und die damit verbundenen Kosten.

Dies bedeutet, dass die Canvas-basierte Entwicklung mehr Aufmerksamkeit erfordert, wenn es darum geht, Elemente zu trennen und ihnen Ereignisse und Verhaltensweisen zuzuordnen. EaselJS kommt zur Rettung; Wir können codieren, als ob wir es mit einzelnen Elementen zu tun hätten, und die EaselJS-Bibliothek Ihre Mouseovers, Klicks und Kollisionen verarbeiten lassen.

SVG-basierte Codierung hat einen großen Vorteil: SVG hat eine alte Spezifikation, und es gibt viele Design-Tools, die SVG-Assets für die Verwendung in der Entwicklung exportieren, sodass die Zusammenarbeit zwischen Designern und Entwicklern gut funktioniert. Beliebte Bibliotheken wie D3.JS und neuere, leistungsfähigere Bibliotheken wie SnapSVG bringen viel auf den Tisch.

Wenn der Designer-zu-Entwickler-Workflow der einzige Grund ist, warum Sie SVGs verwenden würden, ziehen Sie Erweiterungen für Adobe Illustrator (AI) in Betracht, die Code aus Formen generieren, die in KI erstellt wurden. In unserem Kontext generieren solche Erweiterungen EaselJS-Code oder ProcessingJS-Code, die beide HTML5 Canvas-basierte Bibliotheken sind

Fazit: Wenn Sie ein neues Projekt starten, gibt es keinen Grund mehr, SVGs zu verwenden!

SoundJS ist Teil der CreateJS-Suite; Es bietet eine einfache API für die HTML5-Audiospezifikation.

PreloadJS wird verwendet, um Assets wie Bitmaps, Sounddateien und dergleichen vorab zu laden. Es funktioniert gut in Kombination mit anderen CreateJS-Bibliotheken.

EaselJS, SoundJS und PreloadJS machen die Spieleentwicklung für jeden JavaScript-Ninja super einfach. Seine API-Methoden sind jedem bekannt, der Flash-basierte Spieleentwicklung verwendet hat.

„Das ist alles großartig. Aber was ist, wenn wir ein Entwicklerteam haben, das eine Reihe von Spielen von Flash in HTML5 konvertiert? Ist das mit dieser Suite möglich?“

Die Antwort: „Ja, aber nur, wenn alle Ihre Entwickler auf Jedi-Level sind!“.

Wenn Sie ein Team von Entwicklern mit unterschiedlichen Fähigkeiten haben, was häufig der Fall ist, kann es ein wenig beängstigend sein, CreateJS zu verwenden und einen skalierbaren und modularen Code zu erwarten. Was wäre, wenn wir die CreateJS-Suite mit AngularJS zusammenbringen würden? Können wir dieses Risiko mindern, indem wir das beste und am weitesten verbreitete Front-End-JS-Framework einführen?

Ja , und in diesem HTML5 Canvas-Spieltutorial erfahren Sie, wie Sie mit CreateJS und AngularJS ein einfaches Spiel erstellen!

HTML5 Canvas-Spieltutorial mit CreateJS und AngularJS

Den Samen pflanzen

AngularJS reduziert die Komplexität erheblich, indem es Ihrem Entwicklungsteam Folgendes ermöglicht:

  1. Hinzufügen von Codemodularität, damit sich Teammitglieder auf verschiedene Aspekte des Spiels konzentrieren können.
  2. Aufteilen des Codes in separate testbare und wartbare Teile.
  3. Aktivieren der Wiederverwendung von Code, sodass eine Factory-Klasse mehrmals instanziiert und wiederverwendet werden kann, um andere, aber ähnliche Assets und Verhaltensweisen zu laden.
  4. Beschleunigung der Entwicklung, da mehrere Teammitglieder parallel arbeiten können, ohne sich gegenseitig auf die Füße zu treten.
  5. Schutz von Entwicklern vor der Verwendung von schlechten Mustern (Javascript trägt notorisch schlechte Teile mit sich und JSLint kann uns nur begrenzt helfen).
  6. Hinzufügen eines soliden Testframeworks.

Wenn Sie wie ich ein „Tüftler“ oder ein taktiler Lerner sind, sollten Sie sich den Code von GitHub holen und mit dem Lernen beginnen. Mein Vorschlag ist, meine Check-Ins durchzusehen und die Schritte zu verstehen, die ich unternommen habe, um die Vorteile des Hinzufügens von AngularJS-Güte zu CreateJS-Code zu nutzen.

Ausführen Ihres AngularJS-Seed-Projekts

Falls noch nicht geschehen, müssen Sie nodeJS installieren, bevor Sie diese Demo ausführen können.

Nachdem Sie ein AngularJS-Seed-Projekt erstellt oder von GitHub heruntergeladen haben, führen Sie npm install aus, um alle Abhängigkeiten in Ihren App-Ordner herunterzuladen.

Um Ihre Anwendung auszuführen, führen Sie npm start aus demselben Ordner aus und navigieren Sie in Ihrem Browser zu http://localhost:8000/app/#/view1 . Ihre Seite sollte wie im Bild unten aussehen.

Seite Beispiel

EaselJS trifft auf AngularJS

Fügen Sie Ihrem AngularJS-Seed-Projekt eine CreateJS-Bibliotheksreferenz hinzu. Stellen Sie sicher, dass das CreateJS-Skript nach AngularJS enthalten ist.

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

Bereinigen Sie als Nächstes die Anwendung:

  • Löschen Sie den Ordner view2 aus Ihrem App-Ordner
  • Entfernen Sie Menü- und AngularJS-Versionsinformationen aus index.html, indem Sie den unten gezeigten Code löschen:
 <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>

Entfernen Sie das Modul view2 aus app.js , indem Sie die folgende Zeile löschen

myApp.view2,

Wenn Sie AngularJS noch nie verwendet haben und mit AngularJS-Direktiven nicht vertraut sind, sehen Sie sich dieses Tutorial an. Direktiven in AngularJS sind eine Möglichkeit, HTML einige neue Tricks beizubringen. Sie sind das am besten durchdachte Feature im Framework und machen AngularJS leistungsfähig und erweiterbar.

Wann immer Sie eine spezialisierte DOM-Funktionalität oder eine Komponente benötigen, suchen Sie online danach; Es besteht eine gute Chance, dass es bereits an Orten wie Angular-Modulen verfügbar ist.

Als nächstes müssen wir eine neue AngularJS-Direktive erstellen, die das Beispiel von EaselJS implementiert. Erstellen Sie eine neue Direktive namens spriteSheetRunner in einer neuen Datei, die sich in /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); } } } });

Sobald Ihre Anweisung erstellt ist, fügen Sie der App eine Abhängigkeit hinzu, indem /app/app.js wie folgt aktualisieren:

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

Fügen Sie den Direktivencode in index.html ein, indem Sie einen Verweis auf spriteSheetRunner.js .

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

Wir sind fast fertig! Kopieren Sie die Spiel-Assets in Ihren App-Ordner. Ich habe die Bilder vorbereitet, Sie können sie also gerne herunterladen und in Ihrem App/Assets-Ordner speichern.

  • app/assets/spritesheet_grant.png
  • app/assets/ground.png
  • app/assets/hill1.png
  • app/assets/hill2.png
  • app/assets/sky.png

Fügen Sie als letzten Schritt unsere neu erstellte Direktive zur Seite hinzu. Ändern Sie dazu Ihre Datei app/view/view1.html und machen Sie daraus einen Einzeiler:

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

Starte deine Bewerbung und du bringst deinen Läufer in Bewegung :)

Läufer in Bewegung

Wenn dies Ihre erste AngularJS- oder erste CreateJS-Anwendung ist, feiern Sie, Sie haben gerade etwas wirklich Cooles gemacht!

Vorabladen von Assets in einem Dienst

Dienste in AngularJS sind Singletons, die hauptsächlich zum Teilen des Codes und der Daten verwendet werden. Wir werden einen Dienst verwenden, um die „Spiel-Assets“ über die Anwendung hinweg zu teilen. Weitere Informationen zu AngularJS-Diensten finden Sie in der AngularJS-Dokumentation.

AngularJS-Entwicklungsservices bieten einen effektiven Mechanismus zum Laden und Verwalten aller Assets an einem Ort. Asset-Änderungen werden an jede einzelne Instanz eines Dienstes weitergegeben, wodurch unser Code viel einfacher zu warten ist.

Erstellen Sie eine neue JS-Datei namens loaderSvc.js in Ihrem Ordner /app/view1/services .

 //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 verlangt von uns, dass wir jeden von uns verwendeten Dienst registrieren. Aktualisieren Sie dazu Ihre app.js -Datei so, dass sie einen Verweis auf 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', []);

Aktualisieren Sie Ihren Direktivencode in der Datei app/view1/directives/spriteSheetRunner.js , um den vorab geladenen Code zu entfernen und stattdessen den Dienst zu verwenden.

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

Erstellen einer UI-Elemente-Factory

Die Wiederverwendung und Wiederholung von Sprites in der Spieleentwicklung ist sehr wichtig. Um die Instanziierung von UI-Klassen (in unserem Fall Sprites) zu ermöglichen, verwenden wir AngularJS Factories.

Factory wird wie jedes andere AngularJS-Modul in der Anwendung registriert. Um eine uiClasses-Factory zu erstellen, ändern Sie Ihre app.js-Datei so, dass sie wie folgt aussieht:

 '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', []);

Lassen Sie uns die neue Fabrik nutzen, um Himmel, Hügel, Boden und unseren Läufer zu erschaffen. Erstellen Sie dazu JavaScript-Dateien wie unten aufgeführt.

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

Vergessen Sie nicht, all diese neuen JS-Dateien in Ihre index.html .

Jetzt müssen wir die Spieldirektive aktualisieren.

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

Beachten Sie, dass das Verschieben von uiClasses aus der Direktive die Größe der Direktive um 20 % reduziert hat, von 91 auf 65 Zeilen.

Darüber hinaus können wir für jede Factory-Klasse unabhängig Tests schreiben, um deren Wartung zu vereinfachen.

Hinweis: Das Testen ist ein Thema, das in diesem Beitrag nicht behandelt wird, aber hier ist ein guter Anfang.

Pfeiltasten-Interaktion

An diesem Punkt in unserem HTML5-Canvas-Spiel-Tutorial wird unser Typ mit einem Mausklick oder Tippen auf ein Handy springen, und wir können ihn nicht aufhalten. Lassen Sie uns Pfeiltasten-Steuerelemente hinzufügen:

  • Pfeil nach links (Spiel pausieren)
  • Pfeil nach oben (Sprung)
  • Pfeil nach rechts (beginnen zu laufen)

Erstellen Sie dazu die Funktion keyDown und fügen Sie einen Ereignis-Listener als letzte Zeile der Funktion 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;

Versuchen Sie erneut, Ihr Spiel auszuführen, und überprüfen Sie die Tastatursteuerung.

Lass die Musik spielen

Ohne Musik machen Spiele keinen Spaß, also lasst uns etwas Musik spielen.

Wir müssen zuerst MP3-Dateien zu unserem App/Assets-Ordner hinzufügen. Sie können sie von den unten angegebenen URLs herunterladen.

  • app/assets/jump.mp3
  • app/assets/runningTrack.mp3

Jetzt müssen wir diese Sounddateien mit unserem Ladedienst vorladen. Wir werden loadQueue der PreloaderJS Bibliothek verwenden. Aktualisieren Sie Ihre app/view1/services/loaderSvc.js , um diese Dateien vorab zu laden.

 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/"); }; });

Ändern Sie Ihre Spieldirektive, um Sounds bei Spielereignissen abzuspielen.

 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); } } } }]);
Siehe auch : AngularJS Best Practices und Tipps von Toptal-Entwicklern

Hinzufügen von Score- und Life-Indikatoren

Fügen wir dem HTML5-Canvas-Spiel die Spielstand- und Lebensindikatoren (Herz) hinzu. Die Punktzahl wird als Zahl in der oberen linken Ecke angezeigt, und Herzsymbole in der oberen rechten Ecke zeigen die Anzahl der Lebenspunkte an.

Wir werden eine externe Schriftbibliothek verwenden, um Herzen zu rendern, also fügen Sie die folgende Zeile zum Header Ihrer index.html -Datei hinzu.

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

Die standardmäßige AngularJS-Bindung bietet Echtzeit-Updates. Fügen Sie Ihrer app/view1/view1.html -Datei den folgenden Code hinzu:

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

Um unsere Indikatoren richtig zu positionieren, müssen wir CSS-Klassen für oben links und oben rechts in der Datei app/app.css .

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

Initialisieren Sie die Variablen score und lifesCount im Controller app/view1/view1.js .

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

Um sicherzustellen, dass die Indikatoren richtig aktualisiert werden, ändern Sie Ihre Hauptspieldirektive so, dass sie die Scope-Variablen verwendet.

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

Um die Bereichsbindung zu testen, fügen Sie diese drei Zeilen am Ende der Methode handleComplete() .

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

Wenn Sie die Anwendung ausführen, sollten Sie die Punktzahl- und Lebensanzeige sehen.

Score- und Lebensindikatoren

Zusätzlicher Leerraum rechts auf der Seite wird weiterhin vorhanden sein, da wir die Breite und Höhe des Spiels an dieser Stelle in unserem Programmier-Tutorial für HTML5-Spiele noch fest codieren.

Anpassen der Spielbreite

AngularJS ist vollgepackt mit nützlichen Methoden und Diensten. Eines davon ist $window, das eine innerWidth-Eigenschaft bereitstellt, mit der wir die Position unserer Elemente berechnen.

Ändern Sie Ihre app/view1/view1.js , um den $window -Dienst einzufügen.

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

Erweitern Sie die Hauptspieldirektive mit Breiten- und Höheneigenschaften und das war's!

 <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() { ...

Jetzt passt sich das Spiel an die Breite des Browserfensters an.

Wenn Sie dies in eine mobile App portieren möchten, schlage ich vor, mein anderes Tutorial zur Entwicklung mobiler Apps über die Verwendung des Ionic-Frameworks zum Erstellen mobiler Apps zu lesen. Sie sollten in weniger als einer Stunde in der Lage sein, eine ionische Seed-App zu erstellen, den gesamten Code aus diesem Projekt zu kopieren und das Spiel auf Ihrem Mobilgerät zu spielen.

Das einzige, was ich hier nicht behandle, ist die Kollisionserkennung. Um mehr darüber zu erfahren, habe ich diesen Artikel gelesen.

Einpacken

Ich glaube, dass Sie im Verlauf dieses Spieleentwicklungs-Tutorials erkannt haben, dass AngularJS und CreateJS ein erfolgreiches Duo für die HTML5-basierte Spieleentwicklung sind. Sie haben alle Grundlagen und ich bin sicher, dass Sie die Vorteile der Kombination dieser beiden Plattformen erkannt haben.

Sie können den Code für diesen Artikel von GitHub herunterladen, verwenden, teilen und zu Ihrem eigenen machen.

Siehe auch: Die 18 häufigsten AngularJS-Fehler, die Entwickler machen