Making an HTML5 Canvas Based Game: A Tutorial Using AngularJS and CreateJS

View all articles

Game development is one of the more interesting, advanced programming techniques that constantly challenges the software development industry.

There are many programming platforms used to develop games, and there are a plethora of devices to play them on, but when it comes to playing games in a web browser, Flash-based development still leads the way.

Rewriting Flash-based games to HTML5 Canvas technology would let us play them on mobile browsers as well. And, with Apache Cordova, skilled web developers could easily wrap them into cross platform mobile game apps.

Folks at CreateJS set out to do that and more.

EaselJS, part of CreateJS’s suite, makes drawing on HTML5 Canvas simple. Imagine building custom data visualization with high performance and thousands of elements. Scalable Vector Graphic (SVG) is not the right choice, because it uses DOM elements. Browsers become overwhelmed when, at around 600 DOM elements, initial renderings, re-draws, and animation become expensive operations. With HTML5 Canvas, we can easily get around these problems; Canvas drawings are like ink on paper, no DOM elements and their associated costs.

This means that Canvas based development needs more attention when it comes to separating elements, and attaching events and behaviors to them. EaselJS comes to the rescue; we can code as if we are dealing with individual elements, letting EaselJS library handle your mouse-overs, clicks, and collisions.

SVG-based coding has one big advantage: SVG has an old specification, and there are a lot of design tools that export SVG assets for use in development, so that cooperation between designers and developers works well. Popular libraries, such as D3.JS, and newer, more powerful libraries like SnapSVG, bring much to the table.

If designer-to-developer workflow is the only reason you would use SVGs, consider drawscript. This is an extension for Adobe Illustrator (AI) and generates code from shapes created in AI. In our context, this extension generates EaselJS code or ProcessingJS code, both of which are HTML5 Canvas-based libraries

Bottom line, if you are starting a new project, there is no reason to use SVGs anymore!

SoundJS is part of the CreateJS suite; it provides a simple API for HTML5 Audio specification.

PreloadJS is used to preload assets such as bitmaps, sound files, and the like. It works well in combination with other CreateJS libraries.

EaselJS, SoundJS, and PreloadJS make game development super easy for any JavaScript ninja. Its API methods are familiar to anyone who used Flash-based game development.

“This is all great. But, what if we have a team of developers converting a bunch of games from Flash to HTML5? Is it possible to do that with this suite?”

The answer: “Yes, but only if all of your developers are at Jedi level!”.

If you have a team of varying skill set developers, which is often the case, it can be a little scary to use CreateJS and expect a scalable and modular code. What if we bring together CreateJS suite with AngularJS? Can we mitigate against this risk by bringing in the best and most adopted front-end JS framework?

Yes, and this HTML5 Canvas game tutorial will teach you how to create a basic game with CreateJS and AngularJS!

HTML5 Canvas game tutorial with CreateJS and AngularJS

Planting The Seed

To give you an idea of what are we going to make in this tutorial, I made a small demo. Check it, and warm up to make your HTML5 Canvas based game.

I started with default AngularJS seed app, created by my WebStorm IDE. Then, I downloaded the source code of EaselJS Spritesheets example from createjs.com, and separated the code into AngularJS directive, service, and factories. I also added some music to the game, and a little more control of the character movement. Plus, I added a score and life count for the character, which are necessary for the game.

AngularJS significantly reduces complexity by enabling your development team with the following:

  1. Adding code modularity, so that team members can focus on different aspects of the game.
  2. Breaking the code into separate testable and maintainable pieces.
  3. Enabling code re-use, so that one factory class can be instantiated multiple times, and re-used to load different but similar assets and behaviors.
  4. Speeding up the development because multiple team members can work in parallel, without stepping on each other’s toes.
  5. Protecting developers from using bad patterns (Javascript carries notoriously bad parts with it and JSLint can only help us so much).
  6. Adding a solid testing framework.

If, like me, you are a “tinkerer” or a tactile learner, you should get the code from GitHub and start learning. My suggestion is to look through my check-ins and understand the steps I took to gain benefits of adding AngularJS goodness to CreateJS code.

Running Your AngularJS Seed Project

If you haven’t already done so, you need to install nodeJS before you can run this demo.

After creating an AngularJS seed project, or downloading it from GitHub, run npm install to download all dependencies to your app folder.

To run your application, execute npm start from same folder and navigate to http://localhost:8000/app/#/view1 in your browser. Your page should look like the image below.

page example

EaselJS Meets AngularJS

Add CreateJS library reference to your AngularJS seed project. Make sure that CreateJS script is included after AngularJS.

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

Next, clean up the application:

  • Delete view2 folder from your app folder
  • Remove menu and AngularJS version info from index.html, by deleting the code shown below:
<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>

Remove view2 module from app.js, by deleting the following line

myApp.view2,

If you haven’t used AngularJS before, and are not familiar with AngularJS directives, check this tutorial. Directives in AngularJS are a way to teach HTML some new tricks. They are the most well thought out feature in the framework, and make AngularJS powerful and extensible.

Whenever you need a specialized DOM functionality or a component, search for it online; there is a good chance it is already available in places like Angular modules.

The next thing we need to do is create a new AngularJS directive that will implement the example from EaselJS. Create a new directive called spriteSheetRunner in a new file located in /app/view1/directives/spriteSheetRunner.js.

angular.module('myApp.directives', [])
.directive('spriteSheetRunner', function () {
       "use strict";
       return {
           restrict : 'EAC',
           replace : true,
           scope :{
           },
           template: "<canvas width='960' height='400'></canvas>",
           link: function (scope, element, attribute) {
               var w, h, loader, manifest, sky, grant, ground, hill, hill2;
               drawGame();
               function drawGame() {
                   //drawing the game canvas from scratch here
                   //In future we can pass stages as param and load indexes from arrays of background elements etc
                   if (scope.stage) {
                       scope.stage.autoClear = true;
                       scope.stage.removeAllChildren();
                       scope.stage.update();
                   } else {
                       scope.stage = new createjs.Stage(element[0]);
                   }
                   w = scope.stage.canvas.width;
                   h = scope.stage.canvas.height;
                   manifest = [
                       {src: "spritesheet_grant.png", id: "grant"},
                       {src: "sky.png", id: "sky"},
                       {src: "ground.png", id: "ground"},
                       {src: "hill1.png", id: "hill"},
                       {src: "hill2.png", id: "hill2"}
                   ];
                   loader = new createjs.LoadQueue(false);
                   loader.addEventListener("complete", handleComplete);
                   loader.loadManifest(manifest, true, "/app/assets/");
               }
               function handleComplete() {
                   sky = new createjs.Shape();
                   sky.graphics.beginBitmapFill(loader.getResult("sky")).drawRect(0, 0, w, h);
                   var groundImg = loader.getResult("ground");
                   ground = new createjs.Shape();
                   ground.graphics.beginBitmapFill(groundImg).drawRect(0, 0, w  groundImg.width, groundImg.height);
                   ground.tileW = groundImg.width;
                   ground.y = h - groundImg.height;
                   hill = new createjs.Bitmap(loader.getResult("hill"));
                   hill.setTransform(Math.random() * w, h - hill.image.height * 4 - groundImg.height, 4, 4);
                   hill.alpha = 0.5;
                   hill2 = new createjs.Bitmap(loader.getResult("hill2"));
                   hill2.setTransform(Math.random() * w, h - hill2.image.height * 3 - groundImg.height, 3, 3);
                   var spriteSheet = new createjs.SpriteSheet({
                       framerate: 30,
                       "images": [loader.getResult("grant")],
                       "frames": {"regX": 82, "height": 292, "count": 64, "regY": 0, "width": 165},
                       // define two animations, run (loops, 1.5x speed) and jump (returns to run):
                       "animations": {
                           "run": [0, 25, "run", 1.5],
                           "jump": [26, 63, "run"]
                       }
                   });
                   grant = new createjs.Sprite(spriteSheet, "run");
                   grant.y = 35;
                   scope.stage.addChild(sky, hill, hill2, ground, grant);
                   scope.stage.addEventListener("stagemousedown", handleJumpStart);
                   createjs.Ticker.timingMode = createjs.Ticker.RAF;
                   createjs.Ticker.addEventListener("tick", tick);
               }
               function handleJumpStart() {
                   grant.gotoAndPlay("jump");
               }
               function tick(event) {
                   var deltaS = event.delta / 1000;
                   var position = grant.x  150 * deltaS;
                   var grantW = grant.getBounds().width * grant.scaleX;
                   grant.x = (position >= w  grantW) ? -grantW : position;
                   ground.x = (ground.x - deltaS * 150) % ground.tileW;
                   hill.x = (hill.x - deltaS * 30);
                   if (hill.x  hill.image.width * hill.scaleX <= 0) {
                       hill.x = w;
                   }
                   hill2.x = (hill2.x - deltaS * 45);
                   if (hill2.x  hill2.image.width * hill2.scaleX <= 0) {
                       hill2.x = w;
                   }
                   scope.stage.update(event);
               }
           }
       }
   });

Once your directive is created, add a dependency to the app by updating /app/app.js as below:

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

Include the directive code in index.html by adding a reference to spriteSheetRunner.js.

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

We are almost ready! Copy the game assets to your app folder. I’ve prepared the images, so feel free to download them and save in your app/assets folder.

As a final step, add our newly created directive to the page. To do so change your app/view/view1.html file, and make it a one-liner:

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

Start your application and you will get your runner in motion :)

runner in motion

If this is your first AngularJS or first CreateJS application, celebrate, you just made something really cool!

Preloading Assets In A Service

Services in AngularJS are singletons used primarily to share the code and the data. We will use a service to share the ‘game assets’ across the application. To learn more about AngularJS services check the AngularJS documentation.

AngularJS services provide an effective mechanism for loading and managing all assets in one place. Asset changes are propagated to each individual instance of a service, making our code much easier to maintain.

Create new JS file named loaderSvc.js in your /app/view1/services folder.

//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 requires us to register any service we are using. To do so, update your app.js file to include reference to 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', []);

Update your directive code, in app/view1/directives/spriteSheetRunner.js file, to remove the preloading code and use the service instead.

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

Creating UI elements factory

Re-using and repeating sprites in game development is very important. In order to enable instantiation of UI classes (which are sprites in our case) we will use AngularJS Factories.

Factory is registered in the application just like any other AngularJS module. To create uiClasses factory, modify your app.js file to look like this:

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

Let’s use the new factory to create sky, hill, ground, and our runner. To do so, create JavaScript files as listed below.

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


Do not forget to add all these new JS files in your index.html.

Now, we need to update the game directive.

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

Note that moving uiClasses out of the directive reduced the directive size by 20%, from 91 to 65 lines.

In addition, we can independently write tests for each factory class to simplify its maintenance.

Note: Testing is a topic that is not covered in this post but here is a good place to start.

Arrow Keys Interaction

At this point in our HTML5 Canvas game tutorial, mouse click or tap on a mobile will make our guy jump, and we cannot stop him. Let’s add arrow key controls:

  • Left arrow (pause the game)
  • Up arrow (jump)
  • Right arrow (start running)

To do that, create the keyDown function and add an event listener as last line of handleComplete() function.

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;

Try running your game again and check the keyboard controls.

Let The Music Play

Games are not fun without music, so let’s play some music.

We will first need to add MP3 files to our app/assets folder. You can download them from the URLs provided below.

Now, we need to preload these sound files using our loader service. We will use loadQueue of PreloaderJS library. Update your app/view1/services/loaderSvc.js to preload these files.

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

Modify your game directive to play sounds on game events.

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

Adding Score and Life Indicators

Let’s add the game score and life (heart) indicators to the HTML5 Canvas game. The score will be shown as a number in the upper left corner, and heart symbols, at the top right corner, will indicate life count.

We will use an external font library to render hearts, so add the following line to your index.html file header.

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

Standard AngularJS binding will provide real-time updates. Add the following code to your app/view1/view1.html file:

<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>

To properly position our indicators, we need to add CSS classes for top-left and top-right in app/app.css file.

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

Initialize the score and lifesCount variables in app/view1/view1.js controller.

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

To make sure that indicators are properly updated, modify your main game directive to use the scope variables.

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

To test the scope binding, add these three lines at the end of handleComplete() method.

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

When you run the application you should see the score and life indicators.

score and life indicators

Additional white space, on the right of the page, will continue to be present because we are still hardcoding the game’s width and height at this point in our HTML5 game programming tutorial.

Adapting the Game Width

AngularJS is packed with useful methods and services. One of them is $window, which provides an innerWidth property that we will use to calculate the position of our elements.

Modify your app/view1/view1.js to inject $window service.

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

Extend the main game directive with width and height properties and that’s it!

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

...

Now you have the game adjusting itself to width of the browser window.

If you want to port this into a mobile app, I suggest reading my other mobile app development tutorial about using Ionic framework to create mobile apps. You should be able to create an ionic seed app, copy all the code from this project, and start playing the game on your mobile device in less than an hour.

The only thing I am not covering here is collision detection. To learn more about it, I read this article.

Wrap Up

I believe that through the course of this game development tutorial you realized that AngularJS and CreateJS are a winning duo for HTML5 based game development. You’ve got all the basics and I’m sure you recognized the benefits of combining these two platforms.

You can download the code for this article from GitHub, feel free to use, share, and make it your own.

About the author

Avinash Kaza, United States
member since October 17, 2013
Avinash is a senior developer with experience designing & developing data visualizations that answer difficult questions by using the latest tools like HTML5 Canvas. A seasoned product development expert who coaches teams on collaboration while figuring out the best solutions to difficult problems. [click to continue...]
Hiring? Meet the Top 10 Freelance Web Developers for Hire in September 2016

Comments

Cosmítar
Great example about AngularJS and CreateJS interaction. I've been testing these frameworks a couple of month ago. Also I ported a flash game using AngularJS and LiquidFun (Box2D based) you can check the result: http://www.afterseven.com.ar/futboltricks (It isn't in english but the game play is it relevant). I'll improve the code on my games with your examples here. Nice post!
Ezequiel De simone
Awesome thanks for share!
brianm101
Well if the demo doesn't work end of game I'm afraid!
Avinash
Sorry to hear that demo is not working for you Brian, It seems to work fine for me...can you check the inspector and tell me what is going on?
Evan Wieland
Very neat! Trying this out...
Janusz Kacalak
In case of my HTML5 game I took a different approach and relied on SVG and DOM. I made a remake of the classic pen & pencil Battleship game - http://battleships.mobi/ . The tool which helped me a lot with robust DOM manipulations was ReactJS. But I guess using DOM and SVG is enough for games where no sophisticated animations are needed for example of board or strategy genre.
Luiz
so... why not use a lib made for games? like http://phaser.io/ ? they are made for that, so you don't need to recreate the wheel...
Imal Hasaranga Perera
awesome article thanks....
SpoinkyNL
How is this ever going to work? if (hill.x hill.image.width * hill.scaleX <= 0) {
Jonathan Samples
thanks for the tutorial but your original directive has some missing operators. Not that big of a deal, I was able to figure it out but you might want to fix them
Brett
It appears some `+` signs were removed. If you get console errors find the offending line and add a `+` where needed.
James Adams
Useful Information, Great work guys, keep it up.
Jimbo Slice
All of the Angular examples I've seen were super basic. It's cool to see that you can make games too with Angular.
Michael Plautz
Thanks for the comments above (below?) I saw that there were a few +'s missing, so I added them where ever my IDE told me there were JavaScript errors. Then after that, right prior to running the app for the very first time, I got a few other errors where angular was complaining about missing directives. Turns out I just had to comment out a few lines on the updated app.js: 'myApp.services' and 'myApp.uiClasses'. Once I commented out those two lines, I got the app working just nicely. Also, I had to make sure that the <script> for spriteSheetRunner.js was located after all of the <script> tags within the <body>, not the <head>. Good deal!
Teeriq
I've noticed a couple of compiler errors and I'm not sure how to fix them. For instance: "var position = grant.x 150 * deltaS;"
comments powered by Disqus
Subscribe
The #1 Blog for Engineers
Get the latest content first.
No spam. Just great engineering and design posts.
The #1 Blog for Engineers
Get the latest content first.
Thank you for subscribing!
You can edit your subscription preferences here.
Trending articles
Relevant technologies
About the author
Avinash Kaza
JavaScript Developer
Avinash is a senior developer with experience designing & developing data visualizations that answer difficult questions by using the latest tools like HTML5 Canvas. A seasoned product development expert who coaches teams on collaboration while figuring out the best solutions to difficult problems.