Realizzazione di un gioco basato su tela HTML5: un tutorial che utilizza AngularJS e CreateJS
Pubblicato: 2022-03-11Lo sviluppo di giochi è una delle tecniche di programmazione più interessanti e avanzate che sfida costantemente l'industria dello sviluppo software.
Esistono molte piattaforme di programmazione utilizzate per sviluppare giochi e ci sono una miriade di dispositivi su cui giocarci, ma quando si tratta di giocare in un browser web, lo sviluppo basato su Flash è ancora all'avanguardia.
La riscrittura di giochi basati su Flash sulla tecnologia HTML5 Canvas ci permetterebbe di riprodurli anche su browser mobili. E, con Apache Cordova, abili sviluppatori web potrebbero facilmente avvolgerli in app di giochi mobili multipiattaforma.
La gente di CreateJS ha deciso di fare questo e altro.
EaselJS , parte della suite di CreateJS, semplifica il disegno su HTML5 Canvas. Immagina di creare una visualizzazione dei dati personalizzata con prestazioni elevate e migliaia di elementi. Scalable Vector Graphic (SVG) non è la scelta giusta, perché utilizza elementi DOM. I browser vengono sopraffatti quando, a circa 600 elementi DOM, rendering iniziali, ridisegni e animazioni diventano operazioni costose. Con HTML5 Canvas, possiamo facilmente aggirare questi problemi; I disegni su tela sono come inchiostro su carta, nessun elemento DOM e i relativi costi.
Ciò significa che lo sviluppo basato su Canvas necessita di maggiore attenzione quando si tratta di separare gli elementi e di associare eventi e comportamenti ad essi. EaselJS viene in soccorso; possiamo programmare come se avessimo a che fare con singoli elementi, lasciando che la libreria EaselJS gestisca i passaggi del mouse, i clic e le collisioni.
La codifica basata su SVG ha un grande vantaggio: SVG ha una vecchia specifica e ci sono molti strumenti di progettazione che esportano risorse SVG per l'uso nello sviluppo, in modo che la cooperazione tra designer e sviluppatori funzioni bene. Le librerie popolari, come D3.JS, e le librerie più recenti e più potenti come SnapSVG, portano molto in tavola.
Se il flusso di lavoro da designer a sviluppatore è l'unico motivo per cui dovresti utilizzare SVG, prendi in considerazione le estensioni per Adobe Illustrator (AI) che generano codice da forme create in AI. Nel nostro contesto, tali estensioni generano codice EaselJS o codice ProcessingJS, entrambi librerie basate su HTML5 Canvas
In conclusione, se stai iniziando un nuovo progetto, non c'è più motivo di usare SVG!
SoundJS fa parte della suite CreateJS; fornisce una semplice API per la specifica audio HTML5.
PreloadJS viene utilizzato per precaricare risorse come bitmap, file audio e simili. Funziona bene in combinazione con altre librerie CreateJS.
EaselJS, SoundJS e PreloadJS rendono lo sviluppo del gioco semplicissimo per qualsiasi ninja JavaScript. I suoi metodi API sono familiari a chiunque abbia utilizzato lo sviluppo di giochi basato su Flash.
“È tutto fantastico. Ma cosa succede se abbiamo un team di sviluppatori che converte un sacco di giochi da Flash a HTML5? È possibile farlo con questa suite?"
La risposta: "Sì, ma solo se tutti i tuoi sviluppatori sono a livello Jedi!".
Se si dispone di un team di sviluppatori di competenze diverse, come spesso accade, può essere un po' spaventoso usare CreateJS e aspettarsi un codice scalabile e modulare. E se riunissimo la suite CreateJS con AngularJS? Possiamo mitigare questo rischio introducendo il framework JS front-end migliore e più adottato?
Sì , e questo tutorial di gioco HTML5 Canvas ti insegnerà come creare un gioco di base con CreateJS e AngularJS!
Piantare il seme
AngularJS riduce notevolmente la complessità abilitando il tuo team di sviluppo con quanto segue:
- Aggiunta della modularità del codice, in modo che i membri del team possano concentrarsi su diversi aspetti del gioco.
- Rompere il codice in parti separate testabili e manutenibili.
- Abilitazione del riutilizzo del codice, in modo che una classe factory possa essere istanziata più volte e riutilizzata per caricare asset e comportamenti diversi ma simili.
- Accelerare lo sviluppo perché più membri del team possono lavorare in parallelo, senza calpestarsi a vicenda.
- Proteggere gli sviluppatori dall'uso di schemi errati (Javascript porta con sé parti notoriamente cattive e JSLint può solo aiutarci così tanto).
- Aggiunta di una solida struttura di test.
Se, come me, sei un "armeggiatore" o uno studente tattile, dovresti ottenere il codice da GitHub e iniziare ad imparare. Il mio suggerimento è di esaminare i miei check-in e comprendere i passaggi che ho adottato per ottenere i vantaggi dell'aggiunta della bontà di AngularJS al codice CreateJS.
Esecuzione del tuo progetto seme AngularJS
Se non l'hai già fatto, devi installare nodeJS prima di poter eseguire questa demo.
Dopo aver creato un progetto seme AngularJS o averlo scaricato da GitHub, esegui npm install
per scaricare tutte le dipendenze nella cartella dell'app.
Per eseguire la tua applicazione, esegui npm start
dalla stessa cartella e vai a http://localhost:8000/app/#/view1
nel tuo browser. La tua pagina dovrebbe assomigliare all'immagine qui sotto.
EaselJS incontra AngularJS
Aggiungi il riferimento alla libreria CreateJS al tuo progetto seme AngularJS. Assicurati che lo script CreateJS sia incluso dopo AngularJS.
<script src="http://code.createjs.com/createjs-2014.12.12.min.js"></script>
Quindi, ripulisci l'applicazione:
- Elimina la cartella view2 dalla cartella dell'app
- Rimuovi il menu e le informazioni sulla versione di AngularJS da index.html, eliminando il codice mostrato di seguito:
<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>
Rimuovi il modulo view2
da app.js
, eliminando la riga seguente
myApp.view2,
Se non hai mai usato AngularJS prima e non hai familiarità con le direttive AngularJS, controlla questo tutorial. Le direttive in AngularJS sono un modo per insegnare all'HTML alcuni nuovi trucchi. Sono la funzionalità più ben congegnata nel framework e rendono AngularJS potente ed estensibile.
Ogni volta che hai bisogno di una funzionalità DOM specializzata o di un componente, cercalo online; ci sono buone probabilità che sia già disponibile in posti come i moduli Angular.
La prossima cosa che dobbiamo fare è creare una nuova direttiva AngularJS che implementerà l'esempio di EaselJS. Crea una nuova direttiva chiamata spriteSheetRunner in un nuovo file che si trova 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); } } } });
Una volta creata la direttiva, aggiungi una dipendenza all'app aggiornando /app/app.js
come di seguito:
'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'}); }]);
Includere il codice della direttiva in index.html
aggiungendo un riferimento a spriteSheetRunner.js
.
<script src="view1/directives/spriteSheetRunner.js"></script>
Siamo quasi pronti! Copia le risorse di gioco nella cartella dell'app. Ho preparato le immagini, quindi sentiti libero di scaricarle e salvarle nella tua cartella app/risorse.
- app/assets/spritesheet_grant.png
- app/assets/ground.png
- app/assets/hill1.png
- app/assets/hill2.png
- app/assets/sky.png
Come passaggio finale, aggiungi la nostra direttiva appena creata alla pagina. Per farlo, cambia il tuo file app/view/view1.html
una riga:
<sprite-sheet-runner></sprite-sheet-runner>
Avvia la tua applicazione e metterai in moto il tuo corridore :)
Se questa è la tua prima applicazione AngularJS o CreateJS, festeggia, hai appena realizzato qualcosa di veramente interessante!
Precaricamento di asset in un servizio
I servizi in AngularJS sono singleton utilizzati principalmente per condividere il codice e i dati. Utilizzeremo un servizio per condividere le "risorse di gioco" nell'applicazione. Per saperne di più sui servizi AngularJS, controlla la documentazione di AngularJS.
I servizi di sviluppo AngularJS forniscono un meccanismo efficace per caricare e gestire tutte le risorse in un'unica posizione. Le modifiche alle risorse vengono propagate a ogni singola istanza di un servizio, rendendo il nostro codice molto più semplice da mantenere.
Crea un nuovo file JS chiamato loaderSvc.js
nella tua cartella /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 ci richiede di registrare qualsiasi servizio che stiamo utilizzando. Per fare ciò, aggiorna il tuo file app.js
per includere il riferimento a 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', []);
Aggiorna il codice della direttiva, nel file app/view1/directives/spriteSheetRunner.js
, per rimuovere il codice di precaricamento e utilizzare invece il servizio.
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); } } } }]);
Creazione di UI Elements Factory
Riutilizzare e ripetere gli sprite nello sviluppo del gioco è molto importante. Per abilitare l'istanziazione delle classi dell'interfaccia utente (che nel nostro caso sono sprite) utilizzeremo AngularJS Factories.
Factory è registrato nell'applicazione proprio come qualsiasi altro modulo AngularJS. Per creare uiClasses factory, modifica il tuo file app.js in modo che assomigli a questo:
'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', []);
Usiamo la nuova fabbrica per creare cielo, collina, terra e il nostro corridore. Per fare ciò, crea i file JavaScript come elencato di seguito.

- 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); }]);
Non dimenticare di aggiungere tutti questi nuovi file JS nel tuo index.html
.
Ora, dobbiamo aggiornare la direttiva sul gioco.
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); } } } }]);
Si noti che l'eliminazione di uiClasses
dalla direttiva ha ridotto la dimensione della direttiva del 20%, da 91 a 65 righe.
Inoltre, possiamo scrivere in modo indipendente dei test per ogni classe di fabbrica per semplificarne la manutenzione.
Nota: i test sono un argomento che non è trattato in questo post, ma qui è un buon punto di partenza.
Interazione tasti freccia
A questo punto nel nostro tutorial di gioco HTML5 Canvas, fare clic con il mouse o toccare un cellulare farà sobbalzare il nostro ragazzo e non possiamo fermarlo. Aggiungiamo i controlli dei tasti freccia:
- Freccia sinistra (metti in pausa il gioco)
- Freccia su (salto)
- Freccia destra (inizia a correre)
Per fare ciò, crea la funzione keyDown
e aggiungi un listener di eventi come ultima riga della funzione 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;
Prova a eseguire di nuovo il gioco e controlla i controlli della tastiera.
Lascia che la musica suoni
I giochi non sono divertenti senza musica, quindi riproduciamo un po' di musica.
Dovremo prima aggiungere file MP3 alla nostra cartella app/risorse. Puoi scaricarli dagli URL forniti di seguito.
- app/assets/jump.mp3
- app/assets/runningTrack.mp3
Ora, dobbiamo precaricare questi file audio utilizzando il nostro servizio di caricamento. Useremo loadQueue
della libreria PreloaderJS
. Aggiorna la tua app/view1/services/loaderSvc.js
per precaricare questi file.
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/"); }; });
Modifica la tua direttiva di gioco per riprodurre suoni negli eventi di gioco.
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); } } } }]);
Aggiunta di indicatori di punteggio e di vita
Aggiungiamo il punteggio del gioco e gli indicatori di vita (cuore) al gioco HTML5 Canvas. Il punteggio verrà mostrato come un numero nell'angolo in alto a sinistra e i simboli del cuore, nell'angolo in alto a destra, indicheranno il conteggio dei punti vita.
Useremo una libreria di font esterna per il rendering dei cuori, quindi aggiungi la seguente riga all'intestazione del tuo file index.html
.
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
L'associazione AngularJS standard fornirà aggiornamenti in tempo reale. Aggiungi il codice seguente al file 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>
Per posizionare correttamente i nostri indicatori, dobbiamo aggiungere classi CSS per in alto a sinistra e in alto a destra nel file app/app.css
.
.top-left { position: absolute; left: 30px; top: 10px; } .top-right { position: absolute; right: 100px; top: 10px; float: right; }
Inizializza le variabili punteggio e lifesCount
nel 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; }]);
Per assicurarti che gli indicatori siano aggiornati correttamente, modifica la tua direttiva di gioco principale per utilizzare le variabili scope.
... replace : true, scope :{ score: '=score', lifesCount: '=lifesCount' }, template: ...
Per testare l'associazione dell'ambito, aggiungi queste tre righe alla fine del metodo handleComplete()
.
scope.score = 10; scope.lifesCount = 2; scope.$apply();
Quando esegui l'applicazione dovresti vedere gli indicatori del punteggio e della durata.
Uno spazio bianco aggiuntivo, a destra della pagina, continuerà a essere presente perché a questo punto stiamo ancora codificando la larghezza e l'altezza del gioco nel nostro tutorial sulla programmazione del gioco HTML5.
Adattare la larghezza del gioco
AngularJS è ricco di metodi e servizi utili. Uno di questi è $window, che fornisce una proprietà innerWidth che useremo per calcolare la posizione dei nostri elementi.
Modifica la tua app/view1/view1.js
per inserire il servizio $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; }]);
Estendi la direttiva del gioco principale con le proprietà di larghezza e altezza e il gioco è fatto!
<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() { ...
Ora hai il gioco che si adatta alla larghezza della finestra del browser.
Se vuoi trasferirlo in un'app mobile, ti suggerisco di leggere il mio altro tutorial sullo sviluppo di app mobili sull'utilizzo del framework Ionic per creare app mobili. Dovresti essere in grado di creare un'app seed ionica, copiare tutto il codice da questo progetto e iniziare a giocare sul tuo dispositivo mobile in meno di un'ora.
L'unica cosa che non sto trattando qui è il rilevamento delle collisioni. Per saperne di più, ho letto questo articolo.
Incartare
Credo che nel corso di questo tutorial sullo sviluppo di giochi ti sia reso conto che AngularJS e CreateJS sono una coppia vincente per lo sviluppo di giochi basati su HTML5. Hai tutte le basi e sono sicuro che hai riconosciuto i vantaggi della combinazione di queste due piattaforme.
Puoi scaricare il codice per questo articolo da GitHub, sentiti libero di usarlo, condividerlo e personalizzarlo.