Créer un jeu HTML5 basé sur Canvas : un didacticiel utilisant AngularJS et CreateJS
Publié: 2022-03-11Le développement de jeux est l'une des techniques de programmation avancées les plus intéressantes qui défient constamment l'industrie du développement de logiciels.
Il existe de nombreuses plates-formes de programmation utilisées pour développer des jeux, et il existe une pléthore d'appareils sur lesquels les jouer, mais lorsqu'il s'agit de jouer à des jeux dans un navigateur Web, le développement basé sur Flash est toujours en tête.
La réécriture de jeux basés sur Flash vers la technologie HTML5 Canvas nous permettrait également de les jouer sur des navigateurs mobiles. Et, avec Apache Cordova, les développeurs Web qualifiés pourraient facilement les intégrer dans des applications de jeux mobiles multiplateformes.
Les gens de CreateJS ont décidé de faire cela et plus encore.
EaselJS , qui fait partie de la suite de CreateJS, simplifie le dessin sur HTML5 Canvas. Imaginez créer une visualisation de données personnalisée avec des performances élevées et des milliers d'éléments. Scalable Vector Graphic (SVG) n'est pas le bon choix, car il utilise des éléments DOM. Les navigateurs sont submergés lorsque, à environ 600 éléments DOM, les rendus initiaux, les redessins et les animations deviennent des opérations coûteuses. Avec HTML5 Canvas, nous pouvons facilement contourner ces problèmes ; Les dessins sur toile sont comme de l'encre sur papier, sans éléments DOM et leurs coûts associés.
Cela signifie que le développement basé sur Canvas nécessite plus d'attention lorsqu'il s'agit de séparer des éléments et d'y attacher des événements et des comportements. EaselJS vient à la rescousse ; nous pouvons coder comme s'il s'agissait d'éléments individuels, laissant la bibliothèque EaselJS gérer vos survols, clics et collisions.
Le codage basé sur SVG a un gros avantage : SVG a une ancienne spécification et il existe de nombreux outils de conception qui exportent des actifs SVG pour une utilisation dans le développement, de sorte que la coopération entre les concepteurs et les développeurs fonctionne bien. Les bibliothèques populaires, telles que D3.JS, et les bibliothèques plus récentes et plus puissantes comme SnapSVG, apportent beaucoup à la table.
Si le flux de travail du concepteur au développeur est la seule raison pour laquelle vous utiliseriez des SVG, envisagez des extensions pour Adobe Illustrator (AI) qui génèrent du code à partir de formes créées dans AI. Dans notre contexte, de telles extensions génèrent du code EaselJS ou du code ProcessingJS, qui sont tous deux des bibliothèques HTML5 basées sur Canvas.
Bref, si vous démarrez un nouveau projet, il n'y a plus aucune raison d'utiliser des SVG !
SoundJS fait partie de la suite CreateJS ; il fournit une API simple pour la spécification audio HTML5.
PreloadJS est utilisé pour précharger des actifs tels que des bitmaps, des fichiers audio, etc. Il fonctionne bien en combinaison avec d'autres bibliothèques CreateJS.
EaselJS, SoundJS et PreloadJS rendent le développement de jeux super facile pour tout ninja JavaScript. Ses méthodes API sont familières à tous ceux qui ont utilisé le développement de jeux basés sur Flash.
"Tout cela est génial. Mais que se passe-t-il si nous avons une équipe de développeurs convertissant un tas de jeux de Flash en HTML5 ? Est-il possible de faire cela avec cette suite ?
La réponse : "Oui, mais seulement si tous vos développeurs sont au niveau Jedi !".
Si vous avez une équipe de développeurs aux compétences variées, ce qui est souvent le cas, il peut être un peu effrayant d'utiliser CreateJS et d'attendre un code évolutif et modulaire. Et si nous combinions la suite CreateJS avec AngularJS ? Pouvons-nous atténuer ce risque en intégrant le framework JS frontal le meilleur et le plus adopté ?
Oui , et ce tutoriel de jeu HTML5 Canvas vous apprendra comment créer un jeu de base avec CreateJS et AngularJS !
Planter la graine
AngularJS réduit considérablement la complexité en permettant à votre équipe de développement de :
- Ajout de la modularité du code, afin que les membres de l'équipe puissent se concentrer sur différents aspects du jeu.
- Décomposer le code en éléments distincts testables et maintenables.
- Permettre la réutilisation du code, de sorte qu'une classe de fabrique puisse être instanciée plusieurs fois et réutilisée pour charger des actifs et des comportements différents mais similaires.
- Accélérer le développement car plusieurs membres de l'équipe peuvent travailler en parallèle, sans se marcher sur les pieds.
- Protéger les développeurs contre l'utilisation de mauvais modèles (Javascript contient des parties notoirement mauvaises et JSLint ne peut que nous aider beaucoup).
- Ajout d'un cadre de test solide.
Si, comme moi, vous êtes un "bricoleur" ou un apprenant tactile, vous devriez obtenir le code de GitHub et commencer à apprendre. Ma suggestion est de parcourir mes enregistrements et de comprendre les étapes que j'ai suivies pour tirer parti de l'ajout de la qualité AngularJS au code CreateJS.
Exécution de votre projet de démarrage AngularJS
Si vous ne l'avez pas déjà fait, vous devez installer nodeJS avant de pouvoir exécuter cette démo.
Après avoir créé un projet de départ AngularJS ou l'avoir téléchargé à partir de GitHub, exécutez npm install
pour télécharger toutes les dépendances dans le dossier de votre application.
Pour exécuter votre application, exécutez npm start
à partir du même dossier et accédez à http://localhost:8000/app/#/view1
dans votre navigateur. Votre page devrait ressembler à l'image ci-dessous.
EaselJS rencontre AngularJS
Ajoutez la référence de la bibliothèque CreateJS à votre projet de départ AngularJS. Assurez-vous que le script CreateJS est inclus après AngularJS.
<script src="http://code.createjs.com/createjs-2014.12.12.min.js"></script>
Ensuite, nettoyez l'application :
- Supprimer le dossier view2 du dossier de votre application
- Supprimez le menu et les informations de version d'AngularJS de index.html, en supprimant le code ci-dessous :
<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>
Supprimer le module view2
de app.js
, en supprimant la ligne suivante
myApp.view2,
Si vous n'avez jamais utilisé AngularJS auparavant et que vous n'êtes pas familier avec les directives AngularJS, consultez ce tutoriel. Les directives dans AngularJS sont un moyen d'enseigner de nouvelles astuces à HTML. Ils sont la fonctionnalité la plus bien pensée du framework et rendent AngularJS puissant et extensible.
Chaque fois que vous avez besoin d'une fonctionnalité ou d'un composant DOM spécialisé, recherchez-le en ligne ; il y a de fortes chances qu'il soit déjà disponible dans des endroits comme les modules angulaires.
La prochaine chose que nous devons faire est de créer une nouvelle directive AngularJS qui implémentera l'exemple d'EaselJS. Créez une nouvelle directive appelée spriteSheetRunner dans un nouveau fichier situé dans /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); } } } });
Une fois votre directive créée, ajoutez une dépendance à l'application en mettant à jour /app/app.js
comme ci-dessous :
'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'}); }]);
Incluez le code de la directive dans index.html
en ajoutant une référence à spriteSheetRunner.js
.
<script src="view1/directives/spriteSheetRunner.js"></script>
Nous sommes presque prêts ! Copiez les ressources du jeu dans le dossier de votre application. J'ai préparé les images, alors n'hésitez pas à les télécharger et à les enregistrer dans votre dossier app/assets.
- app/assets/spritesheet_grant.png
- app/assets/ground.png
- app/assets/hill1.png
- app/assets/hill2.png
- app/assets/sky.png
Enfin, ajoutez notre directive nouvellement créée à la page. Pour ce faire, modifiez votre fichier app/view/view1.html
et faites-en un one-liner :
<sprite-sheet-runner></sprite-sheet-runner>
Démarrez votre application et vous mettrez votre coureur en mouvement :)
S'il s'agit de votre première application AngularJS ou CreateJS, célébrez, vous venez de créer quelque chose de vraiment cool !
Précharger des ressources dans un service
Les services dans AngularJS sont des singletons utilisés principalement pour partager le code et les données. Nous utiliserons un service pour partager les "ressources du jeu" dans l'application. Pour en savoir plus sur les services AngularJS, consultez la documentation AngularJS.
Les services de développement AngularJS fournissent un mécanisme efficace pour charger et gérer tous les actifs en un seul endroit. Les changements d'actifs sont propagés à chaque instance individuelle d'un service, ce qui rend notre code beaucoup plus facile à maintenir.
Créez un nouveau fichier JS nommé loaderSvc.js
dans votre dossier /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 nous oblige à enregistrer tout service que nous utilisons. Pour ce faire, mettez à jour votre fichier app.js
pour inclure une référence à 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', []);
Mettez à jour votre code de directive, dans le fichier app/view1/directives/spriteSheetRunner.js
, pour supprimer le code de préchargement et utiliser le service à la place.
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); } } } }]);
Création d'une fabrique d'éléments d'interface utilisateur
La réutilisation et la répétition des sprites dans le développement de jeux sont très importantes. Afin de permettre l'instanciation des classes d'interface utilisateur (qui sont des sprites dans notre cas), nous utiliserons AngularJS Factories.
Factory est enregistré dans l'application comme n'importe quel autre module AngularJS. Pour créer une fabrique uiClasses, modifiez votre fichier app.js pour qu'il ressemble à ceci :
'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', []);
Utilisons la nouvelle usine pour créer le ciel, la colline, le sol et notre coureur. Pour ce faire, créez des fichiers JavaScript comme indiqué ci-dessous.

- 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); }]);
N'oubliez pas d'ajouter tous ces nouveaux fichiers JS dans votre index.html
.
Maintenant, nous devons mettre à jour la directive du jeu.
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); } } } }]);
Notez que le déplacement des uiClasses
hors de la directive a réduit la taille de la directive de 20 %, passant de 91 à 65 lignes.
De plus, nous pouvons écrire indépendamment des tests pour chaque classe d'usine afin de simplifier sa maintenance.
Remarque : Les tests sont un sujet qui n'est pas abordé dans cet article, mais voici un bon point de départ.
Interaction des touches fléchées
À ce stade de notre didacticiel sur le jeu HTML5 Canvas, un clic de souris ou un appui sur un mobile fera sursauter notre gars, et nous ne pouvons pas l'arrêter. Ajoutons des contrôles de touches fléchées :
- Flèche gauche (mettre le jeu en pause)
- Flèche vers le haut (saut)
- Flèche droite (commencer à courir)
Pour ce faire, créez la fonction keyDown
et ajoutez un écouteur d'événement en dernière ligne de la fonction 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;
Essayez de relancer votre jeu et vérifiez les commandes du clavier.
Laisse jouer la musique
Les jeux ne sont pas amusants sans musique, alors jouons de la musique.
Nous devrons d'abord ajouter des fichiers MP3 à notre dossier app/assets. Vous pouvez les télécharger à partir des URL fournies ci-dessous.
- app/assets/jump.mp3
- app/assets/runningTrack.mp3
Maintenant, nous devons précharger ces fichiers audio à l'aide de notre service de chargement. Nous utiliserons loadQueue
de la bibliothèque PreloaderJS
. Mettez à jour votre app/view1/services/loaderSvc.js
pour précharger ces fichiers.
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/"); }; });
Modifiez votre directive de jeu pour jouer des sons sur les événements du jeu.
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); } } } }]);
Ajouter des indicateurs de score et de vie
Ajoutons le score du jeu et les indicateurs de vie (cœur) au jeu HTML5 Canvas. Le score sera affiché sous forme de nombre dans le coin supérieur gauche, et les symboles de cœur, dans le coin supérieur droit, indiqueront le nombre de points de vie.
Nous utiliserons une bibliothèque de polices externe pour afficher les cœurs, alors ajoutez la ligne suivante à l'en-tête de votre fichier index.html
.
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet">
La liaison AngularJS standard fournira des mises à jour en temps réel. Ajoutez le code suivant à votre fichier 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>
Pour positionner correctement nos indicateurs, nous devons ajouter des classes CSS pour le haut à gauche et le haut à droite dans le fichier app/app.css
.
.top-left { position: absolute; left: 30px; top: 10px; } .top-right { position: absolute; right: 100px; top: 10px; float: right; }
Initialisez les variables score et lifesCount
dans le contrôleur 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; }]);
Pour vous assurer que les indicateurs sont correctement mis à jour, modifiez votre directive de jeu principale pour utiliser les variables de portée.
... replace : true, scope :{ score: '=score', lifesCount: '=lifesCount' }, template: ...
Pour tester la liaison de portée, ajoutez ces trois lignes à la fin de la méthode handleComplete()
.
scope.score = 10; scope.lifesCount = 2; scope.$apply();
Lorsque vous exécutez l'application, vous devriez voir les indicateurs de score et de vie.
Un espace blanc supplémentaire, à droite de la page, continuera d'être présent car nous continuons à coder en dur la largeur et la hauteur du jeu à ce stade de notre didacticiel de programmation de jeux HTML5.
Adapter la largeur du jeu
AngularJS regorge de méthodes et de services utiles. L'un d'eux est $window, qui fournit une propriété innerWidth que nous utiliserons pour calculer la position de nos éléments.
Modifiez votre app/view1/view1.js
pour injecter le service $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; }]);
Étendez la directive principale du jeu avec les propriétés width et height et c'est tout !
<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() { ...
Le jeu s'adapte maintenant à la largeur de la fenêtre du navigateur.
Si vous souhaitez le transférer dans une application mobile, je vous suggère de lire mon autre didacticiel de développement d'applications mobiles sur l'utilisation du framework Ionic pour créer des applications mobiles. Vous devriez pouvoir créer une application de graine ionique, copier tout le code de ce projet et commencer à jouer au jeu sur votre appareil mobile en moins d'une heure.
La seule chose que je ne couvre pas ici est la détection de collision. Pour en savoir plus, j'ai lu cet article.
Emballer
Je pense qu'au cours de ce didacticiel de développement de jeux, vous avez réalisé qu'AngularJS et CreateJS forment un duo gagnant pour le développement de jeux basés sur HTML5. Vous avez toutes les bases et je suis sûr que vous avez reconnu les avantages de combiner ces deux plateformes.
Vous pouvez télécharger le code de cet article sur GitHub, n'hésitez pas à l'utiliser, à le partager et à vous l'approprier.