SlideShare a Scribd company logo
Asteroid(s)*


                         with HTML5 Canvas




(c) 2012 Steve Purkis




                                        * Asteroids™ is a trademark of Atari Inc.
About Steve

Software Dev + Manager
  not really a front-end dev
         nor a game dev
           (but I like to play!)
Uhh...
Asteroids?


                      What’s that, then?




             http://www.flickr.com/photos/onkel_wart/201905222/
Asteroids™




      http://en.wikipedia.org/wiki/File:Asteroi1.png
Asteroids™
“ Asteroids is a video arcade game released in November 1979 by Atari
Inc. It was one of the most popular and influential games of the Golden
Age of Arcade Games, selling 70,000 arcade cabinets.[1] Asteroids uses
a vector display and a two-dimensional view that wraps around in both
screen axes. The player controls a spaceship in an asteroid field which
is periodically traversed by flying saucers. The object of the game is to
shoot and destroy asteroids and saucers while not colliding with either,
or being hit by the saucers' counter-fire.”
                     http://en.wikipedia.org/wiki/Asteroids_(video_game)




                                                             http://en.wikipedia.org/wiki/File:Asteroi1.png
Asteroids™
Note that the term “Asteroids” is © Atari when used with a game.
              I didn’t know that when I wrote this...
                              Oops.


Atari:
• I promise I’ll change the name of the game.
• In the meantime, consider this free marketing! :-)
Why DIY Asteroid(s)?

• Yes, it’s been done before.
• Yes, I could have used a Game Engine.
• Yes, I could have used open|web GL.
• No, I’m not a “Not Invented Here” guy.
Why not?

• It’s a fun way to learn new tech.
• I learn better by doing.

   (ok, so I’ve secretly wanted to write my own version of asteroids since I was a kid.)




                                                                       I was learning about HTML5 (yes I know it came out several years ago. I’ve been busy).
                                                                       A few years ago, the only way you’d be able to do this is with Flash.
                                                                                                                                                          demo
It’s not Done...




     (but it’s playable)
It’s a Hack!




       http://www.flickr.com/photos/cheesygarlicboy/269419718/
It’s a Hack!


• No Tests (!)
• somewhat broken in IE
• mobile? what’s that?
• no sound, fancy gfx, ...
                         http://www.flickr.com/photos/cheesygarlicboy/269419718/
If you see this...




     you know what to expect.
What I’ll cover...

• Basics of canvas 2D (as I go)
• Overview of how it’s put together
• Some game mechanics
• Performance
What’s Canvas?
• “New” to HTML5!
• lets you draw 2D graphics:
 •   images, text

 •   vector graphics (lines, curves, etc)

 •   trades performance & control for convenience


• ... and 3D graphics (WebGL)
 •   still a draft standard
Drawing a Ship
•     Get canvas element

•     draw lines that make up the ship
    <body onload="drawShip()">
        <h1>Canvas:</h1>
        <canvas id="demo" width="300" height="200" style="border: 1px solid black" />
    </body>


           function drawShip() {
             var canvas = document.getElementById("demo");
             var ctx = canvas.getContext('2d');

               var center = {x: canvas.width/2, y: canvas.height/2};
               ctx.translate( center.x, center.y );

               ctx.strokeStyle = 'black';                                     Canvas is an element.
               ctx.beginPath();                                               You use one of its ‘context’ objects to draw to it.

               ctx.moveTo(0,0);                                               2D Context is pretty simple
               ctx.lineTo(14,7);
                                                                              Walk through ctx calls:
               ctx.lineTo(0,14);                                              •     translate: move “origin” to center of canvas
               ctx.quadraticCurveTo(7,7, 0,0);                                •     moveTo: move without drawing
               ctx.closePath();                                               •     lineTo: draw a line
                                                                              •     curve: draw a curve
               ctx.stroke();
                                                                                                                        demo v
           }
                                     syntax highlighting: http://tohtml.com                                                     ship.html
Moving it around
var canvas, ctx, center, ship;                                               function drawShip() {
                                                                               ctx.save();
function drawShipLoop() {                                                      ctx.clearRect( 0,0, canvas.width,canvas.height );
  canvas = document.getElementById("demo");                                    ctx.translate( ship.x, ship.y );
  ctx = canvas.getContext('2d');                                               ctx.rotate( ship.facing );
  center = {x: canvas.width/2, y: canvas.height/2};
  ship = {x: center.x, y: center.y, facing: 0};                                  ctx.strokeStyle = 'black';
                                                                                 ctx.beginPath();
    setTimeout( updateAndDrawShip, 20 );                                         ctx.moveTo(0,0);
}                                                                                ctx.lineTo(14,7);
                                                                                 ctx.lineTo(0,14);
function updateAndDrawShip() {                                                   ctx.quadraticCurveTo(7,7, 0,0);
  // set a fixed velocity:                                                       ctx.closePath();
  ship.y += 1;                                                                   ctx.stroke();
  ship.x += 1;
  ship.facing += Math.PI/360 * 5;                                                ctx.restore();
                                                                             }
    drawShip();

    if (ship.y < canvas.height-10) {
      setTimeout( updateAndDrawShip, 20 );
    } else {
      drawGameOver();
    }                                                                        function drawGameOver() {
}                                                                              ctx.save();
                       Introducing:                                            ctx.globalComposition = "lighter";
                       •     animation loop: updateAndDraw...                  ctx.font = "20px Verdana";
                       •     keeping track of an object’s co-ords              ctx.fillStyle = "rgba(50,50,50,0.9)";
                       •     velocity & rotation                               ctx.fillText("Game Over", this.canvas.width/2 - 50,
                       •     clearing the canvas                                            this.canvas.height/2);
                                                                               ctx.restore();
                       Don’t setInterval - we’ll get to that later.
                                                                             }
                       globalComposition - drawing mode, lighter so we
                       can see text in some scenarios.
                                                                  demo -->
                                                                                                                             ship-move.html
Controls
          Wow!
<div id="controlBox">
    <input id="controls" type="text"
     placeholder="click to control" autofocus="autofocus"/>
    <p>Controls:
                                                            super-high-tech solution:
         <ul class="controlsInfo">
                                                                • use arrow keys to control your ship
             <li>up/i: accelerate forward</li>                  • space to fire
             <li>down/k: accelerate backward</li>               • thinking of patenting it :)
             <li>left/j: spin ccw</li>
             <li>right/l: spin cw</li>
             <li>space: shoot</li>       $("#controls").keydown(function(event) {self.handleKeyEvent(event)});
             <li>w: switch weapon</li>   $("#controls").keyup(function(event) {self.handleKeyEvent(event)});
         </ul>
    </p>                                 AsteroidsGame.prototype.handleKeyEvent = function(event) {
</div>                                       // TODO: send events, get rid of ifs.
                                             switch (event.which) {
                                               case 73: // i = up
                                               case 38: // up = accel
                                                  if (event.type == 'keydown') {
                                                      this.ship.startAccelerate();
                                                  } else { // assume keyup
                                                      this.ship.stopAccelerate();
                                                  }
                                                  event.preventDefault();
                                                  break;
                                             ...


                                                                                                       AsteroidsGame.js
Controls: Feedback
• Lines: thrust forward, backward, or spin
          (think exhaust from a jet...)


• Thrust: ‘force’ in status bar.
 // renderThrustForward
 // offset from center of ship
 // we translate here before drawing
 render.x = -13;
 render.y = -3;

 ctx.strokeStyle = 'black';
 ctx.beginPath();
 ctx.moveTo(8,0);
 ctx.lineTo(0,0);
 ctx.moveTo(8,3);                            Thrust
 ctx.lineTo(3,3);
 ctx.moveTo(8,6);
 ctx.lineTo(0,6);
 ctx.closePath();
 ctx.stroke();
                                                      Ships.js
Status bars...
                    ... are tightly-coupled to Ships atm:
Ship.prototype.initialize = function(game, spatial) {
    // Status Bars
    // for displaying ship info: health, shield, thrust, ammo
    // TODO: move these into their own objects
    ...
    this.thrustWidth = 100;
    this.thrustHeight = 10;
    this.thrustX = this.healthX;
    this.thrustY = this.healthY + this.healthHeight + 5;
    this.thrustStartX = Math.floor( this.thrustWidth / 2 );
    ...

Ship.prototype.renderThrustBar = function() {
    var render = this.getClearThrustBarCanvas();
    var ctx = render.ctx;

    var   thrustPercent = Math.floor(this.thrust/this.maxThrust * 100);
    var   fillWidth = Math.floor(thrustPercent * this.thrustWidth / 100 / 2);
    var   r = 100;
    var   b = 200 + Math.floor(thrustPercent/2);
    var   g = 100;
    var   fillStyle = 'rgba('+ r +','+ g +','+ b +',0.5)';

    ctx.fillStyle = fillStyle;
    ctx.fillRect(this.thrustStartX, 0, fillWidth, this.thrustHeight);

    ctx.strokeStyle = 'rgba(5,5,5,0.75)';
    ctx.strokeRect(0, 0, this.thrustWidth, this.thrustHeight);

    this.render.thrustBar = render;
}

                                                                                http://www.flickr.com/photos/imelda/497456854/
Drawing an Asteroid
                                               (or planet)
 ctx.beginPath();
 ctx.arc(this.radius, this.radius, this.radius, 0, deg_to_rad[360], false);
 ctx.closePath()
 if (this.fillStyle) {
   ctx.fillStyle = this.fillStyle;
   ctx.fill();
 } else {
   ctx.strokeStyle = this.strokeStyle;
   ctx.stroke();
 }




Show: cookie cutter level                                                        Planets.js
                                                                              Compositing
Drawing an Asteroid
                                              (or planet)
 ctx.beginPath();
 ctx.arc(this.radius, this.radius, this.radius, 0, deg_to_rad[360], false);
 ctx.closePath()
 if (this.fillStyle) {
   ctx.fillStyle = this.fillStyle;
   ctx.fill();
 } else {
   ctx.strokeStyle = this.strokeStyle;
   ctx.stroke();
 }                                  // Using composition as a cookie cutter:
                                  if (this.image != null) {
                                    this.render = this.createPreRenderCanvas(this.radius*2, this.radius*2);
                                    var ctx = this.render.ctx;

                                      // Draw a circle to define what we want to keep:
                                      ctx.globalCompositeOperation = 'destination-over';
                                      ctx.beginPath();
                                      ctx.arc(this.radius, this.radius, this.radius, 0, deg_to_rad[360], false);
                                      ctx.closePath();
                                      ctx.fillStyle = 'white';
                                      ctx.fill();

                                      // Overlay the image:
                                      ctx.globalCompositeOperation = 'source-in';
                                      ctx.drawImage(this.image, 0, 0, this.radius*2, this.radius*2);
                                      return;
                                  }


Show: cookie cutter level                                                                                          Planets.js
                                                                                                               Compositing
Space Objects

•   DRY                                               Planet           Asteroid


•   Base class for all things spacey.

•   Everything is a circle.                Ship                Planetoid




                                        SpaceObject
Err, why is everything a
         circle?
•   An asteroid is pretty much a circle, right?

•   And so are planets...

•   And so is a Ship with a shield around it! ;-)


•   ok, really:
    •   Game physics can get complicated.

    •   Keep it simple!
Space Objects
have...                             can...

 •        radius                     •       draw themselves

 •        coords: x, y               •       update their positions

 •        facing angle               •       accelerate, spin

 •        velocity, spin, thrust     •       collide with other objects

 •        health, mass, damage       •       apply damage, die

 •        and a whole lot more...    •       etc...




                                                                          SpaceObject.js
Game Mechanics



  what makes the game feel right.
Game Mechanics



        what makes the game feel right.




This is where it gets hairy.
                                          http://www.flickr.com/photos/maggz/2860543788/
The god class...

• AsteroidsGame does it all!
 •   user controls, game loop, 95% of the game mechanics ...

 •   Ok, so it’s not 5000 lines long (yet), but...

 •   it should really be split up!




                                                               AsteroidsGame.js
Game Mechanics
•   velocity & spin

•   acceleration & drag

•   gravity

•   collision detection, impact, bounce

•   health, damage, life & death

•   object attachment & push

•   out-of-bounds, viewports & scrolling
Velocity & Spin
SpaceObject.prototype.initialize = function(game,       SpaceObject.prototype.updateX = function(dX) {
spatial) {                                                if (this.stationary) return false;
   ...                                                    if (dX == 0) return false;
                                                          this.x += dX;
    this.x = 0;      // starting position on x axis       return true;
    this.y = 0;      // starting position on y axis     }
    this.facing = 0; // currently facing angle (rad)
                                                        SpaceObject.prototype.updateY = function(dY) {
    this.stationary = false; // should move?              if (this.stationary) return false;
                                                          if (dY == 0) return false;
    this.vX = spatial.vX || 0; // speed along X axis      this.y += dY;
    this.vY = spatial.vY || 0; // speed along Y axis      return true;
    this.maxV = spatial.maxV || 2; // max velocity      }
    this.maxVSquared = this.maxV*this.maxV; // cache
                                                        SpaceObject.prototype.updateFacing = function(delta)
    // thrust along facing                              {
    this.thrust = spatial.initialThrust || 0;             if (delta == 0) return false;
    this.maxThrust = spatial.maxThrust || 0.5;            this.facing += delta;
    this.thrustChanged = false;
                                                            // limit facing   angle to 0 <= facing <= 360
    this.spin = spatial.spin || 0; // spin in Rad/sec       if (this.facing   >= deg_to_rad[360] ||
    this.maxSpin = deg_to_rad[10];                              this.facing   <= deg_to_rad[360]) {
}                                                             this.facing =   this.facing % deg_to_rad[360];
                                                            }
SpaceObject.prototype.updatePositions = function
(objects) {                                                 if (this.facing < 0) {
    ...                                                       this.facing = deg_to_rad[360] + this.facing;
    if (this.updateFacing(this.spin)) changed = true;       }
    if (this.updateX(this.vX)) changed = true;
    if (this.updateY(this.vY)) changed = true;              return true;
}                                                       }




velocity = ∆ distance / time
spin = angular velocity = ∆ angle / time
Velocity & Spin
SpaceObject.prototype.initialize = function(game,       SpaceObject.prototype.updateX = function(dX) {
spatial) {                                                if (this.stationary) return false;
   ...                                                    if (dX == 0) return false;
                                                          this.x += dX;
    this.x = 0;      // starting position on x axis       return true;
    this.y = 0;      // starting position on y axis     }
    this.facing = 0; // currently facing angle (rad)
                                                        SpaceObject.prototype.updateY = function(dY) {
    this.stationary = false; // should move?              if (this.stationary) return false;
                                                          if (dY == 0) return false;
    this.vX = spatial.vX || 0; // speed along X axis      this.y += dY;
    this.vY = spatial.vY || 0; // speed along Y axis      return true;
    this.maxV = spatial.maxV || 2; // max velocity      }
    this.maxVSquared = this.maxV*this.maxV; // cache
                                                        SpaceObject.prototype.updateFacing = function(delta)
    // thrust along facing                              {
    this.thrust = spatial.initialThrust || 0;             if (delta == 0) return false;
    this.maxThrust = spatial.maxThrust || 0.5;            this.facing += delta;
    this.thrustChanged = false;
                                                            // limit facing   angle to 0 <= facing <= 360
    this.spin = spatial.spin || 0; // spin in Rad/sec       if (this.facing   >= deg_to_rad[360] ||
    this.maxSpin = deg_to_rad[10];                              this.facing   <= deg_to_rad[360]) {
}                                                             this.facing =   this.facing % deg_to_rad[360];
                                                            }
SpaceObject.prototype.updatePositions = function
(objects) {                                                 if (this.facing < 0) {
    ...                                                       this.facing = deg_to_rad[360] + this.facing;
    if (this.updateFacing(this.spin)) changed = true;       }
    if (this.updateX(this.vX)) changed = true;
    if (this.updateY(this.vY)) changed = true;              return true;
}                                                       }




velocity = ∆ distance / time
spin = angular velocity = ∆ angle / time                where: time = current frame rate
Acceleration
SpaceObject.prototype.initialize = function(game, spatial) {
   ...
    // thrust along facing
    this.thrust = spatial.initialThrust || 0;
    this.maxThrust = spatial.maxThrust || 0.5;
    this.thrustChanged = false;
}

SpaceObject.prototype.accelerateAlong = function(angle, thrust) {
    var accel = thrust/this.mass;
    var dX = Math.cos(angle) * accel;
    var dY = Math.sin(angle) * accel;
    this.updateVelocity(dX, dY);
}




 acceleration = ∆ velocity / time
 acceleration = mass / force
Acceleration
SpaceObject.prototype.initialize = function(game, spatial) {
   ...
    // thrust along facing
    this.thrust = spatial.initialThrust || 0;                       Ship.prototype.startAccelerate = function() {
    this.maxThrust = spatial.maxThrust || 0.5;                          if (this.accelerate) return;
    this.thrustChanged = false;                                         this.accelerate = true;
}                                                                       //console.log("thrust++");

SpaceObject.prototype.accelerateAlong = function(angle, thrust) {        this.clearSlowDownInterval();
    var accel = thrust/this.mass;
    var dX = Math.cos(angle) * accel;                                    var self = this;
    var dY = Math.sin(angle) * accel;                                    this.incThrustIntervalId = setInterval(function(){
    this.updateVelocity(dX, dY);                                    !     self.increaseThrust();
}                                                                        }, 20); // real time
                                                                    };

                                                                    Ship.prototype.increaseThrust = function() {
                                                                        this.incThrust(this.thrustIncrement);
                                                                        this.accelerateAlong(this.facing, this.thrust);
   Ship.prototype.initialize = function(game, spatial) {            }
       ...

       spatial.mass = 10;                                           Ship.prototype.stopAccelerate = function() {
                                                                        //console.log("stop thrust++");
       // current state of user action:                                 if (this.clearIncThrustInterval())
       this.increaseSpin = false;                                   this.resetThrust();
       this.decreaseSpin = false;                                       this.startSlowingDown();
       this.accelerate = false;                                         this.accelerate = false;
       this.decelerate = false;                                     };
       this.firing = false;
                                                                    Ship.prototype.clearIncThrustInterval = function() {
       // for moving about:                                             if (! this.incThrustIntervalId) return false;
       this.thrustIncrement = 0.01;                                     clearInterval(this.incThrustIntervalId);
       this.spinIncrement = deg_to_rad[0.5];                            this.incThrustIntervalId = null;
       ...                                                              return true;
   }                                                                }

 acceleration = ∆ velocity / time
 acceleration = mass / force
Acceleration
SpaceObject.prototype.initialize = function(game, spatial) {
   ...
    // thrust along facing
    this.thrust = spatial.initialThrust || 0;                       Ship.prototype.startAccelerate = function() {
    this.maxThrust = spatial.maxThrust || 0.5;                          if (this.accelerate) return;
    this.thrustChanged = false;                                         this.accelerate = true;
}                                                                       //console.log("thrust++");

SpaceObject.prototype.accelerateAlong = function(angle, thrust) {        this.clearSlowDownInterval();
    var accel = thrust/this.mass;
    var dX = Math.cos(angle) * accel;                                    var self = this;
    var dY = Math.sin(angle) * accel;                                    this.incThrustIntervalId = setInterval(function(){
    this.updateVelocity(dX, dY);                                    !     self.increaseThrust();
}                                                                        }, 20); // real time
                                                                    };

                                                                    Ship.prototype.increaseThrust = function() {
                                                                        this.incThrust(this.thrustIncrement);
                                                                        this.accelerateAlong(this.facing, this.thrust);
   Ship.prototype.initialize = function(game, spatial) {            }
       ...

       spatial.mass = 10;                                           Ship.prototype.stopAccelerate = function() {
                                                                        //console.log("stop thrust++");
       // current state of user action:                                 if (this.clearIncThrustInterval())
       this.increaseSpin = false;                                   this.resetThrust();
       this.decreaseSpin = false;                                       this.startSlowingDown();
       this.accelerate = false;                                         this.accelerate = false;
       this.decelerate = false;                                     };
       this.firing = false;
                                                                    Ship.prototype.clearIncThrustInterval = function() {
       // for moving about:                                             if (! this.incThrustIntervalId) return false;
       this.thrustIncrement = 0.01;                                     clearInterval(this.incThrustIntervalId);
       this.spinIncrement = deg_to_rad[0.5];                            this.incThrustIntervalId = null;
       ...                                                              return true;
   }                                                                }

 acceleration = ∆ velocity / time                                   where: time = real time
 acceleration = mass / force                                        (just to confuse things)
Drag


         Yes, yes, there is no drag in outer space. Very clever.




I disagree.
Drag


         Yes, yes, there is no drag in outer space. Very clever.




                                                          http://nerdadjacent.deviantart.com/art/Ruby-Rhod-Supergreen-265156565
I disagree.
Drag
Ship.prototype.startSlowingDown = function() {          Ship.prototype.slowDown = function() {
    // console.log("slowing down...");                      var vDrag = 0.01;
    if (this.slowDownIntervalId) return;                    if (this.vX > 0) {
                                                        !     this.vX -= vDrag;
    var self = this;                                        } else if (this.vX < 0) {
    this.slowDownIntervalId = setInterval(function(){   !     this.vX += vDrag;
!    self.slowDown()                                        }
    }, 100); // eek! another hard-coded timeout!            if (this.vY > 0) {
}                                                       !     this.vY -= vDrag;
                                                            } else if (this.vY < 0) {
Ship.prototype.clearSlowDownInterval = function() {     !     this.vY += vDrag;
    if (! this.slowDownIntervalId) return false;            }
    clearInterval(this.slowDownIntervalId);
    this.slowDownIntervalId = null;                         if (Math.abs(this.vX) <= vDrag) this.vX = 0;
    return true;                                            if (Math.abs(this.vY) <= vDrag) this.vY = 0;
}
                                                            if (this.vX == 0 && this.vY == 0) {
                                                        !     // console.log('done slowing down');
                                                        !     this.clearSlowDownInterval();
                                                            }
                                                        }




Demo: accel + drag in blank level
Gravity
              var dvX_1 = 0, dvY_1 = 0;
              if (! object1.stationary) {
                var accel_1 = object2.cache.G_x_mass / physics.dist_squared;
                if (accel_1 > 1e-5) { // skip if it's too small to notice
                  if (accel_1 > this.maxAccel) accel_1 = this.maxAccel;
                  var angle_1 = Math.atan2(physics.dX, physics.dY);
                  dvX_1 = -Math.sin(angle_1) * accel_1;
                  dvY_1 = -Math.cos(angle_1) * accel_1;
                  object1.delayUpdateVelocity(dvX_1, dvY_1);
                }
              }

              var dvX_2 = 0, dvY_2 = 0;
              if (! object2.stationary) {
                var accel_2 = object1.cache.G_x_mass / physics.dist_squared;
                if (accel_2 > 1e-5) { // skip if it's too small to notice
                  if (accel_2 > this.maxAccel) accel_2 = this.maxAccel;
                  // TODO: angle_2 = angle_1 - PI?
                  var angle_2 = Math.atan2(-physics.dX, -physics.dY); // note the - signs
                  dvX_2 = -Math.sin(angle_2) * accel_2;
                  dvY_2 = -Math.cos(angle_2) * accel_2;
                  object2.delayUpdateVelocity(dvX_2, dvY_2);
                }
              }




force = G*mass1*mass2 / dist^2
acceleration1 = force / mass1
Collision Detection
AsteroidsGame.prototype.applyGamePhysicsTo = function(object1, object2) {
    ...
    var dX = object1.x - object2.x;
    var dY = object1.y - object2.y;

    // find dist between center of mass:
    // avoid sqrt, we don't need dist yet...
    var dist_squared = dX*dX + dY*dY;

    var total_radius = object1.radius + object2.radius;
    var total_radius_squared = Math.pow(total_radius, 2);

    // now check if they're touching:
    if (dist_squared > total_radius_squared) {
       // nope
    } else {
       // yep
       this.collision( object1, object2, physics );
    }

    ...                                                                     http://www.flickr.com/photos/wsmonty/4299389080/




          Aren’t you glad we stuck with circles?
Bounce


Formula:

 •   Don’t ask.
*bounce*
*bounce*
        !    // Thanks Emanuelle! bounce algorithm adapted from:
        !    // http://www.emanueleferonato.com/2007/08/19/managing-ball-vs-ball-collision-with-flash/
        !    collision.angle = Math.atan2(collision.dY, collision.dX);
        !    var magnitude_1 = Math.sqrt(object1.vX*object1.vX + object1.vY*object1.vY);
        !    var magnitude_2 = Math.sqrt(object2.vX*object2.vX + object2.vY*object2.vY);

        !    var direction_1 = Math.atan2(object1.vY, object1.vX);
        !    var direction_2 = Math.atan2(object2.vY, object2.vX);

        !    var   new_vX_1   =   magnitude_1*Math.cos(direction_1-collision.angle);
        !    var   new_vY_1   =   magnitude_1*Math.sin(direction_1-collision.angle);
        !    var   new_vX_2   =   magnitude_2*Math.cos(direction_2-collision.angle);
        !    var   new_vY_2   =   magnitude_2*Math.sin(direction_2-collision.angle);

        !    [snip]

        !    // bounce the objects:
        !    var final_vX_1 = ( (cache1.delta_mass * new_vX_1 + object2.cache.mass_x_2 * new_vX_2)
        !    !      !         / cache1.total_mass * this.elasticity );
        !    var final_vX_2 = ( (object1.cache.mass_x_2 * new_vX_1 + cache2.delta_mass * new_vX_2)
        !    !      !         / cache2.total_mass * this.elasticity );
        !    var final_vY_1 = new_vY_1 * this.elasticity;
        !    var final_vY_2 = new_vY_2 * this.elasticity;


        !    var   cos_collision_angle = Math.cos(collision.angle);
        !    var   sin_collision_angle = Math.sin(collision.angle);
        !    var   cos_collision_angle_halfPI = Math.cos(collision.angle + halfPI);
        !    var   sin_collision_angle_halfPI = Math.sin(collision.angle + halfPI);

        !    var vX1 = cos_collision_angle*final_vX_1 + cos_collision_angle_halfPI*final_vY_1;
        !    var vY1 = sin_collision_angle*final_vX_1 + sin_collision_angle_halfPI*final_vY_1;
        !    object1.delaySetVelocity(vX1, vY1);

        !    var vX2 = cos_collision_angle*final_vX_2 + cos_collision_angle_halfPI*final_vY_2;
        !    var vY2 = sin_collision_angle*final_vX_2 + sin_collision_angle_halfPI*final_vY_2;
        !    object2.delaySetVelocity(vX2, vY2);




Aren’t you *really* glad we stuck with circles?
Making it *hurt*
AsteroidsGame.prototype.collision = function(object1, object2, collision) {              Shield
  ...
  // “collision” already contains a bunch of calcs                                                                                  Health
  collision[object1.id] = {
    cplane: {vX: new_vX_1, vY: new_vY_1}, // relative to collision plane
    dX: collision.dX,
    dY: collision.dY,
    magnitude: magnitude_1
  }
  // do the same for object2
                                                  SpaceObject.prototype.collided = function(object, collision) {
  // let the objects fight it out                     this.colliding[object.id] = object;
  object1.collided(object2, collision);
  object2.collided(object1, collision);               if (this.damage) {
}                                                 !     var damageDone = this.damage;
                                                  !     if (collision.impactSpeed != null) {
                                                  !         damageDone = Math.ceil(damageDone * collision.impactSpeed);
                                                  !     }
                                                  !     object.decHealth( damageDone );
                                                      }
                                                  }

                                                SpaceObject.prototype.decHealth = function(delta) {
                                                    this.healthChanged = true;
                                                    this.health -= delta;
                                                    if (this.health <= 0) {
                                                !     this.health = -1;
                                                !     this.die();
                                                    }
Ship.prototype.decHealth = function(delta) {    }
    if (this.shieldActive) {                                             When a collision occurs the Game Engine fires off 2 events to the objects in
!     delta = this.decShield(delta);                                     question
    }                                                                    •      For damage, I opted for a property rather than using mass * impact
    if (delta) Ship.prototype.parent.decHealth.call(this, delta);               speed in the general case.
}                                                                        Applying damage is fairly straightforward:
                                                                         •      Objects are responsible for damaging each other
                                                                         •      When damage is done dec Health (for a Ship, shield first)
                                                                         •      If health < 0, an object dies.
                                                                                                                                           SpaceObject.js
Object Lifecycle
       SpaceObject.prototype.die = function() {
           this.died = true;
           this.update = false;
           this.game.objectDied( this );
       }


AsteroidsGame.prototype.objectDied = function(object) {
    // if (object.is_weapon) {
    //} else if (object.is_asteroid) {                        Asteroid.prototype.die = function() {
                                                                this.parent.die.call( this );
    if (object.is_planet) {                                     if (this.spawn <= 0) return;
!     throw "planet died!?"; // not allowed                     for (var i=0; i < this.spawn; i++) {
    } else if (object.is_ship) {                                  var mass = Math.floor(this.mass / this.spawn * 1000)/1000;
!     // TODO: check how many lives they've got                   var radius = getRandomInt(2, this.radius);
!     if (object == this.ship) {                                  var asteroid = new Asteroid(this.game, {
!         this.stopGame();                                            mass: mass,
!     }                                                               x: this.x + i/10, // don't overlap
                                                                      y: this.y + i/10,
    }                                                                 vX: this.vX * Math.random(),
                                                                      vX: this.vY * Math.random(),
    this.removeObject(object);                                        radius: radius,
}                                                                     health: getRandomInt(0, this.maxSpawnHealth),
                                                                      spawn: getRandomInt(0, this.spawn-1),
AsteroidsGame.prototype.removeObject = function(object) {             image: getRandomInt(0, 5) > 0 ? this.image : null,
    var objects = this.objects;                                       // let physics engine handle movement
                                                                  });
    var i = objects.indexOf(object);                              this.game.addObject( asteroid );
    if (i >= 0) {                                               }
!     objects.splice(i,1);                                    }
!     this.objectUpdated( object );
    }

    // avoid memory bloat: remove references to this object   AsteroidsGame.prototype.addObject = function(object) {
    // from other objects' caches:                                //console.log('adding ' + object);
    var oid = object.id;                                          this.objects.push( object );
    for (var i=0; i < objects.length; i++) {                      this.objectUpdated( object );
!     delete objects[i].cache[oid];                               object.preRender();
    }                                                             this.cachePhysicsFor(object);
}                                                             }
Attachment
• Attach objects that are ‘gently’ touching
  •   then apply special physics

• Why?



                                              AsteroidsGame.js
Attachment
• Attach objects that are ‘gently’ touching
  •   then apply special physics

• Why?
  Prevent the same collision from recurring.
                     +
            Allows ships to land.
                     +
              Poor man’s Orbit.
                                               AsteroidsGame.js
Push!

         • When objects get too close
          • push them apart!
          • otherwise they overlap...
                            (and the game physics gets weird)




demo: what happens when you disable applyPushAway()
Out-of-bounds
                       When you have a map that is not wrapped...
                                                                                             Simple strategy:
                                                                                               •   kill most objects that stray
                                                                                               •   push back important things like ships

AsteroidsGame.prototype.applyOutOfBounds = function(object) {
    if (object.stationary) return;                                  ...

    var level = this.level;                                         if (object.y < 0) {
    var die_if_out_of_bounds =                                  !     if (level.wrapY) {
        !(object.is_ship || object.is_planet);                  !         object.setY(level.maxY + object.y);
                                                                !     } else {
    if (object.x < 0) {                                         !         if (die_if_out_of_bounds && object.vY < 0) {
!     if (level.wrapX) {                                        !     !     return object.die();
!         object.setX(level.maxX + object.x);                   !         }
!     } else {                                                  !         // push back into bounds
!         if (die_if_out_of_bounds && object.vX < 0) {          !         object.updateVelocity(0, 0.1);
!     !     return object.die();                                !     }
!         }                                                         } else if (object.y > level.maxY) {
!         object.updateVelocity(0.1, 0);                        !     if (level.wrapY) {
!     }                                                         !         object.setY(object.y - level.maxY);
    } else if (object.x > level.maxX) {                         !     } else {
!     if (level.wrapX) {                                        !         if (die_if_out_of_bounds && object.vY > 0) {
!         object.setX(object.x - level.maxX);                   !     !     return object.die();
!     } else {                                                  !         }
!         if (die_if_out_of_bounds && object.vX > 0) {          !         // push back into bounds
!     !     return object.die();                                !         object.updateVelocity(0, -0.1);
!         }                                                     !     }
!         object.updateVelocity(-0.1, 0);                           }
!     }                                                         }
    }
    ...
Viewport + Scrolling
                      When the dimensions of your map exceed
                              those of your canvas...


AsteroidsGame.prototype.updateViewOffset = function() {
    var canvas = this.ctx.canvas;
    var offset = this.viewOffset;
    var dX = Math.round(this.ship.x - offset.x - canvas.width/2);
    var dY = Math.round(this.ship.y - offset.y - canvas.height/2);

    // keep the ship centered in the current view, but don't let the view
    // go out of bounds
    offset.x += dX;
    if (offset.x < 0) offset.x = 0;
    if (offset.x > this.level.maxX-canvas.width) offset.x = this.level.maxX-canvas.width;

    offset.y += dY;
    if (offset.y < 0) offset.y = 0;
    if (offset.y > this.level.maxY-canvas.height) offset.y = this.level.maxY-canvas.height;
}


                                                                       AsteroidsGame.prototype.redrawCanvas = function() {
                                                                       ...
                                                                           // shift view to compensate for current offset
                                                                           var offset = this.viewOffset;
                                                                           ctx.save();
           Let browser manage complexity: if you draw to canvas            ctx.translate(-offset.x, -offset.y);
           outside of current width/height, browser doesn’t draw it.
Putting it all together




Demo: hairballs & chainsaws level.
Weapons
Gun + Bullet
              •       Gun                                                                     Ammo

                    •       Fires Bullets

                    •       Has ammo
                                                                                                  Planet             Astero
                    •       Belongs to a Ship

                                                                              Gun
                                                                     Bullet            Ship                Planetoid




                                                                                    SpaceObject
When you shoot, bullets inherit the Ship’s velocity.
Each weapon has a different recharge rate (measured in real time).
                                                                                                                 Weapons.js
Other Weapons
• Gun
• SprayGun
• Cannon
• GrenadeCannon
• GravBenda™
                      (back in my day, we
                      used to read books!)




    Current weapon
Enemies.




(because shooting circles gets boring)
A basic enemy...

                       ComputerShip.prototype.findAndDestroyClosestEnemy = function() {
                           var enemy = this.findClosestEnemy();
                           if (enemy == null) return;

                           // Note: this is a basic algorith, it doesn't take a lot of things
                           // into account (enemy trajectory & facing, other objects, etc)

                           // navigate towards enemy
                           // shoot at the enemy
                       }




Demo: Level Lone enemy.
Show: ComputerShip class...                                                                     Ships.js
A basic enemy...

                       ComputerShip.prototype.findAndDestroyClosestEnemy = function() {
                           var enemy = this.findClosestEnemy();
                           if (enemy == null) return;

                           // Note: this is a basic algorith, it doesn't take a lot of things
                           // into account (enemy trajectory & facing, other objects, etc)

                           // navigate towards enemy
                           // shoot at the enemy
                       }




                 of course, it’s a bit more involved...


Demo: Level Lone enemy.
Show: ComputerShip class...                                                                     Ships.js
Levels
• Define:
 • map dimensions
 • space objects
 • spawning
 • general properties of the canvas - color,
    etc


                                               Levels.js
/******************************************************************************
 * TrainingLevel: big planet out of field of view with falling asteroids.
 */

function TrainingLevel(game) {
    if (game) return this.initialize(game);
    return this;
}

TrainingLevel.inheritsFrom( Level );
TrainingLevel.description = "Training Level - learn how to fly!";
TrainingLevel.images = [ "planet.png", "planet-80px-green.png" ];

gameLevels.push(TrainingLevel);

TrainingLevel.prototype.initialize = function(game) {
    TrainingLevel.prototype.parent.initialize.call(this, game);
    this.wrapX = false;
    this.wrapY = false;

    var maxX = this.maxX;
    var maxY = this.maxY;

    var canvas = this.game.ctx.canvas;
    this.planets.push(
!   {x: 1/2*maxX, y: 1/2*maxY, mass: 100, radius: 50, damage: 5, stationary: true, image_src: "planet.png" }
!   , {x: 40, y: 40, mass: 5, radius: 20, vX: 2, vY: 0, image_src:"planet-80px-green.png"}
!   , {x: maxX-40, y: maxY-40, mass: 5, radius: 20, vX: -2, vY: 0, image_src:"planet-80px-green.png"}
    );

    this.ships.push(
!   {x: 4/5*canvas.width, y: 1/3*canvas.height}
    );

    this.asteroids.push(
!   {x: 1/10*maxX, y: 6/10*maxY, mass: 0.5, radius: 14, vX: 0, vY: 0, spawn: 1, health: 1},
        {x: 1/10*maxX, y: 2/10*maxY, mass: 1, radius: 5, vX: 0, vY: -0.1, spawn: 3 },
        {x: 5/10*maxX, y: 1/10*maxY, mass: 2, radius: 6, vX: -0.2, vY: 0.25, spawn: 4 },
        {x: 5/10*maxX, y: 2/10*maxY, mass: 3, radius: 8, vX: -0.22, vY: 0.2, spawn: 7 }
    );
}

                        As usual, I had grandiose plans of an interactive level editor... This was all I had time for.
                                                                                                                         Levels.js
Performance


“Premature optimisation is the root of all evil.”
     http://c2.com/cgi/wiki?PrematureOptimization
Use requestAnimationFrame


Paul Irish knows why:
• don’t animate if your canvas is not visible
• adjust your frame rate based on actual performance
• lets the browser manage your app better
Profile your code
•   profile in different browsers
•   identify the slow stuff
•   ask yourself: “do we really need to do this?”
•   optimise it?
    • cache slow operations
    • change algorithm?
    • simplify?
Examples...
// see if we can use cached values first:                                              // put any calculations we can avoid repeating here
var g_cache1 = physics.cache1.last_G;                                                  AsteroidsGame.prototype.cachePhysicsFor = function(object1) {
var g_cache2 = physics.cache2.last_G;                                                      for (var i=0; i < this.objects.length; i++) {
                                                                                       !      var object2 = this.objects[i];
if (g_cache1) {                                                                        !      if (object1 == object2) continue;
  var delta_dist_sq = Math.abs( physics.dist_squared - g_cache1.last_dist_squared);
  var percent_diff = delta_dist_sq / physics.dist_squared;                             !       // shared calcs
  // set threshold @ 5%                                                                !       var total_radius = object1.radius + object2.radius;
  if (percent_diff < 0.05) {                                                           !       var total_radius_squared = Math.pow(total_radius, 2);
    // we haven't moved much, use last G values                                        !       var total_mass = object1.mass + object2.mass;
    //console.log("using G cache");
    object1.delayUpdateVelocity(g_cache1.dvX, g_cache1.dvY);                           !       // create separate caches from perspective of objects:
    object2.delayUpdateVelocity(g_cache2.dvX, g_cache2.dvY);                           !       object1.cache[object2.id] = {
    return;                                                                            !           total_radius: total_radius,
  }                                                                                    !           total_radius_squared: total_radius_squared,
}                                                                                      !           total_mass: total_mass,
                                                                                       !           delta_mass: object1.mass - object2.mass
                                                                                       !       }
   // avoid overhead of update calculations & associated checks: batch together        !       object2.cache[object1.id] = {
   SpaceObject.prototype.delayUpdateVelocity = function(dvX, dvY) {                    !           total_radius: total_radius,
       if (this._updates == null) this.init_updates();                                 !           total_radius_squared: total_radius_squared,
       this._updates.dvX += dvX;                                                       !           total_mass: total_mass,
       this._updates.dvY += dvY;                                                       !           delta_mass: object2.mass - object1.mass
   }                                                                                   !       }
                                                                                           }
                                                                                       }

      var dist_squared = dX*dX + dY*dY; // avoid sqrt, we don't need dist yet




                                            this.maxVSquared = this.maxV*this.maxV;   // cache for speed


                                             if (accel_1 > 1e-5) { // skip if it's too small to notice



                                                                        ...
Performance
Great Ideas from Boris Smus:
•   Only redraw changes
•   Pre-render to another canvas
•   Draw background in another canvas / element
•   Don't use floating point co-ords
    ... and more ...
Can I Play?


http://www.spurkis.org/asteroids/
See Also...
Learning / examples:
 •       https://developer.mozilla.org/en-US/docs/Canvas_tutorial
 •       http://en.wikipedia.org/wiki/Canvas_element
 •       http://www.html5rocks.com/en/tutorials/canvas/performance/
 •       http://www.canvasdemos.com
 •       http://billmill.org/static/canvastutorial/
 •       http://paulirish.com/2011/requestanimationframe-for-smart-animating/
Specs:
 •       http://dev.w3.org/html5/spec/
 •       http://www.khronos.org/registry/webgl/specs/latest/
Questions?

More Related Content

PDF
04 - Qt Data
PDF
CUDA Deep Dive
PPT
Using QString effectively
PDF
05 - Qt External Interaction and Graphics
PDF
Html5 game programming overview
PDF
[Ultracode Munich #4] Demo on Animatron by Anton Kotenko
PPT
HTML5 Canvas
PPTX
Lecture1 classes3
04 - Qt Data
CUDA Deep Dive
Using QString effectively
05 - Qt External Interaction and Graphics
Html5 game programming overview
[Ultracode Munich #4] Demo on Animatron by Anton Kotenko
HTML5 Canvas
Lecture1 classes3

What's hot (20)

PDF
HTML5 Canvas - The Future of Graphics on the Web
PPTX
MongoDB Live Hacking
PPTX
Introduction to HTML5 Canvas
PPTX
Gpu programming with java
PDF
Introduction to CUDA C: NVIDIA : Notes
PDF
tutorial5
PDF
Intro to Clojure's core.async
PDF
Html5 canvas
PDF
CUDA Raytracing을 이용한 Voxel오브젝트 가시성 테스트
PPTX
Box2D with SIMD in JavaScript
PPTX
Trident International Graphics Workshop 2014 1/5
PDF
Prototype UI Intro
PDF
Introduction to cuda geek camp singapore 2011
PPTX
WebGL and three.js - Web 3D Graphics
PDF
WebGL - 3D in your Browser
PDF
Monolith to Reactive Microservices
PDF
Qt Widget In-Depth
PDF
kissy-past-now-future
PDF
Exploiting Concurrency with Dynamic Languages
PPTX
KISSY 的昨天、今天与明天
HTML5 Canvas - The Future of Graphics on the Web
MongoDB Live Hacking
Introduction to HTML5 Canvas
Gpu programming with java
Introduction to CUDA C: NVIDIA : Notes
tutorial5
Intro to Clojure's core.async
Html5 canvas
CUDA Raytracing을 이용한 Voxel오브젝트 가시성 테스트
Box2D with SIMD in JavaScript
Trident International Graphics Workshop 2014 1/5
Prototype UI Intro
Introduction to cuda geek camp singapore 2011
WebGL and three.js - Web 3D Graphics
WebGL - 3D in your Browser
Monolith to Reactive Microservices
Qt Widget In-Depth
kissy-past-now-future
Exploiting Concurrency with Dynamic Languages
KISSY 的昨天、今天与明天

Viewers also liked (6)

KEY
A Brief Introduction to JQuery Mobile
PDF
Developing Developers Through Apprenticeship
PPT
Entertaining pixie
PPTX
jQuery Mobile
PPT
High Availability Perl DBI + MySQL
PDF
Intro to jquery
A Brief Introduction to JQuery Mobile
Developing Developers Through Apprenticeship
Entertaining pixie
jQuery Mobile
High Availability Perl DBI + MySQL
Intro to jquery

Similar to Writing a Space Shooter with HTML5 Canvas (20)

PPT
Rotoscope inthebrowserppt billy
PDF
How to build a html5 websites.v1
PPTX
Introduction to Canvas - Toronto HTML5 User Group
PDF
Introduction to Canvas - Toronto HTML5 User Group
PPTX
Intro to Canva
PPTX
How to make a video game
PDF
Exploring Canvas
PDF
Google's HTML5 Work: what's next?
PDF
Intro to HTML5 Canvas
KEY
Exploring Canvas
PPTX
HTML5 Animation in Mobile Web Games
PDF
Mapping the world with Twitter
PDF
Is HTML5 Ready? (workshop)
PDF
Is html5-ready-workshop-110727181512-phpapp02
PDF
I Can't Believe It's Not Flash
PDF
JavaOne 2009 - 2d Vector Graphics in the browser with Canvas and SVG
PDF
HTML5: where flash isn't needed anymore
KEY
Interactive Graphics
KEY
The Canvas API for Rubyists
PPTX
Advanced html5 diving into the canvas tag
Rotoscope inthebrowserppt billy
How to build a html5 websites.v1
Introduction to Canvas - Toronto HTML5 User Group
Introduction to Canvas - Toronto HTML5 User Group
Intro to Canva
How to make a video game
Exploring Canvas
Google's HTML5 Work: what's next?
Intro to HTML5 Canvas
Exploring Canvas
HTML5 Animation in Mobile Web Games
Mapping the world with Twitter
Is HTML5 Ready? (workshop)
Is html5-ready-workshop-110727181512-phpapp02
I Can't Believe It's Not Flash
JavaOne 2009 - 2d Vector Graphics in the browser with Canvas and SVG
HTML5: where flash isn't needed anymore
Interactive Graphics
The Canvas API for Rubyists
Advanced html5 diving into the canvas tag

More from Steve Purkis (14)

PPTX
Organised Services Operating Model Overview - Services Week 2025
PPTX
Start the Wardley Mapping Foundation
PPTX
Maps: a better way to organise
PPTX
Making sense of complex systems
PDF
Glasswall Wardley Maps & Services
PPTX
What do Wardley Maps mean to me? (Map Camp 2020)
PDF
Introduction to Wardley Maps
PPTX
COVID-19 - Systems & Complexity Thinking in Action
PPTX
Predicting & Influencing with Kanban Metrics
PPTX
Map Your Values: Connect & Collaborate
PPTX
Modern agile overview
PPTX
Kanban in the Kitchen
PPT
Scalar::Footnote
PDF
TAP-Harness + friends
Organised Services Operating Model Overview - Services Week 2025
Start the Wardley Mapping Foundation
Maps: a better way to organise
Making sense of complex systems
Glasswall Wardley Maps & Services
What do Wardley Maps mean to me? (Map Camp 2020)
Introduction to Wardley Maps
COVID-19 - Systems & Complexity Thinking in Action
Predicting & Influencing with Kanban Metrics
Map Your Values: Connect & Collaborate
Modern agile overview
Kanban in the Kitchen
Scalar::Footnote
TAP-Harness + friends

Recently uploaded (20)

PPTX
A Presentation on Artificial Intelligence
PDF
TokAI - TikTok AI Agent : The First AI Application That Analyzes 10,000+ Vira...
PDF
MIND Revenue Release Quarter 2 2025 Press Release
PPTX
Digital-Transformation-Roadmap-for-Companies.pptx
PDF
Per capita expenditure prediction using model stacking based on satellite ima...
PDF
Machine learning based COVID-19 study performance prediction
PPTX
Programs and apps: productivity, graphics, security and other tools
PDF
Spectral efficient network and resource selection model in 5G networks
PDF
Build a system with the filesystem maintained by OSTree @ COSCUP 2025
PDF
Profit Center Accounting in SAP S/4HANA, S4F28 Col11
PDF
Accuracy of neural networks in brain wave diagnosis of schizophrenia
PPTX
Group 1 Presentation -Planning and Decision Making .pptx
PPT
Teaching material agriculture food technology
PPTX
SOPHOS-XG Firewall Administrator PPT.pptx
PDF
Reach Out and Touch Someone: Haptics and Empathic Computing
PDF
Getting Started with Data Integration: FME Form 101
PDF
Electronic commerce courselecture one. Pdf
PDF
Optimiser vos workloads AI/ML sur Amazon EC2 et AWS Graviton
PDF
Architecting across the Boundaries of two Complex Domains - Healthcare & Tech...
PPT
“AI and Expert System Decision Support & Business Intelligence Systems”
A Presentation on Artificial Intelligence
TokAI - TikTok AI Agent : The First AI Application That Analyzes 10,000+ Vira...
MIND Revenue Release Quarter 2 2025 Press Release
Digital-Transformation-Roadmap-for-Companies.pptx
Per capita expenditure prediction using model stacking based on satellite ima...
Machine learning based COVID-19 study performance prediction
Programs and apps: productivity, graphics, security and other tools
Spectral efficient network and resource selection model in 5G networks
Build a system with the filesystem maintained by OSTree @ COSCUP 2025
Profit Center Accounting in SAP S/4HANA, S4F28 Col11
Accuracy of neural networks in brain wave diagnosis of schizophrenia
Group 1 Presentation -Planning and Decision Making .pptx
Teaching material agriculture food technology
SOPHOS-XG Firewall Administrator PPT.pptx
Reach Out and Touch Someone: Haptics and Empathic Computing
Getting Started with Data Integration: FME Form 101
Electronic commerce courselecture one. Pdf
Optimiser vos workloads AI/ML sur Amazon EC2 et AWS Graviton
Architecting across the Boundaries of two Complex Domains - Healthcare & Tech...
“AI and Expert System Decision Support & Business Intelligence Systems”

Writing a Space Shooter with HTML5 Canvas

  • 1. Asteroid(s)* with HTML5 Canvas (c) 2012 Steve Purkis * Asteroids™ is a trademark of Atari Inc.
  • 2. About Steve Software Dev + Manager not really a front-end dev nor a game dev (but I like to play!)
  • 3. Uhh... Asteroids? What’s that, then? http://www.flickr.com/photos/onkel_wart/201905222/
  • 4. Asteroids™ http://en.wikipedia.org/wiki/File:Asteroi1.png
  • 5. Asteroids™ “ Asteroids is a video arcade game released in November 1979 by Atari Inc. It was one of the most popular and influential games of the Golden Age of Arcade Games, selling 70,000 arcade cabinets.[1] Asteroids uses a vector display and a two-dimensional view that wraps around in both screen axes. The player controls a spaceship in an asteroid field which is periodically traversed by flying saucers. The object of the game is to shoot and destroy asteroids and saucers while not colliding with either, or being hit by the saucers' counter-fire.” http://en.wikipedia.org/wiki/Asteroids_(video_game) http://en.wikipedia.org/wiki/File:Asteroi1.png
  • 6. Asteroids™ Note that the term “Asteroids” is © Atari when used with a game. I didn’t know that when I wrote this... Oops. Atari: • I promise I’ll change the name of the game. • In the meantime, consider this free marketing! :-)
  • 7. Why DIY Asteroid(s)? • Yes, it’s been done before. • Yes, I could have used a Game Engine. • Yes, I could have used open|web GL. • No, I’m not a “Not Invented Here” guy.
  • 8. Why not? • It’s a fun way to learn new tech. • I learn better by doing. (ok, so I’ve secretly wanted to write my own version of asteroids since I was a kid.) I was learning about HTML5 (yes I know it came out several years ago. I’ve been busy). A few years ago, the only way you’d be able to do this is with Flash. demo
  • 9. It’s not Done... (but it’s playable)
  • 10. It’s a Hack! http://www.flickr.com/photos/cheesygarlicboy/269419718/
  • 11. It’s a Hack! • No Tests (!) • somewhat broken in IE • mobile? what’s that? • no sound, fancy gfx, ... http://www.flickr.com/photos/cheesygarlicboy/269419718/
  • 12. If you see this... you know what to expect.
  • 13. What I’ll cover... • Basics of canvas 2D (as I go) • Overview of how it’s put together • Some game mechanics • Performance
  • 14. What’s Canvas? • “New” to HTML5! • lets you draw 2D graphics: • images, text • vector graphics (lines, curves, etc) • trades performance & control for convenience • ... and 3D graphics (WebGL) • still a draft standard
  • 15. Drawing a Ship • Get canvas element • draw lines that make up the ship <body onload="drawShip()"> <h1>Canvas:</h1> <canvas id="demo" width="300" height="200" style="border: 1px solid black" /> </body> function drawShip() { var canvas = document.getElementById("demo"); var ctx = canvas.getContext('2d'); var center = {x: canvas.width/2, y: canvas.height/2}; ctx.translate( center.x, center.y ); ctx.strokeStyle = 'black'; Canvas is an element. ctx.beginPath(); You use one of its ‘context’ objects to draw to it. ctx.moveTo(0,0); 2D Context is pretty simple ctx.lineTo(14,7); Walk through ctx calls: ctx.lineTo(0,14); • translate: move “origin” to center of canvas ctx.quadraticCurveTo(7,7, 0,0); • moveTo: move without drawing ctx.closePath(); • lineTo: draw a line • curve: draw a curve ctx.stroke(); demo v } syntax highlighting: http://tohtml.com ship.html
  • 16. Moving it around var canvas, ctx, center, ship; function drawShip() { ctx.save(); function drawShipLoop() { ctx.clearRect( 0,0, canvas.width,canvas.height ); canvas = document.getElementById("demo"); ctx.translate( ship.x, ship.y ); ctx = canvas.getContext('2d'); ctx.rotate( ship.facing ); center = {x: canvas.width/2, y: canvas.height/2}; ship = {x: center.x, y: center.y, facing: 0}; ctx.strokeStyle = 'black'; ctx.beginPath(); setTimeout( updateAndDrawShip, 20 ); ctx.moveTo(0,0); } ctx.lineTo(14,7); ctx.lineTo(0,14); function updateAndDrawShip() { ctx.quadraticCurveTo(7,7, 0,0); // set a fixed velocity: ctx.closePath(); ship.y += 1; ctx.stroke(); ship.x += 1; ship.facing += Math.PI/360 * 5; ctx.restore(); } drawShip(); if (ship.y < canvas.height-10) { setTimeout( updateAndDrawShip, 20 ); } else { drawGameOver(); } function drawGameOver() { } ctx.save(); Introducing: ctx.globalComposition = "lighter"; • animation loop: updateAndDraw... ctx.font = "20px Verdana"; • keeping track of an object’s co-ords ctx.fillStyle = "rgba(50,50,50,0.9)"; • velocity & rotation ctx.fillText("Game Over", this.canvas.width/2 - 50, • clearing the canvas this.canvas.height/2); ctx.restore(); Don’t setInterval - we’ll get to that later. } globalComposition - drawing mode, lighter so we can see text in some scenarios. demo --> ship-move.html
  • 17. Controls Wow! <div id="controlBox"> <input id="controls" type="text" placeholder="click to control" autofocus="autofocus"/> <p>Controls: super-high-tech solution: <ul class="controlsInfo"> • use arrow keys to control your ship <li>up/i: accelerate forward</li> • space to fire <li>down/k: accelerate backward</li> • thinking of patenting it :) <li>left/j: spin ccw</li> <li>right/l: spin cw</li> <li>space: shoot</li> $("#controls").keydown(function(event) {self.handleKeyEvent(event)}); <li>w: switch weapon</li> $("#controls").keyup(function(event) {self.handleKeyEvent(event)}); </ul> </p> AsteroidsGame.prototype.handleKeyEvent = function(event) { </div> // TODO: send events, get rid of ifs. switch (event.which) { case 73: // i = up case 38: // up = accel if (event.type == 'keydown') { this.ship.startAccelerate(); } else { // assume keyup this.ship.stopAccelerate(); } event.preventDefault(); break; ... AsteroidsGame.js
  • 18. Controls: Feedback • Lines: thrust forward, backward, or spin (think exhaust from a jet...) • Thrust: ‘force’ in status bar. // renderThrustForward // offset from center of ship // we translate here before drawing render.x = -13; render.y = -3; ctx.strokeStyle = 'black'; ctx.beginPath(); ctx.moveTo(8,0); ctx.lineTo(0,0); ctx.moveTo(8,3); Thrust ctx.lineTo(3,3); ctx.moveTo(8,6); ctx.lineTo(0,6); ctx.closePath(); ctx.stroke(); Ships.js
  • 19. Status bars... ... are tightly-coupled to Ships atm: Ship.prototype.initialize = function(game, spatial) { // Status Bars // for displaying ship info: health, shield, thrust, ammo // TODO: move these into their own objects ... this.thrustWidth = 100; this.thrustHeight = 10; this.thrustX = this.healthX; this.thrustY = this.healthY + this.healthHeight + 5; this.thrustStartX = Math.floor( this.thrustWidth / 2 ); ... Ship.prototype.renderThrustBar = function() { var render = this.getClearThrustBarCanvas(); var ctx = render.ctx; var thrustPercent = Math.floor(this.thrust/this.maxThrust * 100); var fillWidth = Math.floor(thrustPercent * this.thrustWidth / 100 / 2); var r = 100; var b = 200 + Math.floor(thrustPercent/2); var g = 100; var fillStyle = 'rgba('+ r +','+ g +','+ b +',0.5)'; ctx.fillStyle = fillStyle; ctx.fillRect(this.thrustStartX, 0, fillWidth, this.thrustHeight); ctx.strokeStyle = 'rgba(5,5,5,0.75)'; ctx.strokeRect(0, 0, this.thrustWidth, this.thrustHeight); this.render.thrustBar = render; } http://www.flickr.com/photos/imelda/497456854/
  • 20. Drawing an Asteroid (or planet) ctx.beginPath(); ctx.arc(this.radius, this.radius, this.radius, 0, deg_to_rad[360], false); ctx.closePath() if (this.fillStyle) { ctx.fillStyle = this.fillStyle; ctx.fill(); } else { ctx.strokeStyle = this.strokeStyle; ctx.stroke(); } Show: cookie cutter level Planets.js Compositing
  • 21. Drawing an Asteroid (or planet) ctx.beginPath(); ctx.arc(this.radius, this.radius, this.radius, 0, deg_to_rad[360], false); ctx.closePath() if (this.fillStyle) { ctx.fillStyle = this.fillStyle; ctx.fill(); } else { ctx.strokeStyle = this.strokeStyle; ctx.stroke(); } // Using composition as a cookie cutter: if (this.image != null) { this.render = this.createPreRenderCanvas(this.radius*2, this.radius*2); var ctx = this.render.ctx; // Draw a circle to define what we want to keep: ctx.globalCompositeOperation = 'destination-over'; ctx.beginPath(); ctx.arc(this.radius, this.radius, this.radius, 0, deg_to_rad[360], false); ctx.closePath(); ctx.fillStyle = 'white'; ctx.fill(); // Overlay the image: ctx.globalCompositeOperation = 'source-in'; ctx.drawImage(this.image, 0, 0, this.radius*2, this.radius*2); return; } Show: cookie cutter level Planets.js Compositing
  • 22. Space Objects • DRY Planet Asteroid • Base class for all things spacey. • Everything is a circle. Ship Planetoid SpaceObject
  • 23. Err, why is everything a circle? • An asteroid is pretty much a circle, right? • And so are planets... • And so is a Ship with a shield around it! ;-) • ok, really: • Game physics can get complicated. • Keep it simple!
  • 24. Space Objects have... can... • radius • draw themselves • coords: x, y • update their positions • facing angle • accelerate, spin • velocity, spin, thrust • collide with other objects • health, mass, damage • apply damage, die • and a whole lot more... • etc... SpaceObject.js
  • 25. Game Mechanics what makes the game feel right.
  • 26. Game Mechanics what makes the game feel right. This is where it gets hairy. http://www.flickr.com/photos/maggz/2860543788/
  • 27. The god class... • AsteroidsGame does it all! • user controls, game loop, 95% of the game mechanics ... • Ok, so it’s not 5000 lines long (yet), but... • it should really be split up! AsteroidsGame.js
  • 28. Game Mechanics • velocity & spin • acceleration & drag • gravity • collision detection, impact, bounce • health, damage, life & death • object attachment & push • out-of-bounds, viewports & scrolling
  • 29. Velocity & Spin SpaceObject.prototype.initialize = function(game, SpaceObject.prototype.updateX = function(dX) { spatial) { if (this.stationary) return false; ... if (dX == 0) return false; this.x += dX; this.x = 0; // starting position on x axis return true; this.y = 0; // starting position on y axis } this.facing = 0; // currently facing angle (rad) SpaceObject.prototype.updateY = function(dY) { this.stationary = false; // should move? if (this.stationary) return false; if (dY == 0) return false; this.vX = spatial.vX || 0; // speed along X axis this.y += dY; this.vY = spatial.vY || 0; // speed along Y axis return true; this.maxV = spatial.maxV || 2; // max velocity } this.maxVSquared = this.maxV*this.maxV; // cache SpaceObject.prototype.updateFacing = function(delta) // thrust along facing { this.thrust = spatial.initialThrust || 0; if (delta == 0) return false; this.maxThrust = spatial.maxThrust || 0.5; this.facing += delta; this.thrustChanged = false; // limit facing angle to 0 <= facing <= 360 this.spin = spatial.spin || 0; // spin in Rad/sec if (this.facing >= deg_to_rad[360] || this.maxSpin = deg_to_rad[10]; this.facing <= deg_to_rad[360]) { } this.facing = this.facing % deg_to_rad[360]; } SpaceObject.prototype.updatePositions = function (objects) { if (this.facing < 0) { ... this.facing = deg_to_rad[360] + this.facing; if (this.updateFacing(this.spin)) changed = true; } if (this.updateX(this.vX)) changed = true; if (this.updateY(this.vY)) changed = true; return true; } } velocity = ∆ distance / time spin = angular velocity = ∆ angle / time
  • 30. Velocity & Spin SpaceObject.prototype.initialize = function(game, SpaceObject.prototype.updateX = function(dX) { spatial) { if (this.stationary) return false; ... if (dX == 0) return false; this.x += dX; this.x = 0; // starting position on x axis return true; this.y = 0; // starting position on y axis } this.facing = 0; // currently facing angle (rad) SpaceObject.prototype.updateY = function(dY) { this.stationary = false; // should move? if (this.stationary) return false; if (dY == 0) return false; this.vX = spatial.vX || 0; // speed along X axis this.y += dY; this.vY = spatial.vY || 0; // speed along Y axis return true; this.maxV = spatial.maxV || 2; // max velocity } this.maxVSquared = this.maxV*this.maxV; // cache SpaceObject.prototype.updateFacing = function(delta) // thrust along facing { this.thrust = spatial.initialThrust || 0; if (delta == 0) return false; this.maxThrust = spatial.maxThrust || 0.5; this.facing += delta; this.thrustChanged = false; // limit facing angle to 0 <= facing <= 360 this.spin = spatial.spin || 0; // spin in Rad/sec if (this.facing >= deg_to_rad[360] || this.maxSpin = deg_to_rad[10]; this.facing <= deg_to_rad[360]) { } this.facing = this.facing % deg_to_rad[360]; } SpaceObject.prototype.updatePositions = function (objects) { if (this.facing < 0) { ... this.facing = deg_to_rad[360] + this.facing; if (this.updateFacing(this.spin)) changed = true; } if (this.updateX(this.vX)) changed = true; if (this.updateY(this.vY)) changed = true; return true; } } velocity = ∆ distance / time spin = angular velocity = ∆ angle / time where: time = current frame rate
  • 31. Acceleration SpaceObject.prototype.initialize = function(game, spatial) { ... // thrust along facing this.thrust = spatial.initialThrust || 0; this.maxThrust = spatial.maxThrust || 0.5; this.thrustChanged = false; } SpaceObject.prototype.accelerateAlong = function(angle, thrust) { var accel = thrust/this.mass; var dX = Math.cos(angle) * accel; var dY = Math.sin(angle) * accel; this.updateVelocity(dX, dY); } acceleration = ∆ velocity / time acceleration = mass / force
  • 32. Acceleration SpaceObject.prototype.initialize = function(game, spatial) { ... // thrust along facing this.thrust = spatial.initialThrust || 0; Ship.prototype.startAccelerate = function() { this.maxThrust = spatial.maxThrust || 0.5; if (this.accelerate) return; this.thrustChanged = false; this.accelerate = true; } //console.log("thrust++"); SpaceObject.prototype.accelerateAlong = function(angle, thrust) { this.clearSlowDownInterval(); var accel = thrust/this.mass; var dX = Math.cos(angle) * accel; var self = this; var dY = Math.sin(angle) * accel; this.incThrustIntervalId = setInterval(function(){ this.updateVelocity(dX, dY); ! self.increaseThrust(); } }, 20); // real time }; Ship.prototype.increaseThrust = function() { this.incThrust(this.thrustIncrement); this.accelerateAlong(this.facing, this.thrust); Ship.prototype.initialize = function(game, spatial) { } ... spatial.mass = 10; Ship.prototype.stopAccelerate = function() { //console.log("stop thrust++"); // current state of user action: if (this.clearIncThrustInterval()) this.increaseSpin = false; this.resetThrust(); this.decreaseSpin = false; this.startSlowingDown(); this.accelerate = false; this.accelerate = false; this.decelerate = false; }; this.firing = false; Ship.prototype.clearIncThrustInterval = function() { // for moving about: if (! this.incThrustIntervalId) return false; this.thrustIncrement = 0.01; clearInterval(this.incThrustIntervalId); this.spinIncrement = deg_to_rad[0.5]; this.incThrustIntervalId = null; ... return true; } } acceleration = ∆ velocity / time acceleration = mass / force
  • 33. Acceleration SpaceObject.prototype.initialize = function(game, spatial) { ... // thrust along facing this.thrust = spatial.initialThrust || 0; Ship.prototype.startAccelerate = function() { this.maxThrust = spatial.maxThrust || 0.5; if (this.accelerate) return; this.thrustChanged = false; this.accelerate = true; } //console.log("thrust++"); SpaceObject.prototype.accelerateAlong = function(angle, thrust) { this.clearSlowDownInterval(); var accel = thrust/this.mass; var dX = Math.cos(angle) * accel; var self = this; var dY = Math.sin(angle) * accel; this.incThrustIntervalId = setInterval(function(){ this.updateVelocity(dX, dY); ! self.increaseThrust(); } }, 20); // real time }; Ship.prototype.increaseThrust = function() { this.incThrust(this.thrustIncrement); this.accelerateAlong(this.facing, this.thrust); Ship.prototype.initialize = function(game, spatial) { } ... spatial.mass = 10; Ship.prototype.stopAccelerate = function() { //console.log("stop thrust++"); // current state of user action: if (this.clearIncThrustInterval()) this.increaseSpin = false; this.resetThrust(); this.decreaseSpin = false; this.startSlowingDown(); this.accelerate = false; this.accelerate = false; this.decelerate = false; }; this.firing = false; Ship.prototype.clearIncThrustInterval = function() { // for moving about: if (! this.incThrustIntervalId) return false; this.thrustIncrement = 0.01; clearInterval(this.incThrustIntervalId); this.spinIncrement = deg_to_rad[0.5]; this.incThrustIntervalId = null; ... return true; } } acceleration = ∆ velocity / time where: time = real time acceleration = mass / force (just to confuse things)
  • 34. Drag Yes, yes, there is no drag in outer space. Very clever. I disagree.
  • 35. Drag Yes, yes, there is no drag in outer space. Very clever. http://nerdadjacent.deviantart.com/art/Ruby-Rhod-Supergreen-265156565 I disagree.
  • 36. Drag Ship.prototype.startSlowingDown = function() { Ship.prototype.slowDown = function() { // console.log("slowing down..."); var vDrag = 0.01; if (this.slowDownIntervalId) return; if (this.vX > 0) { ! this.vX -= vDrag; var self = this; } else if (this.vX < 0) { this.slowDownIntervalId = setInterval(function(){ ! this.vX += vDrag; ! self.slowDown() } }, 100); // eek! another hard-coded timeout! if (this.vY > 0) { } ! this.vY -= vDrag; } else if (this.vY < 0) { Ship.prototype.clearSlowDownInterval = function() { ! this.vY += vDrag; if (! this.slowDownIntervalId) return false; } clearInterval(this.slowDownIntervalId); this.slowDownIntervalId = null; if (Math.abs(this.vX) <= vDrag) this.vX = 0; return true; if (Math.abs(this.vY) <= vDrag) this.vY = 0; } if (this.vX == 0 && this.vY == 0) { ! // console.log('done slowing down'); ! this.clearSlowDownInterval(); } } Demo: accel + drag in blank level
  • 37. Gravity var dvX_1 = 0, dvY_1 = 0; if (! object1.stationary) { var accel_1 = object2.cache.G_x_mass / physics.dist_squared; if (accel_1 > 1e-5) { // skip if it's too small to notice if (accel_1 > this.maxAccel) accel_1 = this.maxAccel; var angle_1 = Math.atan2(physics.dX, physics.dY); dvX_1 = -Math.sin(angle_1) * accel_1; dvY_1 = -Math.cos(angle_1) * accel_1; object1.delayUpdateVelocity(dvX_1, dvY_1); } } var dvX_2 = 0, dvY_2 = 0; if (! object2.stationary) { var accel_2 = object1.cache.G_x_mass / physics.dist_squared; if (accel_2 > 1e-5) { // skip if it's too small to notice if (accel_2 > this.maxAccel) accel_2 = this.maxAccel; // TODO: angle_2 = angle_1 - PI? var angle_2 = Math.atan2(-physics.dX, -physics.dY); // note the - signs dvX_2 = -Math.sin(angle_2) * accel_2; dvY_2 = -Math.cos(angle_2) * accel_2; object2.delayUpdateVelocity(dvX_2, dvY_2); } } force = G*mass1*mass2 / dist^2 acceleration1 = force / mass1
  • 38. Collision Detection AsteroidsGame.prototype.applyGamePhysicsTo = function(object1, object2) { ... var dX = object1.x - object2.x; var dY = object1.y - object2.y; // find dist between center of mass: // avoid sqrt, we don't need dist yet... var dist_squared = dX*dX + dY*dY; var total_radius = object1.radius + object2.radius; var total_radius_squared = Math.pow(total_radius, 2); // now check if they're touching: if (dist_squared > total_radius_squared) { // nope } else { // yep this.collision( object1, object2, physics ); } ... http://www.flickr.com/photos/wsmonty/4299389080/ Aren’t you glad we stuck with circles?
  • 39. Bounce Formula: • Don’t ask.
  • 41. *bounce* ! // Thanks Emanuelle! bounce algorithm adapted from: ! // http://www.emanueleferonato.com/2007/08/19/managing-ball-vs-ball-collision-with-flash/ ! collision.angle = Math.atan2(collision.dY, collision.dX); ! var magnitude_1 = Math.sqrt(object1.vX*object1.vX + object1.vY*object1.vY); ! var magnitude_2 = Math.sqrt(object2.vX*object2.vX + object2.vY*object2.vY); ! var direction_1 = Math.atan2(object1.vY, object1.vX); ! var direction_2 = Math.atan2(object2.vY, object2.vX); ! var new_vX_1 = magnitude_1*Math.cos(direction_1-collision.angle); ! var new_vY_1 = magnitude_1*Math.sin(direction_1-collision.angle); ! var new_vX_2 = magnitude_2*Math.cos(direction_2-collision.angle); ! var new_vY_2 = magnitude_2*Math.sin(direction_2-collision.angle); ! [snip] ! // bounce the objects: ! var final_vX_1 = ( (cache1.delta_mass * new_vX_1 + object2.cache.mass_x_2 * new_vX_2) ! ! ! / cache1.total_mass * this.elasticity ); ! var final_vX_2 = ( (object1.cache.mass_x_2 * new_vX_1 + cache2.delta_mass * new_vX_2) ! ! ! / cache2.total_mass * this.elasticity ); ! var final_vY_1 = new_vY_1 * this.elasticity; ! var final_vY_2 = new_vY_2 * this.elasticity; ! var cos_collision_angle = Math.cos(collision.angle); ! var sin_collision_angle = Math.sin(collision.angle); ! var cos_collision_angle_halfPI = Math.cos(collision.angle + halfPI); ! var sin_collision_angle_halfPI = Math.sin(collision.angle + halfPI); ! var vX1 = cos_collision_angle*final_vX_1 + cos_collision_angle_halfPI*final_vY_1; ! var vY1 = sin_collision_angle*final_vX_1 + sin_collision_angle_halfPI*final_vY_1; ! object1.delaySetVelocity(vX1, vY1); ! var vX2 = cos_collision_angle*final_vX_2 + cos_collision_angle_halfPI*final_vY_2; ! var vY2 = sin_collision_angle*final_vX_2 + sin_collision_angle_halfPI*final_vY_2; ! object2.delaySetVelocity(vX2, vY2); Aren’t you *really* glad we stuck with circles?
  • 42. Making it *hurt* AsteroidsGame.prototype.collision = function(object1, object2, collision) { Shield ... // “collision” already contains a bunch of calcs Health collision[object1.id] = { cplane: {vX: new_vX_1, vY: new_vY_1}, // relative to collision plane dX: collision.dX, dY: collision.dY, magnitude: magnitude_1 } // do the same for object2 SpaceObject.prototype.collided = function(object, collision) { // let the objects fight it out this.colliding[object.id] = object; object1.collided(object2, collision); object2.collided(object1, collision); if (this.damage) { } ! var damageDone = this.damage; ! if (collision.impactSpeed != null) { ! damageDone = Math.ceil(damageDone * collision.impactSpeed); ! } ! object.decHealth( damageDone ); } } SpaceObject.prototype.decHealth = function(delta) { this.healthChanged = true; this.health -= delta; if (this.health <= 0) { ! this.health = -1; ! this.die(); } Ship.prototype.decHealth = function(delta) { } if (this.shieldActive) { When a collision occurs the Game Engine fires off 2 events to the objects in ! delta = this.decShield(delta); question } • For damage, I opted for a property rather than using mass * impact if (delta) Ship.prototype.parent.decHealth.call(this, delta); speed in the general case. } Applying damage is fairly straightforward: • Objects are responsible for damaging each other • When damage is done dec Health (for a Ship, shield first) • If health < 0, an object dies. SpaceObject.js
  • 43. Object Lifecycle SpaceObject.prototype.die = function() { this.died = true; this.update = false; this.game.objectDied( this ); } AsteroidsGame.prototype.objectDied = function(object) { // if (object.is_weapon) { //} else if (object.is_asteroid) { Asteroid.prototype.die = function() { this.parent.die.call( this ); if (object.is_planet) { if (this.spawn <= 0) return; ! throw "planet died!?"; // not allowed for (var i=0; i < this.spawn; i++) { } else if (object.is_ship) { var mass = Math.floor(this.mass / this.spawn * 1000)/1000; ! // TODO: check how many lives they've got var radius = getRandomInt(2, this.radius); ! if (object == this.ship) { var asteroid = new Asteroid(this.game, { ! this.stopGame(); mass: mass, ! } x: this.x + i/10, // don't overlap y: this.y + i/10, } vX: this.vX * Math.random(), vX: this.vY * Math.random(), this.removeObject(object); radius: radius, } health: getRandomInt(0, this.maxSpawnHealth), spawn: getRandomInt(0, this.spawn-1), AsteroidsGame.prototype.removeObject = function(object) { image: getRandomInt(0, 5) > 0 ? this.image : null, var objects = this.objects; // let physics engine handle movement }); var i = objects.indexOf(object); this.game.addObject( asteroid ); if (i >= 0) { } ! objects.splice(i,1); } ! this.objectUpdated( object ); } // avoid memory bloat: remove references to this object AsteroidsGame.prototype.addObject = function(object) { // from other objects' caches: //console.log('adding ' + object); var oid = object.id; this.objects.push( object ); for (var i=0; i < objects.length; i++) { this.objectUpdated( object ); ! delete objects[i].cache[oid]; object.preRender(); } this.cachePhysicsFor(object); } }
  • 44. Attachment • Attach objects that are ‘gently’ touching • then apply special physics • Why? AsteroidsGame.js
  • 45. Attachment • Attach objects that are ‘gently’ touching • then apply special physics • Why? Prevent the same collision from recurring. + Allows ships to land. + Poor man’s Orbit. AsteroidsGame.js
  • 46. Push! • When objects get too close • push them apart! • otherwise they overlap... (and the game physics gets weird) demo: what happens when you disable applyPushAway()
  • 47. Out-of-bounds When you have a map that is not wrapped... Simple strategy: • kill most objects that stray • push back important things like ships AsteroidsGame.prototype.applyOutOfBounds = function(object) { if (object.stationary) return; ... var level = this.level; if (object.y < 0) { var die_if_out_of_bounds = ! if (level.wrapY) { !(object.is_ship || object.is_planet); ! object.setY(level.maxY + object.y); ! } else { if (object.x < 0) { ! if (die_if_out_of_bounds && object.vY < 0) { ! if (level.wrapX) { ! ! return object.die(); ! object.setX(level.maxX + object.x); ! } ! } else { ! // push back into bounds ! if (die_if_out_of_bounds && object.vX < 0) { ! object.updateVelocity(0, 0.1); ! ! return object.die(); ! } ! } } else if (object.y > level.maxY) { ! object.updateVelocity(0.1, 0); ! if (level.wrapY) { ! } ! object.setY(object.y - level.maxY); } else if (object.x > level.maxX) { ! } else { ! if (level.wrapX) { ! if (die_if_out_of_bounds && object.vY > 0) { ! object.setX(object.x - level.maxX); ! ! return object.die(); ! } else { ! } ! if (die_if_out_of_bounds && object.vX > 0) { ! // push back into bounds ! ! return object.die(); ! object.updateVelocity(0, -0.1); ! } ! } ! object.updateVelocity(-0.1, 0); } ! } } } ...
  • 48. Viewport + Scrolling When the dimensions of your map exceed those of your canvas... AsteroidsGame.prototype.updateViewOffset = function() { var canvas = this.ctx.canvas; var offset = this.viewOffset; var dX = Math.round(this.ship.x - offset.x - canvas.width/2); var dY = Math.round(this.ship.y - offset.y - canvas.height/2); // keep the ship centered in the current view, but don't let the view // go out of bounds offset.x += dX; if (offset.x < 0) offset.x = 0; if (offset.x > this.level.maxX-canvas.width) offset.x = this.level.maxX-canvas.width; offset.y += dY; if (offset.y < 0) offset.y = 0; if (offset.y > this.level.maxY-canvas.height) offset.y = this.level.maxY-canvas.height; } AsteroidsGame.prototype.redrawCanvas = function() { ... // shift view to compensate for current offset var offset = this.viewOffset; ctx.save(); Let browser manage complexity: if you draw to canvas ctx.translate(-offset.x, -offset.y); outside of current width/height, browser doesn’t draw it.
  • 49. Putting it all together Demo: hairballs & chainsaws level.
  • 51. Gun + Bullet • Gun Ammo • Fires Bullets • Has ammo Planet Astero • Belongs to a Ship Gun Bullet Ship Planetoid SpaceObject When you shoot, bullets inherit the Ship’s velocity. Each weapon has a different recharge rate (measured in real time). Weapons.js
  • 52. Other Weapons • Gun • SprayGun • Cannon • GrenadeCannon • GravBenda™ (back in my day, we used to read books!) Current weapon
  • 54. A basic enemy... ComputerShip.prototype.findAndDestroyClosestEnemy = function() { var enemy = this.findClosestEnemy(); if (enemy == null) return; // Note: this is a basic algorith, it doesn't take a lot of things // into account (enemy trajectory & facing, other objects, etc) // navigate towards enemy // shoot at the enemy } Demo: Level Lone enemy. Show: ComputerShip class... Ships.js
  • 55. A basic enemy... ComputerShip.prototype.findAndDestroyClosestEnemy = function() { var enemy = this.findClosestEnemy(); if (enemy == null) return; // Note: this is a basic algorith, it doesn't take a lot of things // into account (enemy trajectory & facing, other objects, etc) // navigate towards enemy // shoot at the enemy } of course, it’s a bit more involved... Demo: Level Lone enemy. Show: ComputerShip class... Ships.js
  • 56. Levels • Define: • map dimensions • space objects • spawning • general properties of the canvas - color, etc Levels.js
  • 57. /****************************************************************************** * TrainingLevel: big planet out of field of view with falling asteroids. */ function TrainingLevel(game) { if (game) return this.initialize(game); return this; } TrainingLevel.inheritsFrom( Level ); TrainingLevel.description = "Training Level - learn how to fly!"; TrainingLevel.images = [ "planet.png", "planet-80px-green.png" ]; gameLevels.push(TrainingLevel); TrainingLevel.prototype.initialize = function(game) { TrainingLevel.prototype.parent.initialize.call(this, game); this.wrapX = false; this.wrapY = false; var maxX = this.maxX; var maxY = this.maxY; var canvas = this.game.ctx.canvas; this.planets.push( ! {x: 1/2*maxX, y: 1/2*maxY, mass: 100, radius: 50, damage: 5, stationary: true, image_src: "planet.png" } ! , {x: 40, y: 40, mass: 5, radius: 20, vX: 2, vY: 0, image_src:"planet-80px-green.png"} ! , {x: maxX-40, y: maxY-40, mass: 5, radius: 20, vX: -2, vY: 0, image_src:"planet-80px-green.png"} ); this.ships.push( ! {x: 4/5*canvas.width, y: 1/3*canvas.height} ); this.asteroids.push( ! {x: 1/10*maxX, y: 6/10*maxY, mass: 0.5, radius: 14, vX: 0, vY: 0, spawn: 1, health: 1}, {x: 1/10*maxX, y: 2/10*maxY, mass: 1, radius: 5, vX: 0, vY: -0.1, spawn: 3 }, {x: 5/10*maxX, y: 1/10*maxY, mass: 2, radius: 6, vX: -0.2, vY: 0.25, spawn: 4 }, {x: 5/10*maxX, y: 2/10*maxY, mass: 3, radius: 8, vX: -0.22, vY: 0.2, spawn: 7 } ); } As usual, I had grandiose plans of an interactive level editor... This was all I had time for. Levels.js
  • 58. Performance “Premature optimisation is the root of all evil.” http://c2.com/cgi/wiki?PrematureOptimization
  • 59. Use requestAnimationFrame Paul Irish knows why: • don’t animate if your canvas is not visible • adjust your frame rate based on actual performance • lets the browser manage your app better
  • 60. Profile your code • profile in different browsers • identify the slow stuff • ask yourself: “do we really need to do this?” • optimise it? • cache slow operations • change algorithm? • simplify?
  • 61. Examples... // see if we can use cached values first: // put any calculations we can avoid repeating here var g_cache1 = physics.cache1.last_G; AsteroidsGame.prototype.cachePhysicsFor = function(object1) { var g_cache2 = physics.cache2.last_G; for (var i=0; i < this.objects.length; i++) { ! var object2 = this.objects[i]; if (g_cache1) { ! if (object1 == object2) continue; var delta_dist_sq = Math.abs( physics.dist_squared - g_cache1.last_dist_squared); var percent_diff = delta_dist_sq / physics.dist_squared; ! // shared calcs // set threshold @ 5% ! var total_radius = object1.radius + object2.radius; if (percent_diff < 0.05) { ! var total_radius_squared = Math.pow(total_radius, 2); // we haven't moved much, use last G values ! var total_mass = object1.mass + object2.mass; //console.log("using G cache"); object1.delayUpdateVelocity(g_cache1.dvX, g_cache1.dvY); ! // create separate caches from perspective of objects: object2.delayUpdateVelocity(g_cache2.dvX, g_cache2.dvY); ! object1.cache[object2.id] = { return; ! total_radius: total_radius, } ! total_radius_squared: total_radius_squared, } ! total_mass: total_mass, ! delta_mass: object1.mass - object2.mass ! } // avoid overhead of update calculations & associated checks: batch together ! object2.cache[object1.id] = { SpaceObject.prototype.delayUpdateVelocity = function(dvX, dvY) { ! total_radius: total_radius, if (this._updates == null) this.init_updates(); ! total_radius_squared: total_radius_squared, this._updates.dvX += dvX; ! total_mass: total_mass, this._updates.dvY += dvY; ! delta_mass: object2.mass - object1.mass } ! } } } var dist_squared = dX*dX + dY*dY; // avoid sqrt, we don't need dist yet this.maxVSquared = this.maxV*this.maxV; // cache for speed if (accel_1 > 1e-5) { // skip if it's too small to notice ...
  • 62. Performance Great Ideas from Boris Smus: • Only redraw changes • Pre-render to another canvas • Draw background in another canvas / element • Don't use floating point co-ords ... and more ...
  • 64. See Also... Learning / examples: • https://developer.mozilla.org/en-US/docs/Canvas_tutorial • http://en.wikipedia.org/wiki/Canvas_element • http://www.html5rocks.com/en/tutorials/canvas/performance/ • http://www.canvasdemos.com • http://billmill.org/static/canvastutorial/ • http://paulirish.com/2011/requestanimationframe-for-smart-animating/ Specs: • http://dev.w3.org/html5/spec/ • http://www.khronos.org/registry/webgl/specs/latest/