Seth Ladd
May 10, 2011
I'll be your Developer Advocate this afternoon.
Chrome + Games FTW
Chat me up: @sethladd or blog.sethladd.com or at the show
Google Moderator: j.mp/games-qa
Virtually everything we're doing now is against the Digital opportunity. It's where we see our growth.
The US virtual goods market will reach $2.1 billion overall in 2011.
Source: http://venturebeat.com/2010/09/28/u-s-virtual-goods-market-to-hit-2-1-billion-in-2011/
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bad Aliens</title>
</head>
<body>
<canvas id="surface" width="1024" height="768"></canvas>
</body>
</html>
drawImage(img,x,y)
translate(x,y)
function AssetManager() {
this.successCount = 0;
this.errorCount = 0;
this.cache = {};
this.downloadQueue = [];
}
AssetManager.prototype.queueDownload = function(path) {
this.downloadQueue.push(path);
}
AssetManager.prototype.isDone = function() {
return (this.downloadQueue.length == this.successCount + this.errorCount);
}
AssetManager.prototype.downloadAll = function(callback) {
for (var i = 0; i < this.downloadQueue.length; i++) {
var path = this.downloadQueue[i];
var img = new Image();
var that = this;
img.addEventListener("load", function() {
that.successCount += 1;
if (that.isDone()) { callback(); }
});
img.addEventListener("error", function() {
that.errorCount += 1;
if (that.isDone()) { callback(); }
});
img.src = path;
this.cache[path] = img;
}
}
ASSET_MANAGER.queueDownload('img/earth.png');
ASSET_MANAGER.downloadAll(function() {
var x = 0, y = 0;
var sprite = ASSET_MANAGER.getAsset('img/earth.png');
// move coordinate system to center of canvas
ctx.translate(canvas.width/2, canvas.height/2);
// draw image centered
ctx.drawImage(sprite, x - sprite.width/2, y - sprite.height/2);
});
update() and draw()
setInterval() and setTimeout()
requestAnimationFrame(callback,element)
function GameEngine() {
this.entities = [];
this.ctx = null;
this.lastUpdateTimestamp = null;
this.deltaTime = null;
}
GameEngine.prototype.draw = function(callback) {
// loop through all entities, call draw()
}
GameEngine.prototype.update = function() {
// loop through all entities, call update()
}
GameEngine.prototype.loop = function() {
var now = Date.now();
this.deltaTime = now - this.lastUpdateTimestamp;
this.update();
this.draw();
this.lastUpdateTimestamp = now;
}
GameEngine.prototype.start = function() {
console.log("starting game");
this.lastUpdateTimestamp = Date.now();
var that = this;
(function gameLoop() {
that.loop();
requestAnimFrame(gameLoop, that.ctx.canvas);
})();
}
requestAnimationFrame()
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(/* function */ callback, /* DOMElement */ element){
window.setTimeout(callback, 1000 / 60);
};
})();
Learn more: requestAnimationFrame for Smart Animating
Alien.prototype.update = function() {
this.x = this.radial_distance * Math.cos(this.angle);
this.y = this.radial_distance * Math.sin(this.angle);
this.radial_distance -= this.speed * this.game.deltaTime;
Entity.prototype.update.call(this);
}
Alien.prototype.draw = function(ctx) {
this.drawSpriteCentered(ctx);
Entity.prototype.draw.call(this, ctx);
}
Entity.prototype.drawSpriteCentered = function(ctx) {
var x = this.x - this.sprite.width/2;
var y = this.y - this.sprite.height/2;
ctx.drawImage(this.sprite, x, y);
}
function Timer() {
this.gameTime = 0;
this.maxStep = 0.05;
this.wallLastTimestamp = 0;
}
Timer.prototype.tick = function() {
var wallCurrent = Date.now();
var wallDelta = (wallCurrent - this.wallLastTimestamp) / 1000;
this.wallLastTimestamp = wallCurrent;
var gameDelta = Math.min(wallDelta, this.maxStep);
this.gameTime += gameDelta;
return gameDelta;
}
GameEngine.prototype.loop = function() {
this.clockTick = this.timer.tick();
this.update();
this.draw();
}
Alien.prototype.update = function() {
this.x = this.radial_distance * Math.cos(this.angle);
this.y = this.radial_distance * Math.sin(this.angle);
this.radial_distance -= this.speed * this.game.clockTick;
Entity.prototype.update.call(this);
}
rotate(radians) clockwise
rotate() from Canvas origin
translate() to the rescue!
rotate() examples// Step One ctx.translate(x, y); // set 0,0 to entity's location ctx.drawImage(image, 0, 0);
// Step Two ctx.translate(x, y); ctx.rotate(Math.PI/4); // rotate by 45 degrees ctx.drawImage(image, 0, 0);
// Step Three ctx.translate(x, y); ctx.rotate(Math.PI/4); ctx.drawImage(image, -image.width/2, -image.height/2); // center image on location
translate() and rotate()
Alien.prototype.draw = function(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.angle + Math.PI/2);
ctx.drawImage(this.sprite, -this.sprite.width/2, -this.sprite.height/2);
ctx.restore();
Entity.prototype.draw.call(this, ctx);
}
Entity.prototype.rotateAndCache = function(image) {
var offscreenCanvas = document.createElement('canvas');
var offscreenCtx = offscreenCanvas.getContext('2d');
var size = Math.max(image.width, image.height);
offscreenCanvas.width = size;
offscreenCanvas.height = size;
offscreenCtx.translate(size/2, size/2);
offscreenCtx.rotate(this.angle + Math.PI/2);
offscreenCtx.drawImage(image, -(image.width/2), -(image.height/2));
return offscreenCanvas;
}
function Alien(game, radial_distance, angle) {
...
this.sprite = this.rotateAndCache(ASSET_MANAGER.getAsset('img/alien.png'));
...
}
Alien.prototype.draw = function(ctx) {
var x = this.x - this.sprite.width/2;
var y = this.y - this.sprite.height/2;
ctx.drawImage(this.sprite, x, y);
}
click and mousemove events
update()
GameEngine.prototype.startInput = function() {
...
this.ctx.canvas.addEventListener("click", function(e) {
that.click = getXandY(e);
}, false);
this.ctx.canvas.addEventListener("mousemove", function(e) {
that.mouse = getXandY(e);
}, false);
x = e.clientX
x = e.clientX - canvas.getBoundingClientRect().left
e.clientX - canvas.getBoundingClientRect().left - (canvas.width/2)
GameEngine.prototype.startInput = function() {
var getXandY = function(e) {
// offset to canvas element
var x = e.clientX - that.ctx.canvas.getBoundingClientRect().left;
// offset to 0,0 in middle of canvas
x -= (that.ctx.canvas.width/2);
// same thing for y
return {x: x, y: y};
}
...
Sentry.prototype.update = function() {
if (this.game.mouse) {
this.angle = Math.atan2(this.game.mouse.y, this.game.mouse.x);
if (this.angle < 0) {
this.angle += Math.PI * 2;
}
this.x = (Math.cos(this.angle) * this.distanceFromEarthCenter);
this.y = (Math.sin(this.angle) * this.distanceFromEarthCenter);
}
Entity.prototype.update.call(this);
}
GameEngine.prototype.loop = function() {
this.clockTick = this.timer.tick();
this.update();
this.draw();
this.click = null;
}
removeFromWorld bit to Entities
removeFromWorld in GameEngine update()
Sentry.prototype.update = function() {
...
if (this.game.click) {
this.shoot();
}
...
}
Sentry.prototype.shoot = function() {
var bullet = new Bullet(this.game, this.x, this.y,
this.angle, this.game.click);
this.game.addEntity(bullet);
}
Bullet.prototype.update = function() {
if (this.outsideScreen()) {
this.removeFromWorld = true;
} else {
this.x = this.radial_distance * Math.cos(this.angle);
this.y = this.radial_distance * Math.sin(this.angle);
this.radial_distance += this.speed * this.game.clockTick;
}
}
GameEngine.prototype.update = function() {
var entitiesCount = this.entities.length;
for (var i = 0; i < entitiesCount; i++) {
var entity = this.entities[i];
if (!entity.removeFromWorld) {
entity.update();
}
}
for (var i = this.entities.length-1; i >= 0; --i) {
if (this.entities[i].removeFromWorld) {
this.entities.splice(i, 1);
}
}
}
Animation.prototype.drawFrame = function(tick, ctx, x, y, scaleBy) {
...
this.elapsedTime += tick;
...
ctx.drawImage(this.spriteSheet,
this.currentFrame()*this.frameWidth, 0, // source from sheet
this.frameWidth, this.frameHeight,
locX, locY,
this.frameWidth*scaleBy,
this.frameHeight*scaleBy);
}
Animation.prototype.currentFrame = function() {
return Math.floor(this.elapsedTime / this.frameDuration);
}
Animation.prototype.isDone = function() {
return (this.elapsedTime >= this.totalTime);
}
function BulletExplosion(game, x, y) {
...
this.sprite = ASSET_MANAGER.getAsset('img/explosion.png');
this.animation = new Animation(this.sprite,
34, // frame width
0.05); // frame duration
}
BulletExplosion.prototype.draw = function(ctx) {
this.animation.drawFrame(this.game.clockTick, ctx, this.x, this.y,
this.scaleFactor());
...
}
Bullet.prototype.update = function() {
...
} else if (this.explodesHere()) {
this.game.addEntity(new BulletExplosion(this.game,
this.explodesAt.x,
this.explodesAt.y));
this.removeFromWorld = true;
} else {
...
}
AssetManager.prototype.downloadSounds = function(callback) {
var that = this;
soundManager.onready(function() {
// start download for each song
});
}
AssetManager.prototype.downloadSound = function(id, path, callback) {
var that = this;
this.cache[path] = soundManager.createSound({
id: id,
autoLoad: true,
url: path,
onload: function() {
that.successCount += 1;
if (that.isDone()) {
callback();
}
}
});
}
Bullet.prototype.update = function() {
...
} else if (this.explodesHere()) {
ASSET_MANAGER.getSound('audio/bullet_boom.mp3').play();
this.game.addEntity(new BulletExplosion(this.game,
this.explodesAt.x,
this.explodesAt.y));
this.removeFromWorld = true;
} else {
...
}
var distance_squared = square(x1 - x2) + square(y1 - y2); var radii_squared = square(radius1 + radius2); return distance_squared < radii_squared; // true if intersect
BulletExplosion.prototype.update = function() {
...
for (var i = 0; i < this.game.entities.length; i++) {
var alien = this.game.entities[i];
if (alien instanceof Alien && this.isCaughtInExplosion(alien)) {
this.game.score += 10;
alien.explode();
}
}
}
Entity.prototype.draw = function(ctx) {
if (this.game.showOutlines && this.radius) {
ctx.beginPath();
ctx.strokeStyle = "green";
ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2, false);
ctx.stroke();
ctx.closePath();
}
}
fillText()
drawText()
EvilAliens.prototype.draw = function() {
GameEngine.prototype.draw.call(this, function(game) {
game.drawScore();
game.drawLives();
});
}
EvilAliens.prototype.drawScore = function() {
this.ctx.fillStyle = "red";
this.ctx.font = "bold 2em Arial";
this.ctx.fillText("Score: " + this.score, x, y);
}
mozPaintCount
function GameEngine() {
...
this.stats = new Stats();
}
GameEngine.prototype.init = function(ctx) {
...
document.body.appendChild(this.stats.domElement);
}
GameEngine.prototype.loop = function() {
...
this.stats.update();
}
CACHE MANIFEST # version 5 CACHE: scripts/app.js scripts/soundmanager2-nodebug-jsmin.js scripts/stats.js swf/soundmanager2_flash9.swf audio/alien_boom.mp3 img/alien-explosion.png img/alien.png ... favicon.ico index.html
<!DOCTYPE html>
<html manifest="manifest.appcache">
<head>
<meta charset="utf-8">
<title>Evil Aliens 2</title>
"Chrome users play 23% more than those from other ... sources, and are buying virtual goods ... [at] 147% more than average."