at every tick until the game ends, do: input, update, render
Free
Best for rapid prototyping (Easy to learn)
Pixel perfect collision
Professional-grade (Level Editor, Debug Tools, Own Build Platform)
$99
Suite of libraries
Mimics the Flash display model
Sponsored by Adobe and MS
git clone <repository url>
git checkout skeleton
<script data-main='./js/main.js' src='./js/lib/require.js'></script>
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()
//initialize canvas and stage
this.canvas = $('#game_canvas')[0];
this.stage = new createjs.Stage(this.canvas);
createjs.Touch.enable(this.stage);
//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);
//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
//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);
tick : function(){
//this will be called every tick
}
this.stage.update();
tick : function(){
this.stage.update(); //update the stage every tick
}
//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
//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.
//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);
createjs.Ticker.addListener(this);
createjs.CSSPlugin.install(createjs.Tween);
//kill the tick listeners
createjs.Ticker.removeListener(this);
//remove children
this.stage.removeAllChildren();
This is the meat of the game.
Do the usual initialization stuff from before. But this time, we'll also include the game entities:
Spatial Data: .width, .height, .x, .y, .velocity
Collision Data: .boundingBox
Graphics Data: .graphics
- update()
- render()
- getFuturePosition()
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
// 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
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
//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
//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);
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
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;
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;
}
//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
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();
}
//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
// 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
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;
}
//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
});
//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;
// 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();
}
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
// 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.
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
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
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);
}
}
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)
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
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;
}
}
_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;
}
}
_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;
}
}
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
//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;
}
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;
}
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;
}
}