Tworzenie gry opartej na HTML5 Canvas: samouczek za pomocą AngularJS i CreateJS
Opublikowany: 2022-03-11Tworzenie gier to jedna z ciekawszych, bardziej zaawansowanych technik programowania, która nieustannie stawia wyzwania branży programistycznej.
Istnieje wiele platform programistycznych używanych do tworzenia gier i istnieje mnóstwo urządzeń, na których można w nie grać, ale jeśli chodzi o granie w gry w przeglądarce internetowej, rozwój oparty na Flash nadal prowadzi.
Przepisanie gier opartych na Flashu do technologii HTML5 Canvas pozwoliłoby nam grać również w przeglądarkach mobilnych. A dzięki Apache Cordova wykwalifikowani twórcy stron internetowych mogą z łatwością umieścić je w wieloplatformowych aplikacjach do gier mobilnych.
Ludzie z CreateJS postanowili to zrobić i nie tylko.
EaselJS , część pakietu CreateJS, ułatwia rysowanie w HTML5 Canvas. Wyobraź sobie tworzenie niestandardowej wizualizacji danych o wysokiej wydajności i tysiącach elementów. Skalowalna grafika wektorowa (SVG) nie jest właściwym wyborem, ponieważ wykorzystuje elementy DOM. Przeglądarki stają się przytłoczone, gdy przy około 600 elementach DOM początkowe renderowanie, ponowne rysowanie i animacja stają się kosztownymi operacjami. Dzięki HTML5 Canvas możemy łatwo obejść te problemy; Rysunki na płótnie są jak tusz na papierze, bez elementów DOM i związanych z nimi kosztów.
Oznacza to, że rozwój oparty na płótnie wymaga większej uwagi, jeśli chodzi o oddzielanie elementów i dołączanie do nich zdarzeń i zachowań. Na ratunek przychodzi EaselJS; możemy kodować tak, jakbyśmy mieli do czynienia z pojedynczymi elementami, pozwalając bibliotece EaselJS obsłużyć najazdy, kliknięcia i kolizje.
Kodowanie oparte na SVG ma jedną dużą zaletę: SVG ma starą specyfikację i istnieje wiele narzędzi projektowych, które eksportują zasoby SVG do wykorzystania w rozwoju, dzięki czemu współpraca między projektantami i programistami działa dobrze. Popularne biblioteki, takie jak D3.JS, i nowsze, potężniejsze biblioteki, takie jak SnapSVG, wnoszą wiele do tabeli.
Jeśli przepływ pracy od projektanta do programisty jest jedynym powodem, dla którego chciałbyś używać SVG, rozważ rozszerzenia dla programu Adobe Illustrator (AI), które generują kod z kształtów utworzonych w AI. W naszym kontekście takie rozszerzenia generują kod EaselJS lub kod ProcessingJS, które są bibliotekami opartymi na HTML5 Canvas
Podsumowując, jeśli zaczynasz nowy projekt, nie ma już powodu, aby używać SVG!
SoundJS jest częścią pakietu CreateJS; zapewnia prosty interfejs API dla specyfikacji audio HTML5.
PreloadJS służy do wstępnego ładowania zasobów, takich jak mapy bitowe, pliki dźwiękowe i tym podobne. Działa dobrze w połączeniu z innymi bibliotekami CreateJS.
EaselJS, SoundJS i PreloadJS sprawiają, że tworzenie gier jest bardzo łatwe dla każdego ninja JavaScriptu. Jego metody API są znane każdemu, kto korzystał z tworzenia gier w technologii Flash.
„To wszystko świetnie. Ale co, jeśli mamy zespół programistów konwertujących kilka gier z Flasha na HTML5? Czy można to zrobić z tym apartamentem?
Odpowiedź: „Tak, ale tylko wtedy, gdy wszyscy twoi programiści są na poziomie Jedi!”.
Jeśli masz zespół programistów o różnych umiejętnościach, co często ma miejsce, używanie CreateJS i oczekiwanie skalowalnego i modułowego kodu może być trochę przerażające. Co jeśli połączymy pakiet CreateJS z AngularJS? Czy możemy ograniczyć to ryzyko, wprowadzając najlepszy i najczęściej stosowany front-endowy framework JS?
Tak , a ten samouczek gry HTML5 Canvas nauczy Cię, jak stworzyć podstawową grę za pomocą CreateJS i AngularJS!
Sadzenie Ziarna
AngularJS znacznie zmniejsza złożoność, umożliwiając zespołowi programistycznemu:
- Dodanie modułowości kodu, aby członkowie zespołu mogli skupić się na różnych aspektach gry.
- Dzielenie kodu na oddzielne fragmenty, które można testować i konserwować.
- Umożliwienie ponownego użycia kodu, dzięki czemu jedna klasa fabryczna może być wielokrotnie tworzona i ponownie używana do ładowania różnych, ale podobnych zasobów i zachowań.
- Przyspieszenie rozwoju, ponieważ wielu członków zespołu może pracować równolegle, bez deptania sobie nawzajem.
- Ochrona programistów przed używaniem złych wzorców (Javascript niesie ze sobą notorycznie złe części, a JSLint może nam tylko bardzo pomóc).
- Dodanie solidnej platformy testowej.
Jeśli tak jak ja jesteś „majsterkowiczem” lub uczniem dotykowym, powinieneś pobrać kod z GitHub i zacząć się uczyć. Proponuję przejrzeć moje check-iny i zrozumieć kroki, które podjąłem, aby uzyskać korzyści z dodania dobroci AngularJS do kodu CreateJS.
Uruchamianie projektu AngularJS Seed
Jeśli jeszcze tego nie zrobiłeś, musisz zainstalować nodeJS, zanim będziesz mógł uruchomić to demo.
Po utworzeniu projektu źródłowego AngularJS lub pobraniu go z GitHub uruchom npm install
, aby pobrać wszystkie zależności do folderu aplikacji.
Aby uruchomić aplikację, uruchom npm start
z tego samego folderu i przejdź do http://localhost:8000/app/#/view1
w przeglądarce. Twoja strona powinna wyglądać jak na poniższym obrazku.
EaselJS spotyka AngularJS
Dodaj odwołanie do biblioteki CreateJS do projektu źródłowego AngularJS. Upewnij się, że skrypt CreateJS jest dołączony po AngularJS.
<script src="http://code.createjs.com/createjs-2014.12.12.min.js"></script>
Następnie wyczyść aplikację:
- Usuń folder view2 z folderu aplikacji
- Usuń menu i informacje o wersji AngularJS z index.html, usuwając poniższy kod:
<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>
Usuń moduł view2
z app.js
, usuwając następujący wiersz
myApp.view2,
Jeśli nie używałeś wcześniej AngularJS i nie znasz dyrektyw AngularJS, sprawdź ten samouczek. Dyrektywy w AngularJS są sposobem na nauczenie HTML nowych sztuczek. Są one najlepiej przemyślaną funkcją we frameworku i sprawiają, że AngularJS jest potężny i rozszerzalny.
Zawsze, gdy potrzebujesz specjalistycznej funkcjonalności DOM lub komponentu, wyszukaj je online; istnieje duża szansa, że jest już dostępny w miejscach takich jak moduły Angular.
Następną rzeczą, którą musimy zrobić, to stworzyć nową dyrektywę AngularJS, która zaimplementuje przykład z EaselJS. Utwórz nową dyrektywę o nazwie spriteSheetRunner w nowym pliku znajdującym się w /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); } } } });
Po utworzeniu dyrektywy dodaj zależność do aplikacji, aktualizując /app/app.js
jak poniżej:
'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'}); }]);
Uwzględnij kod dyrektywy w index.html
, dodając odwołanie do spriteSheetRunner.js
.
<script src="view1/directives/spriteSheetRunner.js"></script>
Jesteśmy prawie gotowi! Skopiuj zasoby gry do folderu aplikacji. Przygotowałem obrazy, więc możesz je pobrać i zapisać w folderze app/assets.
- app/assets/spritesheet_grant.png
- app/assets/ground.png
- app/assets/hill1.png
- app/assets/hill2.png
- app/assets/sky.png
Na koniec dodaj naszą nowo utworzoną dyrektywę do strony. Aby to zrobić, zmień plik app/view/view1.html
go w jednym wierszu:
<sprite-sheet-runner></sprite-sheet-runner>
Rozpocznij aplikację, a wprawisz swojego biegacza w ruch :)
Jeśli jest to Twoja pierwsza aplikacja AngularJS lub pierwsza aplikacja CreateJS, świętuj, właśnie stworzyłeś coś naprawdę fajnego!
Wstępne ładowanie zasobów w usłudze
Usługi w AngularJS to singletony używane głównie do udostępniania kodu i danych. Wykorzystamy usługę do udostępniania „zasobów gry” w całej aplikacji. Aby dowiedzieć się więcej o usługach AngularJS sprawdź dokumentację AngularJS.
Usługi programistyczne AngularJS zapewniają skuteczny mechanizm ładowania i zarządzania wszystkimi zasobami w jednym miejscu. Zmiany zasobów są propagowane do każdego pojedynczego wystąpienia usługi, dzięki czemu nasz kod jest znacznie łatwiejszy w utrzymaniu.
Utwórz nowy plik JS o nazwie loaderSvc.js
w folderze /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 wymaga od nas zarejestrowania każdej usługi, z której korzystamy. Aby to zrobić, zaktualizuj plik app.js
, aby zawierał odwołanie do 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', []);
Zaktualizuj kod dyrektywy w pliku app/view1/directives/spriteSheetRunner.js
, aby usunąć kod wstępnego ładowania i zamiast tego korzystać z usługi.
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); } } } }]);
Tworzenie Fabryki Elementów UI
Ponowne używanie i powtarzanie sprite'ów w tworzeniu gier jest bardzo ważne. Aby umożliwić tworzenie instancji klas UI (które w naszym przypadku są sprite'ami) wykorzystamy Fabryki AngularJS.
Fabryka jest zarejestrowana w aplikacji tak jak każdy inny moduł AngularJS. Aby utworzyć fabrykę uiClasses, zmodyfikuj plik app.js, aby wyglądał tak:
'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', []);
Wykorzystajmy nową fabrykę do stworzenia nieba, wzgórza, ziemi i naszego biegacza. Aby to zrobić, utwórz pliki JavaScript, jak podano poniżej.

- 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); }]);
Nie zapomnij dodać tych wszystkich nowych plików JS w swoim index.html
.
Teraz musimy zaktualizować dyrektywę gry.
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); } } } }]);
Zwróć uwagę, że przeniesienie uiClasses
z dyrektywy zmniejszyło rozmiar dyrektywy o 20%, z 91 do 65 wierszy.
Dodatkowo możemy samodzielnie pisać testy dla każdej klasy fabrycznej, aby uprościć jej utrzymanie.
Uwaga: Testowanie to temat, który nie został omówiony w tym poście, ale tutaj jest dobrym miejscem do rozpoczęcia.
Interakcja klawiszy strzałek
W tym momencie naszego samouczka dotyczącego gry HTML5 Canvas, kliknięcie myszą lub dotknięcie telefonu komórkowego sprawi, że nasz facet podskoczy, a my nie możemy go powstrzymać. Dodajmy kontrolki klawiszy strzałek:
- Strzałka w lewo (pauza gry)
- Strzałka w górę (skok)
- Strzałka w prawo (rozpocznij bieg)
Aby to zrobić, utwórz funkcję keyDown
i dodaj detektor zdarzeń jako ostatni wiersz funkcji 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;
Spróbuj ponownie uruchomić grę i sprawdź sterowanie na klawiaturze.
Niech gra muzyka
Gry nie są zabawne bez muzyki, więc zagrajmy trochę muzyki.
Najpierw musimy dodać pliki MP3 do naszego folderu app/assets. Możesz je pobrać z adresów URL podanych poniżej.
- app/assets/jump.mp3
- app/assets/runningTrack.mp3
Teraz musimy wstępnie załadować te pliki dźwiękowe za pomocą naszej usługi ładującej. Użyjemy loadQueue
biblioteki PreloaderJS
. Zaktualizuj app/view1/services/loaderSvc.js
, aby wstępnie wczytać te pliki.
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/"); }; });
Zmodyfikuj dyrektywę gry, aby odtwarzać dźwięki podczas wydarzeń w grze.
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); } } } }]);
Dodawanie wskaźników wyniku i życia
Dodajmy wynik gry i wskaźniki życia (serca) do gry HTML5 Canvas. Wynik zostanie pokazany jako liczba w lewym górnym rogu, a symbole serca w prawym górnym rogu wskażą liczbę życia.
Do renderowania serc użyjemy zewnętrznej biblioteki czcionek, więc dodaj następujący wiersz do nagłówka pliku index.html
.
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
Standardowe wiązanie AngularJS zapewni aktualizacje w czasie rzeczywistym. Dodaj następujący kod do pliku 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>
Aby właściwie ustawić nasze wskaźniki, musimy dodać klasy CSS dla lewego górnego i prawego górnego rogu w pliku app/app.css
.
.top-left { position: absolute; left: 30px; top: 10px; } .top-right { position: absolute; right: 100px; top: 10px; float: right; }
Zainicjuj zmienne score i lifesCount
w kontrolerze 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; }]);
Aby upewnić się, że wskaźniki są prawidłowo aktualizowane, zmodyfikuj główną dyrektywę gry tak, aby używała zmiennych zakresu.
... replace : true, scope :{ score: '=score', lifesCount: '=lifesCount' }, template: ...
Aby przetestować wiązanie zakresu, dodaj te trzy wiersze na końcu metody handleComplete()
.
scope.score = 10; scope.lifesCount = 2; scope.$apply();
Po uruchomieniu aplikacji powinieneś zobaczyć wskaźniki wyniku i życia.
Dodatkowa biała przestrzeń po prawej stronie będzie nadal widoczna, ponieważ na tym etapie naszego samouczka programowania gier w HTML5 wciąż na stałe ustalamy szerokość i wysokość gry.
Dostosowywanie szerokości gry
AngularJS jest pełen przydatnych metod i usług. Jednym z nich jest $window, które udostępnia właściwość innerWidth, której użyjemy do obliczenia pozycji naszych elementów.
Zmodyfikuj app/view1/view1.js
, aby wstrzyknąć usługę $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; }]);
Rozszerz główną dyrektywę gry o właściwości szerokości i wysokości i to wszystko!
<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() { ...
Teraz gra dopasowuje się do szerokości okna przeglądarki.
Jeśli chcesz przenieść to do aplikacji mobilnej, sugeruję przeczytanie mojego innego samouczka dotyczącego tworzenia aplikacji mobilnych na temat używania frameworka Ionic do tworzenia aplikacji mobilnych. Powinieneś być w stanie stworzyć aplikację ionic seed, skopiować cały kod z tego projektu i zacząć grać w grę na swoim urządzeniu mobilnym w mniej niż godzinę.
Jedyne, czego tu nie poruszam, to wykrywanie kolizji. Aby dowiedzieć się więcej na ten temat, przeczytałem ten artykuł.
Zakończyć
Wierzę, że w trakcie tego samouczka dotyczącego tworzenia gier zdałeś sobie sprawę, że AngularJS i CreateJS to zwycięski duet w tworzeniu gier opartych na HTML5. Znasz wszystkie podstawy i jestem pewien, że dostrzegłeś korzyści płynące z połączenia tych dwóch platform.
Możesz pobrać kod tego artykułu z GitHub, możesz swobodnie używać, udostępniać i dostosowywać go do własnych potrzeb.