HTML5 

Game Development



THEORY TIME

1/3

THEORY TIME

2/3


at every tick until the game ends, do: input, update, render

THEORY TIME

3/3


Scene Graph

Frameworks / Engines


jawsjs


Free

Best for rapid prototyping (Easy to learn)

Pixel perfect collision

Frameworks / Engines


IMPACTJS


Professional-grade (Level Editor, Debug Tools, Own Build Platform)

$99

Frameworks / Engines


CREATEJS


Suite of libraries

Mimics the Flash display model

Sponsored by Adobe and MS

GAME DEVELOPMENT 

IN 

CREATEJS

ENDLESS RUNNER GAME


PWDO-RUNMAN

FORK:
https://github.com/zerojuan/pwdo-runman
Download:
https://github.com/zerojuan/pwdo-runman/downloads

DEPENDENCIES:
-- require.js
-- preload.js
-- easel.js
-- sound.js
-- tween.js

PROJECT SETUP

If you forked from git, clone and checkout the 'skeleton' branch
git clone <repository url>
git checkout skeleton

Note: you need to run the project on a http server. Some browsers don't like it when you access local directories

Folder Structure

  • assets/ - art and sound resources
  • js/ - scripts
  • index.html - starting point

USING REQUIRE.JS

Working with a lot of .js files in one project can be very difficult. Specially if some files depend on another.

We need to keep our javascripts modular.


<script data-main='./js/main.js' src='./js/lib/require.js'></script>
'./js/main.js' is our springboard for require.js

CODE PLANNING

1. List the game screens or states that the game will go through

2. List the game objects that will interact in the system

GAME STATES

  • Preloader
  • Menu
  • Play
  • Game Over

Game OBJECTS

  • Hero
  • Platform
  • Background

JS Files BASED ON PLAN

	paths : {
		// LOAD OUR OWN MODULES
		'App' : 'app',
		'Preloader' : 'states/preloader',
		'Play' : 'states/play',
		'Menu' : 'states/menu',
		'GameOver' : 'states/gameover',
		'Tilemap' : 'utils/tilemap',		
		'ParallaxLayer' : 'entities/parallaxlayer',
		'Hero' : 'entities/hero',
		'Platform' : 'entities/platform',
		'PlatformManager' : 'entities/platformmanager'
	}    

        urlArgs : "bust="+(new Date()).getTime()
        

App.JS

1. Initialize canvas

2. Handle game states

Initialize Canvas



//initialize canvas and stage
this.canvas = $('#game_canvas')[0];
this.stage = new createjs.Stage(this.canvas);

createjs.Touch.enable(this.stage);
            

GAME STATE CONVENTIONS

  1. enter(canvas, stage, [..]) - setup the state
  2. onExit - callback for when state exits

states/preloader.js


//call preload, and install soundjs as plugin
this.loader = new createjs.PreloadJS();
this.loader.installPlugin(createjs.SoundJS);
//define callbacks
this.loader.onFileLoad = function(loadedFile){
	that.handleFileLoad(loadedFile);
};

this.loader.onComplete = function(){
	that.handleComplete();
}
//load file from manifest
this.loader.loadManifest(assetManifest);

App.js


//start preloader
Preloader.enter(this.canvas, this.stage);
Preloader.onExit = function(assets){
	console.log('Preloading done..');
	that.assets = assets;						
	that.gotoMenu();
}
in the onExit callback, we store the assets that were loaded first before going to the next state

Write Functions For State Handling

gotoMenu()
gotoPlay()
gotoGameOver()

states/menu.js


//draw background
var gfx = new createjs.Graphics().
	beginBitmapFill(this.assets['sky']).
	drawRect(0, 0, this.canvas.width, this.canvas.height).
	endFill();
var background = new createjs.Shape(gfx);
background.x = 0;
//add to display list
this.stage.addChild(background);
//listen to ticks
createjs.Ticker.setFPS(40);
createjs.Ticker.addListener(this);

states/menu.js


when listening for ticks, make sure to implement a tick() function:
tick : function(){
    //this will be called every tick            
}

** EASELJS GOTCHA **


You won't see whatever you add to the stage until you call update()
this.stage.update();
tick : function(){
    this.stage.update(); //update the stage every tick
}

Using DOM for UI


//initialize our DOM based UI
$('.ui').css('display', 'none');
var menuDiv = $('#menuDiv');
menuDiv.css('display', 'block');

this.title = menuDiv.find('#title');
this.start = menuDiv.find('#start');

this.title.css('opacity', 0);
this.title.css('position', 'absolute');
I'm using jquery here, but native DOM is cool too

Using DOM for UI


//img tags can also be used as bitmap
var startBtn = new createjs.Bitmap(this.start[0]);
this.start.remove(); //*quirk* mouse events don't work unless you remove the dom element first
startBtn.onClick = function(evt){
	that.exit();
}
Note: createjs.DOMElement can wrap a dom element with createjs functionalities too but it's still buggy at the moment.

Introducing TweenJS


//do some simple tweening
createjs.Tween
	.get(this.title[0])
	.set({top: 0, left: 150}, this.title[0].style)
	.wait(500)
	.to({opacity: 1, top: 150}, 1000, createjs.Ease.easeIn);

** TWEENJS GOTCHA **


Tweens are timeline dependent
createjs.Ticker.addListener(this);

Tweening CSS attributes requires the CSSPlugin
createjs.CSSPlugin.install(createjs.Tween);

Menu State. Done.


Kill listeners!
//kill the tick listeners
createjs.Ticker.removeListener(this);

Remove children!
//remove children
this.stage.removeAllChildren();

states/Play.js


This is the meat of the game.


Do the usual initialization stuff from before. But this time, we'll also include the game entities:

  • Hero
  • Platform
  • ParallaxLayer

Game Entities Convention


Spatial Data: .width, .height, .x, .y, .velocity

Collision Data: .boundingBox

Graphics Data: .graphics


- update()

- render()

- getFuturePosition()

entities/Hero.js

this.width = 30;
this.height = 30;
this.x = 0;
this.y = 0;
this.spriteSheet = null;	
this.velocity = {x: 0, y: 0};

for(var prop in opts){		
	this[prop] = opts[prop];		
}

this.alive = true;
this.onGround = false;

// setup bounding box with an offset of x: 20, y: 20
this.boundingBox = new createjs.Rectangle(20, 20, this.width, this.height);
give our Hero some spatial data

entities/Hero.js


// Bounding box graphics					
var boundingBoxGfx = new createjs.Graphics();
boundingBoxGfx.beginStroke('#00ff00')
	.drawRect(this.boundingBox.x, 
			this.boundingBox.y, 
			this.boundingBox.width, 
			this.boundingBox.height);
var debugBox = new createjs.Shape(boundingBoxGfx);
// Create our graphics container
this.graphics = new createjs.Container();		
this.graphics.addChild(debugBox);

this.graphics.x = this.x;
this.graphics.y = this.y;
And some graphics

entities/Hero.js


update : function(){						
	this.x += this.velocity.x;
	this.y += this.velocity.y;

	if(this.y > 800){
		this.y = -50;
	}
}
render : function(){						
	this.graphics.x = this.x;
	this.graphics.y = this.y;
}
Let's keep update and render separate so our drawing logic doesn't mix with our logic logic

states/Play.js

//setup spritesheets
var spriteSheetData = {
	animations : {
		run : {
			frames : [0, 1, 2, 3, 4, 5],
			frequency: 2
		},
		jump : {
			frames : [6, 7, 8, 9, 8],
			frequency: 2,
			next : 'false'
		}
	},
	frames : {
		width : 68.5, height: 57
	},
	images : ['assets/funrunframes.gif']
};
declare our spritesheet data

states/Play.js


//initialize hero
var ss = new createjs.SpriteSheet(spriteSheetData);
this.hero = new Hero({
	spriteSheet : ss,
	x : 100,
	y: 100,
	velocity : {x: 0, y:5}
});
this.stage.addChild(this.hero.graphics);

states/Play.js


tick : function(){			
	this.hero.update();							
			
	this.hero.render();			

	this.stage.update();
}
Note: don't forget to add the tick listener or tick() won't get called

entities/ParallaxLayer.js


var graphicsLocal = new createjs.Graphics()
	.beginBitmapFill(this.bitmap)
	.drawRect(0,0, this.width, this.height)
	.endFill();

//create two copies of the image		
this.shapeA = new createjs.Shape(graphicsLocal);
this.shapeB = new createjs.Shape(graphicsLocal);
//position our 2nd image to the left of the 1st one
this.shapeB.x = this.width;

entities/ParallaxLayer.js


update : function(){
    //keep accelerating the x velocity
	this.velocity.x += this.acceleration;				
}
render : function(){			
    //if shapeA has moved completely off the left screen
	if(this.shapeA.x < this.outside){
        //move it to the back of shapeB
		var temp = this.shapeA;
		temp.x = this.shapeB.x+this.width;
        //switch shapeA to shapeB and shapeB to shapeA
		this.shapeA = this.shapeB;
		this.shapeB = temp;
	}
	
	this.shapeA.x += this.velocity.x;
	this.shapeB.x += this.velocity.x;
}

states/Play.js

//initialize parallax layer
this.parallaxLayer = [];
for(var i in this.assets){
	var result = this.assets[i];

	switch(i){
		case "sky" :
			that.parallaxLayer['sky'] = new ParallaxLayer({
				bitmap: result, 
				x: 0, y: 0, 
				width: 800,
				height: 600, 
				velocity: {x: 0, y: 0},
				acceleration : 0
			});
			break;
		...		
	}
}
loud our parallax assets

states/play.js


Add the layers to our addChild call
this.stage.addChild(
	this.parallaxLayer['sky'].graphics,
	this.parallaxLayer['ground1'].graphics,
	this.parallaxLayer['ground2'].graphics,
	this.hero.graphics
);
Remember to call update and render in our tick function
for(var i in this.parallaxLayer){
	this.parallaxLayer[i].update();
	this.parallaxLayer[i].render();
}

entities/platform.js


//width and height are based on rows and cols
this.width = this.cols * this.tileWidth;
this.height = this.rows * this.tileHeight;
this.outside = -this.width;
Same as the Hero entity, except width and height is by rows and cols

entities/platform.js


// Setup bounding box
this.boundingBox = new createjs.Rectangle(0, 8, this.cols * this.tileWidth, this.rows * this.tileHeight);
var boundingBoxGfx = new createjs.Graphics();
	boundingBoxGfx.beginStroke('#00ff00')
		.drawRect(
			this.boundingBox.x, this.boundingBox.y, 
			this.boundingBox.width, this.boundingBox.height);
var debugBox = new createjs.Shape(boundingBoxGfx);
Note the +8 y-offset in the boundingBox

entities/platform.js


update : function(){	
	this.velocity.x += this.acceleration;
	this.x += this.velocity.x;
        //keep track when this platform has 
        //moved out of the left screen
	if(this.x < this.outside){
		this.isOutsideLeft = true;
	}
}
render : function(){
	this.graphics.x = this.x;
}

entities/platform.js


We can add a platform into the stage now, but it would be really great if we have a class that will manage the creating and updating of many platforms for us.

entities/platformmanager.js


//precreate the platforms to memory
var startPlatform = new Platform({
	tilesheet : this.bitmap,
	rows : 60,
	cols : 20,
	y : 300,
	acceleration : this.acceleration
});
var platform2 = new Platform({
	tilesheet : this.bitmap,
	rows : 30,
	cols : 20,
	y : 400,
	x : 600,
	acceleration : this.acceleration
});

entities/platformmanager.js


//put the platforms in a list
this.collidables.push(startPlatform);
this.collidables.push(platform2);
// add platform graphics to display list
this.graphics = new createjs.Container();
for(var i in this.collidables){
	this.graphics.addChild(this.collidables[i].graphics);
}
//keep a reference to the last platform
this.lastPlatformIndex = this.collidables.length - 1;

entities/platformmanager.js

// if a platform is on the left offscreen
if(this.collidables[i].isOutsideLeft){
	//move the left platform to the back of the last platform
	var lastPlatform = this.collidables[this.lastPlatformIndex];
	var lastX = lastPlatform.x + lastPlatform.width;

        //randomly give this platform a new shape
        //to create variety
	this.collidables[i].reset({
		cols: Math.abs(Math.random() * 40 - 20),
		y : lastPlatform.y + (Math.random() * 100 - 50), 
		x: lastX + Math.random() * 100 + 100});
	this.lastPlatformIndex = i;
};
//render
for(var i in this.collidables){
	this.collidables[i].render();
}

entities/platform.js


Create the reset() function.
Reconstruct the platform graphics and spatial data.
Don't recreate the container graphics.

states/play.js


case "platforms" : 
	that.platformManager = new PlatformManager({
		bitmap : result, 
		x : 0, y: 0, 
		acceleration: -.01
	});
	break;
this.platformManager.update();

this.platformManager.render();
Don't forget to add the platformManager to the display list

Add some more graphics


Placeholder art is nice but boring.

We need colorful pixels!

entities/hero.js


// setup animation
this.animation = new createjs.BitmapAnimation(this.spriteSheet);
this.animation.gotoAndPlay('run');
this.graphics.addChild(this.animation, debugBox);
Remember the spritesheet we passed to Hero? Let's animate that.

entities/platform.js


var map = [];
for(var i=0; i < this.cols; i++){
	map.push([]);
	for(var j=0; j < this.rows; j++){
		if(j == 0){					
			map[i][j] = 1;
		}else{
			map[i][j] = 0;
		}
	}
}		

var tilemap = new Tilemap({tileSheet : this.tilesheet, map : map});
We have to use a special Tilemap class for the platforms

utils/tilemap.js


var p = Tilemap.prototype = new createjs.DisplayObject();

p.DisplayObject_initialize = p.initialize;

p.DisplayObject_draw = p.draw;
When prototyping/subclassing a createjs display object, remember DisplayObject_initialize and DisplayObject_draw

Let them collide


Detecting if two objects collided is one of the staple codes for game developers.
Everyone has their own idea on how to implement it.
Here's one:

states/play.js

Inside the tick function, call our collide logic
this.collideWithGroup(this.hero, this.platformManager);
collideWithGroup : function(objA, objB){			
	var groupB = objB.collidables;
	for(var i in groupB){				
		this.collides(objA, groupB[i], objA.collide, objB.collide);
	}
}

states/play.js

var rect1 = objA.boundingBox;
var rect2 = objB.boundingBox;
//calculate the overlap between the bounds
var r1={}, r2={};
r1.left = rect1.x + objA.getFuturePosition().x;
r1.top = rect1.y + objA.getFuturePosition().y;
r1.right = r1.left + rect1.width;
r1.bottom = r1.top + rect1.height;
r2.left = rect2.x + objB.getFuturePosition().x;
r2.top = rect2.y + objB.getFuturePosition().y;
r2.right = r2.left + rect2.width;
r2.bottom = r2.top + rect2.height;
var x_overlap = Math.max(0, Math.min(r1.right, r2.right) - Math.max(r1.left, r2.left));
var y_overlap = Math.max(0, Math.min(r1.bottom, r2.bottom) - Math.max(r1.top, r2.top));
collides(objA, objB, objACallback, objBCallback)

states/play.js


if (x_overlap > 0 && y_overlap > 0) {	
	objACallback.call(objA, objB, {width: x_overlap, height: y_overlap})						   
}
a collision happened, if the two bounding boxes overlapped

entities/hero.js


collide : function(objB, data){
        //check if the hero collided from the top/bottom 
        //or from the sides
	if(data.width < data.height){
		this._separateX(objB, data);	
	}else{
		this._separateY(objB, data);		
		this.collision = data;
	}
}

entities/hero.js


_separateX : function(objB, data){
	var overlap = data.width;
	var objBX = objB.getFuturePosition().x;		

	if(objBX > this.x){
		//Collided on 'right';			
		this.x -= overlap;        
                //'absorb' the velocity of the collided object
		this.velocity.x = objB.velocity.x;
	}else{
		//Collided on 'left';
		this.x += overlap;
	}
}

entities/hero.js


_separateY : function(objB, data){
	var overlap = data.height;	

	if(overlap > 1 ){
		//Collided on bottom
		this.y = (objB.y + objB.boundingBox.y) - this.boundingBox.height - this.boundingBox.y;
		this.velocity.y = 0;			
	}
}

entities/hero.js


if(this.collision){		
	this.onGround = true;				
	this.collision = null;
}else{							
	this.velocity.y += this.acceleration;			
}
In the update function, only apply 'gravity' velocity if there were no collisions

User input


Listening to mouse or touch events in createjs is easy
//bind to mouseup event
this.stage.onMouseUp = function(evt){
	that.handleInput(evt);
}
handleInput : function(){
	this.jumpClicked = true;
}
if(this.jumpClicked){
	this.hero.jump();
	this.jumpClicked = false;
}

entities/hero.js


jump : function(){	
	this.animation.gotoAndPlay('jump');
	this.onGround = false;
	this.velocity.y = -10;
}
if(this.collision){	
	if(this.animation.currentAnimation != 'run')
		this.animation.gotoAndPlay('run');
	this.onGround = true;				
	this.collision = null;
}

Finishing the game


  1. Listen for a death condition
  2. Exit play state

Finishing the game


if(this.hero.alive){
	if(this.jumpClicked){	
		this.hero.jump();
		this.jumpClicked = false;
	}
	//update			
	this.collideWithGroup(this.hero, this.platformManager);

	this.hero.update();				
}else{
	//show death state
	if(!this.gameOver){
		this.exit();
		this.gameOver = true;
	}		
}

Exercises (quests?)


Beginner

  1. Hide debug boxes
  2. Add more platforms and make sure they don't go over the top or the bottom
  3. Clip the world to a maximum velocity

Exercises (quests?)


Normal

  1. Track the player's score and display it in a DOMElement
  2. Play audio on jump (./assets/jump.wav)
  3. Create the GameOver state that shows player score

Exercises (quests?)


Hardcore

  1. Have the player collect coins
  2. Add different types of platforms with different graphics
  3. Player scores are kept in a leaderboard (database)