Criando um jogo baseado em tela HTML5: um tutorial usando AngularJS e CreateJS

Publicados: 2022-03-11

O desenvolvimento de jogos é uma das técnicas de programação mais interessantes e avançadas que desafia constantemente a indústria de desenvolvimento de software.

Existem muitas plataformas de programação usadas para desenvolver jogos, e há uma infinidade de dispositivos para jogá-los, mas quando se trata de jogar em um navegador da Web, o desenvolvimento baseado em Flash ainda lidera o caminho.

Reescrever jogos baseados em Flash para a tecnologia HTML5 Canvas nos permitiria jogá-los em navegadores móveis também. E, com o Apache Cordova, desenvolvedores web habilidosos podem facilmente envolvê-los em aplicativos de jogos móveis multiplataforma.

O pessoal do CreateJS decidiu fazer isso e muito mais.

EaselJS , parte do pacote CreateJS, simplifica o desenho em HTML5 Canvas. Imagine construir uma visualização de dados personalizada com alto desempenho e milhares de elementos. Scalable Vector Graphic (SVG) não é a escolha certa, pois utiliza elementos DOM. Os navegadores ficam sobrecarregados quando, com cerca de 600 elementos DOM, renderizações iniciais, redesenhos e animações se tornam operações caras. Com o HTML5 Canvas, podemos facilmente contornar esses problemas; Desenhos em tela são como tinta em papel, sem elementos DOM e seus custos associados.

Isso significa que o desenvolvimento baseado em Canvas precisa de mais atenção quando se trata de separar elementos e anexar eventos e comportamentos a eles. EaselJS vem em socorro; podemos codificar como se estivéssemos lidando com elementos individuais, permitindo que a biblioteca EaselJS manipule suas passagens de mouse, cliques e colisões.

A codificação baseada em SVG tem uma grande vantagem: o SVG tem uma especificação antiga e há muitas ferramentas de design que exportam recursos SVG para uso no desenvolvimento, para que a cooperação entre designers e desenvolvedores funcione bem. Bibliotecas populares, como D3.JS, e bibliotecas mais recentes e mais poderosas, como SnapSVG, trazem muito para a mesa.

Se o fluxo de trabalho de designer para desenvolvedor for o único motivo pelo qual você usaria SVGs, considere extensões para Adobe Illustrator (AI) que geram código de formas criadas em AI. Em nosso contexto, tais extensões geram código EaselJS ou código ProcessingJS, ambos bibliotecas baseadas em HTML5 Canvas

Resumindo, se você está iniciando um novo projeto, não há mais motivos para usar SVGs!

SoundJS faz parte da suíte CreateJS; ele fornece uma API simples para especificação de áudio HTML5.

PreloadJS é usado para pré-carregar ativos como bitmaps, arquivos de som e similares. Funciona bem em combinação com outras bibliotecas CreateJS.

EaselJS, SoundJS e PreloadJS tornam o desenvolvimento de jogos super fácil para qualquer ninja JavaScript. Seus métodos de API são familiares para qualquer pessoa que tenha usado o desenvolvimento de jogos baseado em Flash.

“Isso tudo é ótimo. Mas, e se tivermos uma equipe de desenvolvedores convertendo vários jogos de Flash para HTML5? É possível fazer isso com esta suíte?”

A resposta: “Sim, mas apenas se todos os seus desenvolvedores estiverem no nível Jedi!”.

Se você tem uma equipe de desenvolvedores de conjuntos de habilidades variadas, o que geralmente é o caso, pode ser um pouco assustador usar CreateJS e esperar um código escalável e modular. E se juntarmos a suíte CreateJS com AngularJS? Podemos mitigar esse risco trazendo a melhor e mais adotada estrutura JS de front-end?

Sim , e este tutorial de jogo HTML5 Canvas vai te ensinar como criar um jogo básico com CreateJS e AngularJS!

Tutorial de jogo HTML5 Canvas com CreateJS e AngularJS

Plantando a semente

O AngularJS reduz significativamente a complexidade, permitindo que sua equipe de desenvolvimento tenha o seguinte:

  1. Adicionando modularidade de código, para que os membros da equipe possam se concentrar em diferentes aspectos do jogo.
  2. Quebrando o código em partes testáveis ​​e de manutenção separadas.
  3. Habilitando a reutilização de código, para que uma classe de fábrica possa ser instanciada várias vezes e reutilizada para carregar ativos e comportamentos diferentes, mas semelhantes.
  4. Acelerando o desenvolvimento porque vários membros da equipe podem trabalhar em paralelo, sem pisar nos calos uns dos outros.
  5. Protegendo os desenvolvedores de usar padrões ruins (Javascript carrega partes notoriamente ruins com ele e JSLint só pode nos ajudar muito).
  6. Adicionando uma estrutura de teste sólida.

Se, como eu, você é um “funileiro” ou um aprendiz tátil, você deve obter o código do GitHub e começar a aprender. Minha sugestão é examinar meus check-ins e entender as etapas que tomei para obter os benefícios de adicionar a bondade do AngularJS ao código CreateJS.

Executando seu projeto de semente AngularJS

Se você ainda não fez isso, você precisa instalar o nodeJS antes de executar esta demonstração.

Depois de criar um projeto seed do AngularJS ou baixá-lo do GitHub, execute npm install para baixar todas as dependências para a pasta do seu aplicativo.

Para executar seu aplicativo, execute npm start na mesma pasta e navegue até http://localhost:8000/app/#/view1 em seu navegador. Sua página deve ficar como a imagem abaixo.

exemplo de página

EaselJS encontra AngularJS

Adicione a referência da biblioteca CreateJS ao seu projeto de semente AngularJS. Certifique-se de que o script CreateJS esteja incluído após o AngularJS.

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

Em seguida, limpe o aplicativo:

  • Exclua a pasta view2 da pasta do seu aplicativo
  • Remova as informações do menu e da versão do AngularJS do index.html, excluindo o código mostrado abaixo:
 <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>

Remova o módulo view2 de app.js , excluindo a seguinte linha

myApp.view2,

Se você não usou o AngularJS antes e não está familiarizado com as diretivas do AngularJS, confira este tutorial. As diretivas no AngularJS são uma maneira de ensinar alguns truques novos ao HTML. Eles são o recurso mais bem pensado na estrutura e tornam o AngularJS poderoso e extensível.

Sempre que você precisar de uma funcionalidade DOM especializada ou de um componente, procure-o online; há uma boa chance de já estar disponível em lugares como módulos Angular.

A próxima coisa que precisamos fazer é criar uma nova diretiva AngularJS que implementará o exemplo do EaselJS. Crie uma nova diretiva chamada spriteSheetRunner em um novo arquivo localizado em /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); } } } });

Depois que sua diretiva for criada, adicione uma dependência ao aplicativo atualizando /app/app.js conforme abaixo:

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

Inclua o código da diretiva em index.html adicionando uma referência a spriteSheetRunner.js .

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

Estamos quase prontos! Copie os ativos do jogo para a pasta do seu aplicativo. Eu preparei as imagens, então sinta-se à vontade para baixá-las e salvá-las na sua pasta app/assets.

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

Como etapa final, adicione nossa diretiva recém-criada à página. Para fazer isso, altere seu arquivo app/view/view1.html e transforme-o em uma linha:

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

Inicie sua aplicação e você colocará seu corredor em movimento :)

corredor em movimento

Se este é o seu primeiro aplicativo AngularJS ou CreateJS, comemore, você acabou de fazer algo muito legal!

Pré-carregamento de ativos em um serviço

Serviços em AngularJS são singletons usados ​​principalmente para compartilhar o código e os dados. Usaremos um serviço para compartilhar os 'ativos do jogo' em todo o aplicativo. Para saber mais sobre os serviços do AngularJS, consulte a documentação do AngularJS.

Os serviços de desenvolvimento AngularJS fornecem um mecanismo eficaz para carregar e gerenciar todos os ativos em um só lugar. As alterações de ativos são propagadas para cada instância individual de um serviço, tornando nosso código muito mais fácil de manter.

Crie um novo arquivo JS chamado loaderSvc.js em sua pasta /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/"); }; });

O AngularJS exige que registremos qualquer serviço que estejamos usando. Para fazer isso, atualize seu arquivo app.js para incluir a referência 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', []);

Atualize seu código de diretiva, no arquivo app/view1/directives/spriteSheetRunner.js , para remover o código de pré-carregamento e usar o serviço.

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

Criando a Fábrica de Elementos de IU

Reutilizar e repetir sprites no desenvolvimento de jogos é muito importante. Para habilitar a instanciação de classes de UI (que são sprites no nosso caso) usaremos AngularJS Factories.

Factory é registrado no aplicativo como qualquer outro módulo AngularJS. Para criar a fábrica uiClasses, modifique seu arquivo app.js para ficar assim:

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

Vamos usar a nova fábrica para criar céu, colina, chão e nosso corredor. Para fazer isso, crie arquivos JavaScript conforme listado abaixo.

  • 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ão se esqueça de adicionar todos esses novos arquivos JS em seu index.html .

Agora, precisamos atualizar a diretiva do jogo.

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

Observe que a remoção de uiClasses da diretiva reduziu o tamanho da diretiva em 20%, de 91 para 65 linhas.

Além disso, podemos escrever testes de forma independente para cada classe de fábrica para simplificar sua manutenção.

Nota: O teste é um tópico que não é abordado neste post, mas aqui é um bom lugar para começar.

Interação das teclas de seta

Neste ponto em nosso tutorial de jogo HTML5 Canvas, clique do mouse ou toque em um celular fará nosso cara pular, e não podemos detê-lo. Vamos adicionar controles de tecla de seta:

  • Seta para a esquerda (pausa o jogo)
  • Seta para cima (salto)
  • Seta para a direita (começar a correr)

Para fazer isso, crie a função keyDown e adicione um ouvinte de eventos como última linha da função 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;

Tente executar o jogo novamente e verifique os controles do teclado.

Deixe a música tocar

Os jogos não são divertidos sem música, então vamos tocar um pouco de música.

Primeiro precisamos adicionar arquivos MP3 à nossa pasta app/assets. Você pode baixá-los a partir dos URLs fornecidos abaixo.

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

Agora, precisamos pré-carregar esses arquivos de som usando nosso serviço de carregador. Usaremos loadQueue da biblioteca PreloaderJS . Atualize seu app/view1/services/loaderSvc.js para pré-carregar esses arquivos.

 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 sua diretiva de jogo para reproduzir sons em eventos de jogo.

 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: Melhores práticas e dicas do AngularJS por desenvolvedores da Toptal

Adicionando Pontuação e Indicadores de Vida

Vamos adicionar a pontuação do jogo e os indicadores de vida (coração) ao jogo HTML5 Canvas. A pontuação será mostrada como um número no canto superior esquerdo e os símbolos de coração, no canto superior direito, indicarão a contagem de vidas.

Usaremos uma biblioteca de fontes externa para renderizar corações, então adicione a seguinte linha ao cabeçalho do seu arquivo index.html .

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

A ligação AngularJS padrão fornecerá atualizações em tempo real. Adicione o seguinte código ao seu arquivo 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 corretamente nossos indicadores, precisamos adicionar classes CSS para o canto superior esquerdo e o canto superior direito no arquivo app/app.css .

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

Inicialize as variáveis ​​score e lifesCount no 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 garantir que os indicadores sejam atualizados corretamente, modifique sua diretiva principal do jogo para usar as variáveis ​​de escopo.

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

Para testar a vinculação de escopo, adicione essas três linhas no final do método handleComplete() .

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

Ao executar o aplicativo, você deverá ver os indicadores de pontuação e vida.

pontuação e indicadores de vida

O espaço em branco adicional, à direita da página, continuará presente porque ainda estamos codificando a largura e a altura do jogo neste ponto em nosso tutorial de programação de jogos HTML5.

Adaptando a largura do jogo

AngularJS está repleto de métodos e serviços úteis. Um deles é $window, que fornece uma propriedade innerWidth que usaremos para calcular a posição de nossos elementos.

Modifique seu app/view1/view1.js para injetar o serviço $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; }]);

Estenda a diretiva principal do jogo com as propriedades de largura e altura e pronto!

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

Agora você tem o jogo se ajustando à largura da janela do navegador.

Se você quiser portar isso em um aplicativo móvel, sugiro ler meu outro tutorial de desenvolvimento de aplicativos móveis sobre como usar a estrutura Ionic para criar aplicativos móveis. Você deve ser capaz de criar um aplicativo de sementes iônicas, copiar todo o código deste projeto e começar a jogar o jogo em seu dispositivo móvel em menos de uma hora.

A única coisa que não estou cobrindo aqui é a detecção de colisão. Para saber mais sobre isso, eu li este artigo.

Embrulhar

Acredito que ao longo deste tutorial de desenvolvimento de jogos você percebeu que AngularJS e CreateJS são uma dupla vencedora para desenvolvimento de jogos baseados em HTML5. Você tem o básico e tenho certeza que reconheceu os benefícios de combinar essas duas plataformas.

Você pode baixar o código para este artigo no GitHub, sinta-se à vontade para usar, compartilhar e torná-lo seu.

Relacionado: Os 18 erros mais comuns do AngularJS que os desenvolvedores cometem