Super Browser 2 Turbo HD Remix:
Intro to HTML5 Game Dev

Seth Ladd
May 10, 2011

Hi, I'm Seth.

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

Tweet it up: #io2011 #chrome

group of young guys around a table playing a D&D game and drinking soda

http://www.flickr.com/photos/riggzy/4924270938/

And then there was Doom

group of young guys around a table playing computer games

http://www.flickr.com/photos/nickstone333/2739662702

Games, meet HTML5

Virtually everything we're doing now is against the Digital opportunity. It's where we see our growth.
EA CEO John Riccitiello, May '11

Source: http://seekingalpha.com/article/267901-electronic-arts-ceo-discusses-q4-2011-results-earnings-call-transcript

The US virtual goods market will reach $2.1 billion overall in 2011.
Inside Virtual Goods report, Sept '10

Source: http://venturebeat.com/2010/09/28/u-s-virtual-goods-market-to-hit-2-1-billion-in-2011/

Gamers upgrade

Many computer CPUs stacked on top of each other

Games create emotional responses

Guy winning at video games

What are we up to?

No skillz, No problem

Let's build a game!

Bad Aliens

Break it down

Break it down (cont.)

Objective: Bootstrap

Bootstrap Code: Canvas

<!DOCTYPE html>
<html>
  <head>
      <meta charset="utf-8">
      <title>Bad Aliens</title>
  </head>
  <body>
      <canvas id="surface" width="1024" height="768"></canvas>
  </body>
</html>

100% more Void of Space!

Objective: Rendering Images

Rendering Images: Technologies and Techniques

Asset Management Code

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

Asset Management Code (cont.)

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

Canvas Translation

Rendering Images Code

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

100% more Images and Coordinate System!

Objective: Flying Aliens

Flying Aliens Code: Game engine

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

Flying Aliens Code: Game loop

GameEngine.prototype.loop = function() {
  var now = Date.now();
  this.deltaTime = now - this.lastUpdateTimestamp;
  this.update();
  this.draw();
  this.lastUpdateTimestamp = now;
}

Flying Aliens Code: Game engine start

GameEngine.prototype.start = function() {
  console.log("starting game");
  this.lastUpdateTimestamp = Date.now();
  var that = this;
  (function gameLoop() {
      that.loop();
      requestAnimFrame(gameLoop, that.ctx.canvas);
  })();
}

Flying Aliens Code: 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

Flying Aliens Code: Alien

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

Flying Aliens Code: Draw images centered

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

100% more Aliens and Uh Oh!

Objective: Fix Timing (no more wormholes)

Fix Timing Code: Timer

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

Fix Timing Code: Game loop using timer

GameEngine.prototype.loop = function() {
  this.clockTick = this.timer.tick();
  this.update();
  this.draw();
}

Fix Timing Code: Alien velocity using ticks

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

100% less Worm Holes!

Objective: Orient Alien Ships

Orient Alien Ships: Technologies and Techniques

Canvas transformations from MDC

Orient Alien Ships Code: 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

Orient Alien Ships Code: 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);
}

100% more Correct Rotation!

Objective: Optimize Canvas Code, Save Battery

Optimize Canvas Code Code: Off screen canvas

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

Optimize Canvas Code Code: Draw canvas into canvas

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

Optimize Canvas Code: Benefits of Caching

100% more Efficient Image Rotation!

Objective: Turret and Tracking

Turret and Tracking Code: Event handlers

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)

Turret and Tracking Code: Event handlers

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};
  }
  
  ...

Turret and Tracking Code: Rotate sentry turret based on mouse position

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

Turret and Tracking Code: Clearing the click in the loop

GameEngine.prototype.loop = function() {
  this.clockTick = this.timer.tick();
  this.update();
  this.draw();
  this.click = null;
}

100% more Turret and Mouse Tracking!

Objective: Shooting Lasers

Shooting Lasers Code: Blast 'em!

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

Shooting Lasers Code: Remove laser bullets when leave the canvas

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

Shooting Lasers Code: Clear entities from game world

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

100% more Firing Lasers!

Objective: Explosions!

Explosions Code: Animation

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

Explosions Code: Animate explosions over time

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());
  ...
}

Explosions Code: Replace bullets with explosions

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 {
  ...
}

100% more Explosions!

Objective: Sounds

Sounds Code: Add sounds to Asset Manager

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

Sounds Code: Play sounds

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 {
  ...
}

100% more, well, Sounds!

Objective: Blow Up Alien Ships

var distance_squared = square(x1 - x2) + square(y1 - y2);
var radii_squared = square(radius1 + radius2);
return distance_squared < radii_squared;  // true if intersect

Blow Up Alien Ships Code: Bullets collide with Aliens

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

TODO: Optimize with spatial partitioning.

Blow Up Alien Ships Code: Protip: Draw circles around objects for visual debugging

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

100% more Aliens Exploding!

Objective: Display Game Stats

Display Game Stats Code: 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);
}

100% more Scores!

Objective: Measure Performance

Measure Performance: stats.js

Measure Performance Code: stats.js

function GameEngine() {
  ...
  this.stats = new Stats();
}

GameEngine.prototype.init = function(ctx) {
  ...
  document.body.appendChild(this.stats.domElement);
}

GameEngine.prototype.loop = function() {
  ...
  this.stats.update();
}

Measure Performance: Chrome FPS Counter

100% more How We Doin'?

Objective: Offline Play

Offline Play: Application Cache

Offline Play Code: manifest.appcache

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

Offline Play Code: index.html

<!DOCTYPE html>
<html manifest="manifest.appcache">
  <head>
    <meta charset="utf-8">
    <title>Evil Aliens 2</title>

100% more Aliens on a Plane!

game works offline

Don't start from scratch

Other HTML5 game engines

Mobile and HTML5

But I can't draw!?

Distribution

Chrome Web Store

World Golf Tour + Chrome Web Store FTW

"Chrome users play 23% more than those from other ... sources, and are buying virtual goods ... [at] 147% more than average."

World Golf Tour Logo

Learn more

Learn More

Blogs, tweets, tutorials, oh my!

Just remember...

Go make memories and all-nighters!

Thanks!