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

What's hot (20)

PDF
HTML5 Canvas - The Future of Graphics on the Web
Robin Hawkes
 
PPTX
MongoDB Live Hacking
Tobias Trelle
 
PPTX
Introduction to HTML5 Canvas
Mindy McAdams
 
PPTX
Gpu programming with java
Gary Sieling
 
PDF
Introduction to CUDA C: NVIDIA : Notes
Subhajit Sahu
 
PDF
tutorial5
tutorialsruby
 
PDF
Intro to Clojure's core.async
Leonardo Borges
 
PDF
Html5 canvas
Gary Yeh
 
PDF
CUDA Raytracing을 이용한 Voxel오브젝트 가시성 테스트
YEONG-CHEON YOU
 
PPTX
Box2D with SIMD in JavaScript
Intel® Software
 
PPTX
Trident International Graphics Workshop 2014 1/5
Takao Wada
 
PDF
Prototype UI Intro
Juriy Zaytsev
 
PDF
Introduction to cuda geek camp singapore 2011
Raymond Tay
 
PPTX
WebGL and three.js - Web 3D Graphics
PSTechSerbia
 
PDF
WebGL - 3D in your Browser
Phil Reither
 
PDF
Monolith to Reactive Microservices
Reactivesummit
 
PDF
Qt Widget In-Depth
account inactive
 
PDF
kissy-past-now-future
yiming he
 
PDF
Exploiting Concurrency with Dynamic Languages
Tobias Lindaaker
 
PPTX
KISSY 的昨天、今天与明天
tblanlan
 
HTML5 Canvas - The Future of Graphics on the Web
Robin Hawkes
 
MongoDB Live Hacking
Tobias Trelle
 
Introduction to HTML5 Canvas
Mindy McAdams
 
Gpu programming with java
Gary Sieling
 
Introduction to CUDA C: NVIDIA : Notes
Subhajit Sahu
 
tutorial5
tutorialsruby
 
Intro to Clojure's core.async
Leonardo Borges
 
Html5 canvas
Gary Yeh
 
CUDA Raytracing을 이용한 Voxel오브젝트 가시성 테스트
YEONG-CHEON YOU
 
Box2D with SIMD in JavaScript
Intel® Software
 
Trident International Graphics Workshop 2014 1/5
Takao Wada
 
Prototype UI Intro
Juriy Zaytsev
 
Introduction to cuda geek camp singapore 2011
Raymond Tay
 
WebGL and three.js - Web 3D Graphics
PSTechSerbia
 
WebGL - 3D in your Browser
Phil Reither
 
Monolith to Reactive Microservices
Reactivesummit
 
Qt Widget In-Depth
account inactive
 
kissy-past-now-future
yiming he
 
Exploiting Concurrency with Dynamic Languages
Tobias Lindaaker
 
KISSY 的昨天、今天与明天
tblanlan
 

Viewers also liked (6)

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

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

PPT
Rotoscope inthebrowserppt billy
nimbleltd
 
PDF
How to build a html5 websites.v1
Bitla Software
 
PPTX
Introduction to Canvas - Toronto HTML5 User Group
dreambreeze
 
PDF
Introduction to Canvas - Toronto HTML5 User Group
bernice-chan
 
PPTX
Intro to Canva
dreambreeze
 
PPTX
How to make a video game
dandylion13
 
PDF
Exploring Canvas
Kevin Hoyt
 
PDF
Google's HTML5 Work: what's next?
Patrick Chanezon
 
PDF
Intro to HTML5 Canvas
Juho Vepsäläinen
 
KEY
Exploring Canvas
Kevin Hoyt
 
PPTX
HTML5 Animation in Mobile Web Games
livedoor
 
PDF
Mapping the world with Twitter
carlo zapponi
 
PDF
Is HTML5 Ready? (workshop)
Remy Sharp
 
PDF
Is html5-ready-workshop-110727181512-phpapp02
PL dream
 
PDF
I Can't Believe It's Not Flash
Thomas Fuchs
 
PDF
JavaOne 2009 - 2d Vector Graphics in the browser with Canvas and SVG
Patrick Chanezon
 
PDF
HTML5: where flash isn't needed anymore
Remy Sharp
 
KEY
Interactive Graphics
Blazing Cloud
 
KEY
The Canvas API for Rubyists
deanhudson
 
PPTX
Advanced html5 diving into the canvas tag
David Voyles
 
Rotoscope inthebrowserppt billy
nimbleltd
 
How to build a html5 websites.v1
Bitla Software
 
Introduction to Canvas - Toronto HTML5 User Group
dreambreeze
 
Introduction to Canvas - Toronto HTML5 User Group
bernice-chan
 
Intro to Canva
dreambreeze
 
How to make a video game
dandylion13
 
Exploring Canvas
Kevin Hoyt
 
Google's HTML5 Work: what's next?
Patrick Chanezon
 
Intro to HTML5 Canvas
Juho Vepsäläinen
 
Exploring Canvas
Kevin Hoyt
 
HTML5 Animation in Mobile Web Games
livedoor
 
Mapping the world with Twitter
carlo zapponi
 
Is HTML5 Ready? (workshop)
Remy Sharp
 
Is html5-ready-workshop-110727181512-phpapp02
PL dream
 
I Can't Believe It's Not Flash
Thomas Fuchs
 
JavaOne 2009 - 2d Vector Graphics in the browser with Canvas and SVG
Patrick Chanezon
 
HTML5: where flash isn't needed anymore
Remy Sharp
 
Interactive Graphics
Blazing Cloud
 
The Canvas API for Rubyists
deanhudson
 
Advanced html5 diving into the canvas tag
David Voyles
 

More from Steve Purkis (14)

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

Recently uploaded (20)

PPTX
Building Search Using OpenSearch: Limitations and Workarounds
Sease
 
PDF
Jak MŚP w Europie Środkowo-Wschodniej odnajdują się w świecie AI
dominikamizerska1
 
PDF
Newgen Beyond Frankenstein_Build vs Buy_Digital_version.pdf
darshakparmar
 
PDF
SWEBOK Guide and Software Services Engineering Education
Hironori Washizaki
 
PDF
"AI Transformation: Directions and Challenges", Pavlo Shaternik
Fwdays
 
PDF
DevBcn - Building 10x Organizations Using Modern Productivity Metrics
Justin Reock
 
PDF
How Startups Are Growing Faster with App Developers in Australia.pdf
India App Developer
 
PDF
[Newgen] NewgenONE Marvin Brochure 1.pdf
darshakparmar
 
PDF
Building Real-Time Digital Twins with IBM Maximo & ArcGIS Indoors
Safe Software
 
PDF
CIFDAQ Weekly Market Wrap for 11th July 2025
CIFDAQ
 
PDF
Windsurf Meetup Ottawa 2025-07-12 - Planning Mode at Reliza.pdf
Pavel Shukhman
 
PDF
Bitcoin for Millennials podcast with Bram, Power Laws of Bitcoin
Stephen Perrenod
 
PDF
Smart Trailers 2025 Update with History and Overview
Paul Menig
 
PDF
Empower Inclusion Through Accessible Java Applications
Ana-Maria Mihalceanu
 
PPTX
UiPath Academic Alliance Educator Panels: Session 2 - Business Analyst Content
DianaGray10
 
PDF
Reverse Engineering of Security Products: Developing an Advanced Microsoft De...
nwbxhhcyjv
 
PDF
Log-Based Anomaly Detection: Enhancing System Reliability with Machine Learning
Mohammed BEKKOUCHE
 
PPTX
Webinar: Introduction to LF Energy EVerest
DanBrown980551
 
PDF
Using FME to Develop Self-Service CAD Applications for a Major UK Police Force
Safe Software
 
PPTX
OpenID AuthZEN - Analyst Briefing July 2025
David Brossard
 
Building Search Using OpenSearch: Limitations and Workarounds
Sease
 
Jak MŚP w Europie Środkowo-Wschodniej odnajdują się w świecie AI
dominikamizerska1
 
Newgen Beyond Frankenstein_Build vs Buy_Digital_version.pdf
darshakparmar
 
SWEBOK Guide and Software Services Engineering Education
Hironori Washizaki
 
"AI Transformation: Directions and Challenges", Pavlo Shaternik
Fwdays
 
DevBcn - Building 10x Organizations Using Modern Productivity Metrics
Justin Reock
 
How Startups Are Growing Faster with App Developers in Australia.pdf
India App Developer
 
[Newgen] NewgenONE Marvin Brochure 1.pdf
darshakparmar
 
Building Real-Time Digital Twins with IBM Maximo & ArcGIS Indoors
Safe Software
 
CIFDAQ Weekly Market Wrap for 11th July 2025
CIFDAQ
 
Windsurf Meetup Ottawa 2025-07-12 - Planning Mode at Reliza.pdf
Pavel Shukhman
 
Bitcoin for Millennials podcast with Bram, Power Laws of Bitcoin
Stephen Perrenod
 
Smart Trailers 2025 Update with History and Overview
Paul Menig
 
Empower Inclusion Through Accessible Java Applications
Ana-Maria Mihalceanu
 
UiPath Academic Alliance Educator Panels: Session 2 - Business Analyst Content
DianaGray10
 
Reverse Engineering of Security Products: Developing an Advanced Microsoft De...
nwbxhhcyjv
 
Log-Based Anomaly Detection: Enhancing System Reliability with Machine Learning
Mohammed BEKKOUCHE
 
Webinar: Introduction to LF Energy EVerest
DanBrown980551
 
Using FME to Develop Self-Service CAD Applications for a Major UK Police Force
Safe Software
 
OpenID AuthZEN - Analyst Briefing July 2025
David Brossard
 

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/