Crear un juego basado en HTML5 Canvas: un tutorial usando AngularJS y CreateJS

Publicado: 2022-03-11

El desarrollo de juegos es una de las técnicas de programación más interesantes y avanzadas que desafía constantemente a la industria del desarrollo de software.

Hay muchas plataformas de programación que se utilizan para desarrollar juegos, y hay una gran cantidad de dispositivos para jugarlos, pero cuando se trata de jugar juegos en un navegador web, el desarrollo basado en Flash sigue liderando el camino.

La reescritura de juegos basados ​​en Flash a la tecnología HTML5 Canvas también nos permitiría jugarlos en navegadores móviles. Y, con Apache Cordova, los desarrolladores web capacitados podrían incluirlos fácilmente en aplicaciones de juegos móviles multiplataforma.

La gente de CreateJS se propuso hacer eso y más.

EaselJS , parte de la suite de CreateJS, simplifica el dibujo en HTML5 Canvas. Imagine crear una visualización de datos personalizada con alto rendimiento y miles de elementos. El gráfico vectorial escalable (SVG) no es la opción correcta, ya que utiliza elementos DOM. Los navegadores se ven abrumados cuando, con alrededor de 600 elementos DOM, las renderizaciones iniciales, los redibujados y la animación se vuelven operaciones costosas. Con HTML5 Canvas, podemos solucionar fácilmente estos problemas; Los dibujos en lienzo son como tinta sobre papel, sin elementos DOM y sus costos asociados.

Esto significa que el desarrollo basado en Canvas necesita más atención cuando se trata de separar elementos y adjuntarles eventos y comportamientos. EaselJS viene al rescate; podemos codificar como si estuviéramos tratando con elementos individuales, dejando que la biblioteca EaselJS maneje sus desplazamientos de mouse, clics y colisiones.

La codificación basada en SVG tiene una gran ventaja: SVG tiene una especificación antigua y hay muchas herramientas de diseño que exportan activos SVG para usar en el desarrollo, por lo que la cooperación entre diseñadores y desarrolladores funciona bien. Las bibliotecas populares, como D3.JS, y las bibliotecas más nuevas y potentes, como SnapSVG, aportan mucho a la mesa.

Si el flujo de trabajo de diseñador a desarrollador es la única razón por la que usaría SVG, considere las extensiones para Adobe Illustrator (AI) que generan código a partir de formas creadas en AI. En nuestro contexto, tales extensiones generan código EaselJS o código ProcessingJS, los cuales son bibliotecas basadas en HTML5 Canvas.

En pocas palabras, si está comenzando un nuevo proyecto, ¡ya no hay razón para usar SVG!

SoundJS es parte de la suite CreateJS; proporciona una API simple para la especificación de audio HTML5.

PreloadJS se utiliza para precargar activos como mapas de bits, archivos de sonido y similares. Funciona bien en combinación con otras bibliotecas CreateJS.

EaselJS, SoundJS y PreloadJS hacen que el desarrollo de juegos sea muy fácil para cualquier ninja de JavaScript. Sus métodos API son familiares para cualquiera que haya usado el desarrollo de juegos basado en Flash.

“Todo esto es genial. Pero, ¿qué pasa si tenemos un equipo de desarrolladores que convierte un montón de juegos de Flash a HTML5? ¿Es posible hacer eso con esta suite?

La respuesta: “Sí, ¡pero solo si todos tus desarrolladores están al nivel de Jedi!”.

Si tiene un equipo de desarrolladores de diferentes conjuntos de habilidades, lo que suele ser el caso, puede ser un poco aterrador usar CreateJS y esperar un código escalable y modular. ¿Qué pasa si juntamos la suite CreateJS con AngularJS? ¿Podemos mitigar este riesgo incorporando el mejor y más adoptado marco JS front-end?

¡ , y este tutorial de HTML5 Canvas te enseñará cómo crear un juego básico con CreateJS y AngularJS!

Tutorial del juego HTML5 Canvas con CreateJS y AngularJS

Plantar la semilla

AngularJS reduce significativamente la complejidad al permitirle a su equipo de desarrollo lo siguiente:

  1. Agregar modularidad de código, para que los miembros del equipo puedan concentrarse en diferentes aspectos del juego.
  2. Dividir el código en piezas separadas comprobables y mantenibles.
  3. Permitir la reutilización del código, de modo que una clase de fábrica se pueda instanciar varias veces y reutilizar para cargar activos y comportamientos diferentes pero similares.
  4. Acelerar el desarrollo porque varios miembros del equipo pueden trabajar en paralelo, sin molestarse unos a otros.
  5. Proteger a los desarrolladores del uso de malos patrones (Javascript tiene partes notoriamente malas y JSLint solo puede ayudarnos hasta cierto punto).
  6. Agregar un marco de prueba sólido.

Si, como yo, eres un "manitas" o un aprendiz táctil, debes obtener el código de GitHub y comenzar a aprender. Mi sugerencia es revisar mis registros y comprender los pasos que tomé para obtener los beneficios de agregar la bondad de AngularJS al código CreateJS.

Ejecutando su proyecto semilla AngularJS

Si aún no lo ha hecho, debe instalar nodeJS antes de poder ejecutar esta demostración.

Después de crear un proyecto inicial de AngularJS o descargarlo de GitHub, ejecute npm install para descargar todas las dependencias a la carpeta de su aplicación.

Para ejecutar su aplicación, ejecute npm start desde la misma carpeta y navegue hasta http://localhost:8000/app/#/view1 en su navegador. Su página debe verse como la imagen de abajo.

ejemplo de página

EaselJS se encuentra con AngularJS

Agregue la referencia de la biblioteca CreateJS a su proyecto inicial de AngularJS. Asegúrese de que el script CreateJS esté incluido después de AngularJS.

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

A continuación, limpie la aplicación:

  • Elimine la carpeta view2 de la carpeta de su aplicación
  • Elimine el menú y la información de la versión de AngularJS de index.html, eliminando el código que se muestra a continuación:
 <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>

Elimine el módulo view2 de app.js , eliminando la siguiente línea

myApp.view2,

Si no ha usado AngularJS antes y no está familiarizado con las directivas de AngularJS, consulte este tutorial. Las directivas en AngularJS son una forma de enseñarle a HTML algunos trucos nuevos. Son la característica mejor pensada en el marco y hacen que AngularJS sea poderoso y extensible.

Siempre que necesite una funcionalidad DOM especializada o un componente, búsquelo en línea; hay una buena posibilidad de que ya esté disponible en lugares como módulos Angular.

Lo siguiente que debemos hacer es crear una nueva directiva AngularJS que implementará el ejemplo de EaselJS. Cree una nueva directiva llamada spriteSheetRunner en un nuevo archivo ubicado en /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 vez que se crea su directiva, agregue una dependencia a la aplicación actualizando /app/app.js como se muestra a continuación:

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

Incluya el código de la directiva en index.html agregando una referencia a spriteSheetRunner.js .

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

¡Ya casi estamos listos! Copie los recursos del juego en la carpeta de su aplicación. He preparado las imágenes, así que siéntete libre de descargarlas y guardarlas en tu carpeta de aplicaciones/activos.

  • aplicación/activos/spritesheet_grant.png
  • aplicación/activos/tierra.png
  • aplicación/activos/colina1.png
  • aplicación/activos/colina2.png
  • aplicación/activos/cielo.png

Como paso final, agregue nuestra directiva recién creada a la página. Para hacerlo, cambie su archivo app/view/view1.html en una sola línea:

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

Inicie su aplicación y tendrá su corredor en movimiento :)

corredor en movimiento

Si esta es su primera aplicación AngularJS o CreateJS, celébrelo, ¡acaba de hacer algo realmente genial!

Precargar activos en un servicio

Los servicios en AngularJS son singletons que se utilizan principalmente para compartir el código y los datos. Usaremos un servicio para compartir los "activos del juego" en la aplicación. Para obtener más información sobre los servicios de AngularJS, consulte la documentación de AngularJS.

Los servicios de desarrollo de AngularJS brindan un mecanismo efectivo para cargar y administrar todos los activos en un solo lugar. Los cambios de activos se propagan a cada instancia individual de un servicio, lo que hace que nuestro código sea mucho más fácil de mantener.

Cree un nuevo archivo JS llamado loaderSvc.js en su carpeta /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 requiere que registremos cualquier servicio que estemos usando. Para hacerlo, actualice su archivo app.js para incluir una referencia 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', []);

Actualice su código de directiva, en app/view1/directives/spriteSheetRunner.js , para eliminar el código de precarga y usar el servicio en su lugar.

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

Creación de fábrica de elementos de interfaz de usuario

Reutilizar y repetir sprites en el desarrollo de juegos es muy importante. Para habilitar la creación de instancias de clases de UI (que son sprites en nuestro caso), usaremos AngularJS Factories.

Factory está registrado en la aplicación como cualquier otro módulo AngularJS. Para crear una fábrica de uiClasses, modifique su archivo app.js para que se vea así:

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

Usemos la nueva fábrica para crear el cielo, la colina, el suelo y nuestro corredor. Para hacerlo, cree archivos JavaScript como se indica a continuación.

  • 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); }]);
  • aplicación/vista1/tierra.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); }]);

No olvide agregar todos estos nuevos archivos JS en su index.html .

Ahora, necesitamos actualizar la directiva del juego.

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

Tenga en cuenta que sacar uiClasses de la directiva redujo el tamaño de la directiva en un 20 %, de 91 a 65 líneas.

Además, podemos escribir pruebas de forma independiente para cada clase de fábrica para simplificar su mantenimiento.

Nota: las pruebas son un tema que no se cubre en esta publicación, pero este es un buen lugar para comenzar.

Interacción de teclas de flecha

En este punto de nuestro tutorial del juego HTML5 Canvas, hacer clic con el mouse o tocar un dispositivo móvil hará que nuestro chico salte y no podemos detenerlo. Agreguemos controles de tecla de flecha:

  • Flecha izquierda (pausar el juego)
  • Flecha arriba (salto)
  • Flecha derecha (empezar a correr)

Para ello, cree la función keyDown y agregue un detector de eventos como última línea de la función 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;

Intente ejecutar su juego nuevamente y verifique los controles del teclado.

Deja que la música suene

Los juegos no son divertidos sin música, así que pongamos música.

Primero necesitaremos agregar archivos MP3 a nuestra carpeta de aplicaciones/activos. Puede descargarlos desde las URL proporcionadas a continuación.

  • aplicación/activos/jump.mp3
  • app/activos/runningTrack.mp3

Ahora, necesitamos precargar estos archivos de sonido usando nuestro servicio de carga. Usaremos loadQueue de la biblioteca PreloaderJS . Actualice su app/view1/services/loaderSvc.js para precargar estos archivos.

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

Modifique la directiva de su juego para reproducir sonidos en los eventos del juego.

 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); } } } }]);
Relacionado: Mejores prácticas y consejos de AngularJS por parte de los desarrolladores de Toptal

Adición de indicadores de puntuación y vida

Agreguemos la puntuación del juego y los indicadores de vida (corazón) al juego HTML5 Canvas. La puntuación se mostrará como un número en la esquina superior izquierda y los símbolos de corazón, en la esquina superior derecha, indicarán el recuento de vidas.

Usaremos una biblioteca de fuentes externa para representar corazones, así que agregue la siguiente línea a su encabezado de archivo index.html .

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

El enlace estándar de AngularJS proporcionará actualizaciones en tiempo real. Agrega el siguiente código a tu archivo 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>

Para posicionar correctamente nuestros indicadores, debemos agregar clases de CSS para la parte superior izquierda y superior derecha en el archivo app/app.css .

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

Inicialice las variables score y lifesCount en el controlador 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; }]);

Para asegurarse de que los indicadores se actualicen correctamente, modifique la directiva principal de su juego para usar las variables de alcance.

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

Para probar el enlace del alcance, agregue estas tres líneas al final del método handleComplete() .

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

Cuando ejecute la aplicación, debería ver los indicadores de puntuación y vida.

puntuación e indicadores de vida

El espacio en blanco adicional, a la derecha de la página, seguirá estando presente porque todavía estamos codificando el ancho y el alto del juego en este punto de nuestro tutorial de programación de juegos HTML5.

Adaptar el ancho del juego

AngularJS está repleto de métodos y servicios útiles. Uno de ellos es $window, que proporciona una propiedad innerWidth que usaremos para calcular la posición de nuestros elementos.

Modifique su app/view1/view1.js para inyectar el servicio $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; }]);

¡Extienda la directiva principal del juego con propiedades de ancho y alto y eso es todo!

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

Ahora tienes el juego ajustándose al ancho de la ventana del navegador.

Si desea portar esto a una aplicación móvil, le sugiero que lea mi otro tutorial de desarrollo de aplicaciones móviles sobre el uso del marco Ionic para crear aplicaciones móviles. Debería poder crear una aplicación de semillas iónicas, copiar todo el código de este proyecto y comenzar a jugar en su dispositivo móvil en menos de una hora.

Lo único que no estoy cubriendo aquí es la detección de colisiones. Para aprender más sobre esto, leí este artículo.

Envolver

Creo que a lo largo de este tutorial de desarrollo de juegos se dio cuenta de que AngularJS y CreateJS son un dúo ganador para el desarrollo de juegos basados ​​en HTML5. Tienes todos los conceptos básicos y estoy seguro de que reconoces los beneficios de combinar estas dos plataformas.

Puede descargar el código de este artículo de GitHub, siéntase libre de usarlo, compartirlo y personalizarlo.

Relacionado: Los 18 errores más comunes de AngularJS que cometen los desarrolladores