20
\$\begingroup\$

I have recently been teaching a JavaScript module on a games programming undergraduate course and I made this game as a learning exercise and as an example for my students to study. I have made games before but never in JavaScript.

Could you have a look at my style and let me know if there are any obvious improvements?

the code is also on my github page

The .html file includes a canvas element, some styles and a simple game loop.

<!doctype html>
<html>
 <head>
 <title>Asteroids 2</title>
 <style type="text/css">
 body {
 text-align: center;
 margin: 0;
 }
 canvas {
 background-color: rgba(0, 0, 0, 0.9);
 width: 600px;
 height: 600px;
 }
 h1 {
 margin: 0;
 margin-bottom: -0.21em;
 font-family: helvetica;
 letter-spacing: 2.5em;
 padding-left: 2.5em;
 font-size: 1.5em;
 font-variant: small-caps;
 }
 </style>
 </head>
 <body>
 <h1>AsteroidS</h1>
 <p><label>Mute</label><input type="checkbox" checked onclick="game.muted = !game.muted"></p>
 <canvas id="asteroids" width="600" height="600"></canvas>
 <script type="application/javascript" src="asteroids.js"></script>
 <script type="application/javascript">
 var fps = 60;//frames per second
 var canvas = document.getElementById("asteroids");
 var game = new window.Asteroids(canvas);
 var framerate = 1000 / fps;//milliseconds between frames
 window.onkeydown = game.keyDown.bind(game);
 window.onkeyup = game.keyUp.bind(game);
 var previous;
 function step(timestamp) {
 if (!previous) previous = timestamp;
 var elapsed = timestamp - previous;
 game.update(elapsed/1000);
 if (elapsed >= framerate) {
 game.refresh();
 previous = timestamp;
 }
 window.requestAnimationFrame(step);
 }
 window.requestAnimationFrame(step);
 </script>
 </body>
</html>

The JavaScript library is intended to be portable. I use objects and inheritance wherever possible.

(function(window, undefined) {
function extend(ChildClass, ParentClass) {
 var parent = new ParentClass();
 ChildClass.prototype = parent;
 ChildClass.prototype.super = parent.constructor;
 ChildClass.prototype.constructor = ChildClass;
}
function random_position(canvas) {
 return {
 x: Math.random() * canvas.width,
 y: Math.random() * canvas.height
 }
}
function copy_coords(coords) {
 return { x: coords.x, y: coords.y };
}
function middle(canvas) {
 return {
 x: canvas.width / 2,
 y: canvas.height / 2
 }
}
function random_velocity(canvas) {
 //cross canvas in about 10s
 return {
 x: (Math.random() - 0.5) * canvas.width * 0.1,
 y: (Math.random() - 0.5) * canvas.height * 0.1
 }
}
function random_angle() {
 return Math.random() * 2 * Math.PI;
}
function random_rotation() {
 return (0.5 - Math.random()) * Math.PI * 0.5;
}
function distance_between(obj1, obj2) {
 return Math.sqrt(Math.pow(obj1.position.x - obj2.position.x,2) + Math.pow(obj1.position.y - obj2.position.y,2));
}
function compare(a,b) {
 if (a.score === b.score) return 0;
 if (a.score > b.score) {
 return -1;
 } else {
 return 1;
 }
}
// THE GAME===========================
function Game(canvas) {
 this.version = 0.3;
 this.canvas = canvas;
 this.c = this.canvas.getContext("2d");
 this.load_scores();
 this.ship = new Ship(this, this.canvas);
 this.waiting = true;
 this.speed = 1;
 this.muted = true;
}
Game.prototype.restart = function() {
 this.waiting = false;
 this.asteroids = [];
 this.projectiles = [];
 this.powerups = [];
 this.objects = [];
 this.level = 1;
 this.reset_asteroids();
 this.ship.reset();
 this.score = 0;
}
Game.prototype.reset_asteroids = function() {
 this.asteroids = [];
 for (var i=0;i<this.level;i++) {
 this.asteroids.push(new Asteroid(this, 2500, random_position(this.canvas), random_velocity(this.canvas), random_rotation()));
 }
}
Game.prototype.level_up = function() {
 this.level += 1;
 this.reset_asteroids();
}
Game.prototype.add_asteroid = function(asteroid) {
 this.asteroids.push(asteroid);
}
Game.prototype.update = function(elapsed) {
 elapsed *= this.speed;
 if (this.waiting) return;
 if(this.asteroids.length === 0) this.level_up();
 for (var i = 0; i < this.objects.length; i++) {
 if(this.objects[i].delete_me) {
 this.objects.splice(i, 1);
 } else {
 this.objects[i].update(elapsed);
 }
 }
 for (var i = 0; i < this.asteroids.length; i++) {
 if(this.asteroids[i].delete_me) {
 this.asteroids.splice(i, 1);
 } else {
 this.asteroids[i].update(elapsed);
 }
 }
 for (var i = 0; i < this.projectiles.length; i++) {
 if(this.projectiles[i].delete_me) {
 this.projectiles.splice(i, 1);
 } else {
 this.projectiles[i].update(elapsed);
 }
 }
 this.ship.update(elapsed);
 this.detectCollisions(elapsed);
 if(this.ship.life <= 0) {
 if (this.scores.length < 10 || this.score > this.scores[this.scores.length-1]['score']) this.save_score();
 this.waiting = true;
 }
 if (Math.random() > 0.999) {
 var possible = "AFMWLR"; //Ammo, Friction, Manouverability, Weapon, Life, Rate
 var bonus = possible.charAt(Math.floor(Math.random() * possible.length));
 this.powerups.push(new PowerUp(this, bonus));
 }
}
Game.prototype.save_score = function() {
 this.load_scores();
 var name = window.prompt('Please enter your name');
 if (name !== null && name !== "") {
 this.scores.push({
 name: name,
 score: this.score
 })
 this.scores = this.scores.sort(compare);
 this.scores = this.scores.slice(0, 10);
 }
 this.save_scores();
}
Game.prototype.load_scores = function() {
 if(typeof(Storage) !== "undefined") {
 var local_data = JSON.parse(localStorage.getItem("asteroids")) || {};
 var version = local_data.version || 0.1;
 if(version < this.version) {
 alert("New version installed, clearing scores.");
 this.scores = [];
 this.save_scores();
 } else {
 this.scores = local_data['scores'] || [];
 }
 } else {
 this.scores = [];
 }
}
Game.prototype.save_scores = function() {
 if(typeof(Storage) !== "undefined") {
 var local_data = JSON.parse(localStorage.getItem("asteroids")) || {};
 local_data['scores'] = this.scores;
 local_data['version'] = this.version;
 localStorage.setItem("asteroids", JSON.stringify(local_data));
 }
}
Game.prototype.refresh = function() {
 this.c.clearRect (0, 0, this.canvas.width, this.canvas.height);
 this.c.save();
 this.c.translate(this.canvas.width - 35, this.canvas.height - 10)
 this.c.font = "14px Arial";
 this.c.fillStyle = 'white';
 this.c.fillText("v" + this.version,0,0);
 this.c.restore();
 if (this.waiting) {
 this.c.save();
 this.c.fillStyle = 'white';
 this.c.font = "bold 16px Arial";
 this.c.translate(0, 100)
 this.c.fillText('Name',100,0);
 this.c.fillText('Score',this.canvas.width - 150,0);
 this.c.font = "16px Arial";
 this.c.translate(0, 15)
 for (var i=0;i<Math.min(this.scores.length, 10);i++) {
 this.c.translate(0, 15)
 this.c.fillText(i + 1,100,0);
 this.c.fillText(this.scores[i]['name'],140,0);
 this.c.fillText(this.scores[i]['score'],this.canvas.width - 150,0);
 this.c.translate(0, 15)
 }
 this.c.translate(0, 50)
 this.c.fillText('press space to start',180,0);
 this.c.restore();
 return;
 }
 for (var i=0;i<this.projectiles.length;i++) {
 this.projectiles[i].refresh(this.c);
 }
 for (var i=0;i<this.asteroids.length;i++) {
 this.asteroids[i].refresh(this.c);
 }
 for (var i=0;i<this.objects.length;i++) {
 this.objects[i].refresh(this.c);
 }
 this.ship.refresh(this.c);
 this.c.save();
 this.c.fillStyle = 'white';
 this.c.strokeStyle = 'white';
 this.c.translate(this.canvas.width - 120, 20)
 this.c.beginPath();
 this.c.rect(0, 0, this.ship.life, 10);
 this.c.fill();
 this.c.rect(0, 0, 100, 10);
 this.c.stroke();
 this.c.restore();
 this.c.save();
 this.c.fillStyle = 'white';
 this.c.translate(20, 30)
 this.c.font = "14px Arial";
 this.c.fillText("level " + this.level,0,0);
 this.c.restore();
 this.c.save();
 this.c.fillStyle = 'white';
 this.c.translate(20, 50)
 this.c.font = "14px Arial";
 this.c.fillText("score: " + this.score,0,0);
 this.c.restore();
 this.c.save();
 this.c.fillStyle = 'white';
 this.c.strokeStyle = 'white';
 this.c.translate(20, 70)
 this.c.font = "14px Arial";
 this.c.fillText("ammo: ",0,0);
 this.c.beginPath();
 this.c.rect(50, -10, this.ship.ammo * 100 / this.ship.max_ammo, 10);
 this.c.fill();
 this.c.rect(50, -10, 100, 10);
 this.c.stroke();
 this.c.restore();
};
Game.prototype.detectCollisions = function(elapsed) {
 for(var i=0; i<this.asteroids.length; i++) {
 var asteroid = this.asteroids[i];
 for(var j=0; j<this.projectiles.length; j++) {
 var projectile = this.projectiles[j];
 if(projectile.delete_me) continue;
 var distance = distance_between(projectile, asteroid);
 if(distance < (asteroid.radius() + projectile.radius())) {
 projectile.delete_me = true;
 asteroid.explode(projectile);
 }
 }
 var distance = distance_between(this.ship, asteroid);
 if(distance < asteroid.radius() + this.ship.radius()) {
 this.ship.life -=1;
 }
 }
 for(var i=0; i<this.powerups.length; i++) {
 var powerup = this.powerups[i];
 if (powerup.delete_me) continue;
 var distance = distance_between(this.ship, powerup);
 if(distance < powerup.radius() + this.ship.radius()) {
 this.ship.powerup(powerup.bonus);
 powerup.delete_me = true;
 }
 }
}
Game.prototype.ApplyKey = function(e, value) {
 e = e || window.event;
 if (e.keyCode == '32') {// space bar
 if(this.waiting) {
 this.restart();
 } else {
 this.ship.trigger = value;
 }
 }
 if (e.keyCode == '38') {// up arrow
 this.ship.mainThruster = value;
 }
 else if (e.keyCode == '40') {// down arrow
 this.ship.retroThruster = value;
 }
 else if (e.keyCode == '37') {//left arrow
 this.ship.leftBooster = value;
 }
 else if (e.keyCode == '39') {// right arrow
 this.ship.rightBooster = value;
 }
};
Game.prototype.keyDown = function(e) {
 this.ApplyKey(e, true);
};
Game.prototype.keyUp = function(e) {
 this.ApplyKey(e, false);
};
// Generic MASS===========================
function Mass(canvas, mass, density, position, velocity, rotation_speed) {
 this.canvas = canvas;
 this.mass = mass;
 this.density = density;
 this.position = position;
 this.velocity = velocity;
 this.angle = Math.PI;
 this.rotation_speed = rotation_speed;
 this.delete_me = false;
}
Mass.prototype.radius = function() {
 this.area = this.mass / this.density;
 //r = sqrt(area/PI)
 return Math.pow(this.area / Math.PI, 1/2);
}
Mass.prototype.apply_force = function(angle, magnitude) {
 //sohcahtoa
 var force = {
 x: Math.sin(angle) * magnitude,
 y: Math.cos(angle) * magnitude
 }
 // f = ma
 this.velocity.x += force.x / this.mass;
 this.velocity.y += force.y / this.mass;
}
Mass.prototype.apply_torque = function(torque) {
 // f = ma
 this.rotation_speed += torque / this.mass;
}
Mass.prototype.update = function(elapsed) {
 this.position.x += this.velocity.x * elapsed;
 this.position.y += this.velocity.y * elapsed;
 if (this.position.x > this.canvas.width) {
 this.position.x -= this.canvas.width;
 } else if (this.position.x < 0) {
 this.position.x += this.canvas.width;
 }
 if (this.position.y > this.canvas.height) {
 this.position.y -= this.canvas.height;
 } else if (this.position.y < 0) {
 this.position.y += this.canvas.height;
 }
 this.angle += this.rotation_speed * elapsed;
 if(this.angle > 2 * Math.PI) {
 this.angle -= 2 * Math.PI;
 }
 if(this.angle < 0) {
 this.angle += 2 * Math.PI;
 }
}
Mass.prototype.refresh = function(c) {
 c.beginPath();
 c.arc(this.position.x, this.position.y, this.radius(), 0, 2 * Math.PI, false);
 c.lineWidth = 2;
 c.strokeStyle = '#007300';
 c.stroke();
}
// ASTEROIDS===========================
Asteroid = function(game, mass, position, velocity, rotation_speed) {
 this.super(game.canvas, mass, 0.3 + Math.random() * 0.2, position, velocity, rotation_speed);
 this.game = game;
 this.explosion = new Audio('Explosion.mp3');
 this.bumpiness = 0.25 + Math.random() * 0.5;
 this.randoms = [];
 var radius = this.radius();
 var vertices = (5 + Math.random() * 10).toFixed()
 for(var i=0; i<vertices; i++) {
 this.randoms.push(radius * (1 - this.bumpiness) + radius * this.bumpiness * Math.random());
 }
}
extend(Asteroid, Mass);
Asteroid.prototype.refresh = function(c) {
 var angle = Math.PI * 2 / this.randoms.length;
 c.save();
 c.translate(this.position.x, this.position.y);
 c.rotate(this.angle);
 c.beginPath();
 c.moveTo(0, this.randoms[this.randoms.length - 1]);
 for (var i=0;i<this.randoms.length;i++) {
 c.rotate(angle);
 c.lineTo(0, this.randoms[i]);
 }
 c.lineWidth = 2;
 c.strokeStyle = '#00a300';
 c.fillStyle = '#00a300';
 c.font = "10px Arial";
 c.fillText(this.mass.toString(), -5, 0);
 c.stroke();
 c.restore();
}
Asteroid.prototype.update = function(elapsed) {
 if(this.mass < 100) this.delete_me = true;//TODO: remove this?
 Mass.prototype.update.apply(this, arguments);
}
Asteroid.prototype.explode = function(projectile) {
 if(!this.game.muted) {
 this.explosion.pause();
 this.explosion.currentTime=0;
 this.explosion.play();
 }
 this.delete_me = true;
 var mass_taken = projectile.impact;
 var new_mass = Math.ceil((this.mass - mass_taken) / 2);
 if(new_mass >= 100) {
 var new_vel = copy_coords(this.velocity);
 new_vel.x *= 0.95;
 new_vel.y *= 0.95;
 var a1 = new Asteroid(this.game, new_mass, copy_coords(this.position), new_vel, this.rotation_speed);
 var a2 = new Asteroid(this.game, new_mass, copy_coords(this.position), copy_coords(new_vel), this.rotation_speed);
 a1.density = this.density;
 a2.density = this.density;
 var split_angle1 = random_angle();
 var split_angle2 = (split_angle1 + Math.PI) % (Math.PI * 2);
 a1.apply_force(split_angle1, projectile.force);
 a2.apply_force(split_angle2, projectile.force);
 a1.apply_torque(projectile.force/10);
 a2.apply_torque(-projectile.force/10);
 this.game.add_asteroid(a1);
 this.game.add_asteroid(a2);
 } else {
 mass_taken = this.mass;
 }
 this.game.score += mass_taken;
 do {
 var p_mass = Math.min(mass_taken, projectile.impact * 0.25 * Math.random());
 var p = new Particle(this.game, p_mass, this.density, this.position, this.velocity, 10);
 var angle = random_angle();
 p.apply_force(angle, projectile.force * (1 + Math.random()));
 mass_taken -= p_mass;
 } while (mass_taken > 0)
};
// THE SHIP===========================
Ship = function(game) {
 this.super(game.canvas, 10, 0.1, {x: game.canvas.width / 2, y: game.canvas.height / 2}, {x: 0, y: 0}, 0)
 this.game = game;
 this.laser = new Audio('short_laser.mp3');
 this.reset();
}
extend(Ship, Mass);
Ship.prototype.reset = function() {
 this.max_life = 100;
 this.life = this.max_life;
 //position and movement
 this.position = middle(this.canvas);
 this.velocity = {x: 0, y: 0};
 this.rotation_speed = 0;
 this.angle = Math.PI;
 //basic movement stuff
 this.power = 750;
 this.torque = 60;
 this.friction = 0.001;
 this.turning_friction = 1.2;
 //weapon
 this.shooting_power = 25;
 this.projectile_mass = 0.25;
 this.projectile_life = 5;
 this.projectile_impact = 100;
 //ammo
 this.max_ammo = 50;
 this.ammo = this.max_ammo;
 this.reload_time = 1.5;
 this.reload_in = 0;
 this.shoot_time = 0.5;
 this.shoot_in = 0;
 this.thrust_spread = 0.25;
 this.particle_time = 0.01;
 this.particle_in = 0;
 this.mainThruster = false;
 this.leftBooster = false;
 this.rightBooster = false;
 this.retroThruster = false;
 this.trigger = false;
}
Ship.prototype.refresh = function(c) {
 var unit = this.radius();
 c.save();
 c.translate(this.position.x, this.position.y);
 c.rotate(-this.angle);
 c.beginPath();
 c.moveTo(0, 2*unit);
 c.rotate(Math.PI * 2 / 3);
 c.lineTo(0, 1*unit);
 c.rotate(Math.PI * 2 / 3);
 c.lineTo(0, 1*unit);
 c.rotate(Math.PI * 2 / 3);
 c.lineTo(0, 2*unit);
 c.lineWidth = 2;
 c.strokeStyle = '#00a300';
 c.fillStyle = '#000000';
 c.fill();
 c.stroke();
 c.restore();
}
Ship.prototype.apply_friction = function(elapsed) {
 //Fd = 1⁄2ρv2ACd 
 //drag = v * v * this.friction
 this.velocity.x += ((this.velocity.x > 0) ? -1 : 1) * elapsed * this.friction * this.velocity.x * this.velocity.x;
 this.velocity.y += ((this.velocity.y > 0) ? -1 : 1) * elapsed * this.friction * this.velocity.y * this.velocity.y;
 this.rotation_speed += ((this.rotation_speed > 0) ? -1 : 1) * elapsed * this.turning_friction * this.rotation_speed * this.rotation_speed; //retard the rotation for gameplay reasons 
}
Ship.prototype.update = function(elapsed) {
 this.apply_torque(this.torque * (this.leftBooster - this.rightBooster) * elapsed);
 var force_magnitude = this.power * (this.mainThruster - this.retroThruster) * elapsed;
 this.apply_force(this.angle, force_magnitude);
 if (force_magnitude && this.particle_in <= 0) {
 var p = new Particle(this.game, Math.random() * 2, 0.05, this.position, this.velocity, 1);
 var angle = (this.angle + Math.PI + (Math.random() - 0.5) * Math.PI * this.thrust_spread) % (Math.PI * 2);
 p.apply_force(angle, force_magnitude);
 this.particle_in = this.particle_time;
 }
 if(this.reload_in === 0) {
 this.ammo = Math.min(this.ammo + 1, this.max_ammo);
 this.reload_in = this.reload_time;
 }
 if (this.trigger && this.ammo > 0 && this.shoot_in === 0) {
 var projectile = new Projectile(this);
 if(!this.game.muted) {
 this.laser.pause();
 this.laser.currentTime=0;
 this.laser.play(); 
 }
 projectile.apply_force(this.angle, this.shooting_power);
 this.apply_force(this.angle - Math.PI, this.shooting_power);
 this.game.projectiles.push(projectile);
 this.ammo--;
 this.shoot_in = this.shoot_time;
 }
 if(this.reload_in > 0) this.reload_in -= Math.min(elapsed, this.reload_in);
 if(this.shoot_in > 0) this.shoot_in -= Math.min(elapsed, this.shoot_in);
 if(this.particle_in > 0) this.particle_in -= elapsed;
 this.apply_friction(elapsed);
 Mass.prototype.update.apply(this, arguments);
}
Ship.prototype.launch_position = function() {
 var r = this.radius();
 return {
 x: this.position.x + Math.sin(this.angle) * r * 2,
 y: this.position.y + Math.cos(this.angle) * r * 2
 };
}
Ship.prototype.powerup = function(bonus) {
// console.log("bonus: " + bonus);
 switch (bonus) {
 case "W": //weapon
 this.shooting_power += 1;
 this.projectile_impact += 2;
 break;
 case "M": //manouverability
 this.power *= 1.1;
 this.torque *= 1.1;
 break;
 case "A": //ammo
 this.max_ammo += 10;
 this.ammo += 10;
 break;
 case "F": //friction
 this.friction *= 0.999;
 this.turning_friction *= 0.999;
 case "L": //life
 this.life += this.max_life / 4;
 this.life = Math.min(this.max_life, this.life);
 case "R": //rate
 this.shoot_time *= 0.9;
 this.reload_time *= 0.9;
 }
}
// PROJECTILES===========================
Projectile = function(ship) {
 this.super(ship.canvas, ship.projectile_mass, 0.05, ship.launch_position(), {x: ship.velocity.x, y: ship.velocity.y}, 0)
 this.impact = ship.projectile_impact;
 this.life = ship.projectile_life;
 this.force = 1000 + Math.random() * 2000;
}
extend(Projectile, Mass);
Projectile.prototype.update = function(elapsed) {
 this.life -= Math.min(elapsed, this.life);
 if (this.life <= 0) {
 this.delete_me = true;
 }
 Mass.prototype.update.apply(this, arguments);
}
// PARTICLES===========================
Particle = function(game, mass, density, position, velocity, decay) {
 this.super(game.canvas, mass, density, copy_coords(position), copy_coords(velocity), 0)
 this.decay = decay;
 game.objects.push(this);
 var r = Math.floor(Math.pow(Math.random(), 3) * 255).toString(16);
 r = (r.length == 1) ? "0" + r : r;
 this.colour = '#ff' + r + "00";
 this.rotation_speed = random_rotation() * 2;
 this.angle = 0;
}
extend(Particle, Mass);
Particle.prototype.update = function(elapsed) {
 this.mass -= this.decay * elapsed;
 if (this.mass <= 0) {
 this.delete_me = true;
 }
 this.angle += this.rotation_speed * elapsed;
 Mass.prototype.update.apply(this, arguments);
}
Particle.prototype.refresh = function(c) {
 var radius = this.radius();
 c.save();
 c.translate(this.position.x, this.position.y);
 c.rotate(-this.angle);
 c.fillStyle = this.colour;
 c.strokeStyle = this.colour;
 c.lineWidth = 1;
 c.beginPath();
 for(var i = 0; i < 3; i++) {
 var r = radius * (0.5 + Math.random());
 c.rotate(2 * Math.PI / 3);
 c.moveTo(r, r);
 c.lineTo(-r, -r);
 }
 c.stroke();
 c.restore();
}
var PowerUp = function(game, bonus) {
 this.super(game.canvas, 100, 0.5, random_position(game.canvas), random_velocity(game.canvas));
 this.bonus = bonus;
 this.life = 20;
 game.objects.push(this);
}
extend(PowerUp, Mass);
PowerUp.prototype.update = function(elapsed) {
 this.life -= Math.min(elapsed, this.life);
 if (this.life <= 0) {
 this.delete_me = true;
 }
 Mass.prototype.update.apply(this, arguments);
}
PowerUp.prototype.refresh = function(c) {
 c.save();
 c.translate(this.position.x, this.position.y);
 c.beginPath();
 c.arc(0,0, this.radius(),0,2*Math.PI);
 c.fillStyle = '#00a300';
 c.fill();
 c.font = "bold 12px Arial";
 c.fillStyle = '#000000';
 c.fillText(this.bonus, -4, 4);
 c.stroke();
 c.restore();
}
window.Asteroids = Game;
}(this));
200_success
145k22 gold badges190 silver badges478 bronze badges
asked Mar 24, 2015 at 17:10
\$\endgroup\$
1
  • \$\begingroup\$ Anyone who likes this post might be interested that this code (in a modified form) was used to teach an undergraduate module and three years later is now available as a book - Introducing Javascript Game Development (apress) - with a deep exploration of how it is put together. \$\endgroup\$ Commented Jan 24, 2018 at 12:37

1 Answer 1

10
\$\begingroup\$

When using HTML local storage, you should always check for the possibility of failure. Some possible failure modes are:

  • Local storage is not supported by the browser
  • Local storage is prohibited for that domain by policy
  • The storage quota has been exceeded

Therefore, instead of checking if(typeof(Storage) !== "undefined"), you should wrap the operations in a try-catch block. Otherwise, the failures above will crash your entire application.

Game.prototype.load_scores = function() {
 try {
 var local_data = JSON.parse(localStorage.getItem("asteroids")) || {};
 var version = local_data.version || 0.1;
 if(version < this.version) {
 alert("New version installed, clearing scores.");
 this.scores = [];
 this.save_scores();
 } else {
 this.scores = local_data['scores'] || [];
 }
 } catch (e) {
 // Local storage unsupported, or permission denied
 this.scores = [];
 }
}
Game.prototype.save_scores = function() {
 try {
 var local_data = JSON.parse(localStorage.getItem("asteroids")) || {};
 local_data['scores'] = this.scores;
 local_data['version'] = this.version;
 localStorage.setItem("asteroids", JSON.stringify(local_data));
 } catch (e) {
 // Local storage unsupported, quota exceeded, or permission 
 }
}

With that recommendation and one other change, I've reproduced your code verbatim as a Stack Snippet, so that you can play it live.

The other change is that I've put your <script> in a window.onload function. That is not a problem with your code, but rather a consequence of the way Stack Snippets work: the JavaScript is injected at the end, so your initialization code would be called before the Game code is loaded if I didn't defer the initialization like that.

(function(window, undefined) {
function extend(ChildClass, ParentClass) {
 var parent = new ParentClass();
 ChildClass.prototype = parent;
 ChildClass.prototype.super = parent.constructor;
 ChildClass.prototype.constructor = ChildClass;
}
function random_position(canvas) {
 return {
 x: Math.random() * canvas.width,
 y: Math.random() * canvas.height
 }
}
function copy_coords(coords) {
 return { x: coords.x, y: coords.y };
}
function middle(canvas) {
 return {
 x: canvas.width / 2,
 y: canvas.height / 2
 }
}
function random_velocity(canvas) {
 //cross canvas in about 10s
 return {
 x: (Math.random() - 0.5) * canvas.width * 0.1,
 y: (Math.random() - 0.5) * canvas.height * 0.1
 }
}
function random_angle() {
 return Math.random() * 2 * Math.PI;
}
function random_rotation() {
 return (0.5 - Math.random()) * Math.PI * 0.5;
}
function distance_between(obj1, obj2) {
 return Math.sqrt(Math.pow(obj1.position.x - obj2.position.x,2) + Math.pow(obj1.position.y - obj2.position.y,2));
}
function compare(a,b) {
 if (a.score === b.score) return 0;
 if (a.score > b.score) {
 return -1;
 } else {
 return 1;
 }
}
// THE GAME===========================
function Game(canvas) {
 this.version = 0.3;
 this.canvas = canvas;
 this.c = this.canvas.getContext("2d");
 this.load_scores();
 this.ship = new Ship(this, this.canvas);
 this.waiting = true;
 this.speed = 1;
 this.muted = true;
}
Game.prototype.restart = function() {
 this.waiting = false;
 this.asteroids = [];
 this.projectiles = [];
 this.powerups = [];
 this.objects = [];
 this.level = 1;
 this.reset_asteroids();
 this.ship.reset();
 this.score = 0;
}
Game.prototype.reset_asteroids = function() {
 this.asteroids = [];
 for (var i=0;i<this.level;i++) {
 this.asteroids.push(new Asteroid(this, 2500, random_position(this.canvas), random_velocity(this.canvas), random_rotation()));
 }
}
Game.prototype.level_up = function() {
 this.level += 1;
 this.reset_asteroids();
}
Game.prototype.add_asteroid = function(asteroid) {
 this.asteroids.push(asteroid);
}
Game.prototype.update = function(elapsed) {
 elapsed *= this.speed;
 if (this.waiting) return;
 if(this.asteroids.length === 0) this.level_up();
 for (var i = 0; i < this.objects.length; i++) {
 if(this.objects[i].delete_me) {
 this.objects.splice(i, 1);
 } else {
 this.objects[i].update(elapsed);
 }
 }
 for (var i = 0; i < this.asteroids.length; i++) {
 if(this.asteroids[i].delete_me) {
 this.asteroids.splice(i, 1);
 } else {
 this.asteroids[i].update(elapsed);
 }
 }
 for (var i = 0; i < this.projectiles.length; i++) {
 if(this.projectiles[i].delete_me) {
 this.projectiles.splice(i, 1);
 } else {
 this.projectiles[i].update(elapsed);
 }
 }
 this.ship.update(elapsed);
 this.detectCollisions(elapsed);
 if(this.ship.life <= 0) {
 if (this.scores.length < 10 || this.score > this.scores[this.scores.length-1]['score']) this.save_score();
 this.waiting = true;
 }
 if (Math.random() > 0.999) {
 var possible = "AFMWLR"; //Ammo, Friction, Manouverability, Weapon, Life, Rate
 var bonus = possible.charAt(Math.floor(Math.random() * possible.length));
 this.powerups.push(new PowerUp(this, bonus));
 }
}
Game.prototype.save_score = function() {
 this.load_scores();
 var name = window.prompt('Please enter your name');
 if (name !== null && name !== "") {
 this.scores.push({
 name: name,
 score: this.score
 })
 this.scores = this.scores.sort(compare);
 this.scores = this.scores.slice(0, 10);
 }
 this.save_scores();
}
Game.prototype.load_scores = function() {
 try {
 var local_data = JSON.parse(localStorage.getItem("asteroids")) || {};
 var version = local_data.version || 0.1;
 if(version < this.version) {
 alert("New version installed, clearing scores.");
 this.scores = [];
 this.save_scores();
 } else {
 this.scores = local_data['scores'] || [];
 }
 } catch (e) {
 // Local storage unsupported, or permission denied
 this.scores = [];
 }
}
Game.prototype.save_scores = function() {
 try {
 var local_data = JSON.parse(localStorage.getItem("asteroids")) || {};
 local_data['scores'] = this.scores;
 local_data['version'] = this.version;
 localStorage.setItem("asteroids", JSON.stringify(local_data));
 } catch (e) {
 // Local storage unsupported, quota exceeded, or permission denied
 }
}
 
Game.prototype.refresh = function() {
 this.c.clearRect (0, 0, this.canvas.width, this.canvas.height);
 this.c.save();
 this.c.translate(this.canvas.width - 35, this.canvas.height - 10)
 this.c.font = "14px Arial";
 this.c.fillStyle = 'white';
 this.c.fillText("v" + this.version,0,0);
 this.c.restore();
 if (this.waiting) {
 this.c.save();
 this.c.fillStyle = 'white';
 this.c.font = "bold 16px Arial";
 this.c.translate(0, 100)
 this.c.fillText('Name',100,0);
 this.c.fillText('Score',this.canvas.width - 150,0);
 this.c.font = "16px Arial";
 this.c.translate(0, 15)
 for (var i=0;i<Math.min(this.scores.length, 10);i++) {
 this.c.translate(0, 15)
 this.c.fillText(i + 1,100,0);
 this.c.fillText(this.scores[i]['name'],140,0);
 this.c.fillText(this.scores[i]['score'],this.canvas.width - 150,0);
 this.c.translate(0, 15)
 }
 this.c.translate(0, 50)
 this.c.fillText('press space to start',180,0);
 this.c.restore();
 return;
 }
 for (var i=0;i<this.projectiles.length;i++) {
 this.projectiles[i].refresh(this.c);
 }
 for (var i=0;i<this.asteroids.length;i++) {
 this.asteroids[i].refresh(this.c);
 }
 for (var i=0;i<this.objects.length;i++) {
 this.objects[i].refresh(this.c);
 }
 this.ship.refresh(this.c);
 this.c.save();
 this.c.fillStyle = 'white';
 this.c.strokeStyle = 'white';
 this.c.translate(this.canvas.width - 120, 20)
 this.c.beginPath();
 this.c.rect(0, 0, this.ship.life, 10);
 this.c.fill();
 this.c.rect(0, 0, 100, 10);
 this.c.stroke();
 this.c.restore();
 this.c.save();
 this.c.fillStyle = 'white';
 this.c.translate(20, 30)
 this.c.font = "14px Arial";
 this.c.fillText("level " + this.level,0,0);
 this.c.restore();
 this.c.save();
 this.c.fillStyle = 'white';
 this.c.translate(20, 50)
 this.c.font = "14px Arial";
 this.c.fillText("score: " + this.score,0,0);
 this.c.restore();
 this.c.save();
 this.c.fillStyle = 'white';
 this.c.strokeStyle = 'white';
 this.c.translate(20, 70)
 this.c.font = "14px Arial";
 this.c.fillText("ammo: ",0,0);
 this.c.beginPath();
 this.c.rect(50, -10, this.ship.ammo * 100 / this.ship.max_ammo, 10);
 this.c.fill();
 this.c.rect(50, -10, 100, 10);
 this.c.stroke();
 this.c.restore();
};
Game.prototype.detectCollisions = function(elapsed) {
 for(var i=0; i<this.asteroids.length; i++) {
 var asteroid = this.asteroids[i];
 for(var j=0; j<this.projectiles.length; j++) {
 var projectile = this.projectiles[j];
 if(projectile.delete_me) continue;
 var distance = distance_between(projectile, asteroid);
 if(distance < (asteroid.radius() + projectile.radius())) {
 projectile.delete_me = true;
 asteroid.explode(projectile);
 }
 }
 var distance = distance_between(this.ship, asteroid);
 if(distance < asteroid.radius() + this.ship.radius()) {
 this.ship.life -=1;
 }
 }
 for(var i=0; i<this.powerups.length; i++) {
 var powerup = this.powerups[i];
 if (powerup.delete_me) continue;
 var distance = distance_between(this.ship, powerup);
 if(distance < powerup.radius() + this.ship.radius()) {
 this.ship.powerup(powerup.bonus);
 powerup.delete_me = true;
 }
 }
}
Game.prototype.ApplyKey = function(e, value) {
 e = e || window.event;
 if (e.keyCode == '32') {// space bar
 if(this.waiting) {
 this.restart();
 } else {
 this.ship.trigger = value;
 }
 }
 if (e.keyCode == '38') {// up arrow
 this.ship.mainThruster = value;
 }
 else if (e.keyCode == '40') {// down arrow
 this.ship.retroThruster = value;
 }
 else if (e.keyCode == '37') {//left arrow
 this.ship.leftBooster = value;
 }
 else if (e.keyCode == '39') {// right arrow
 this.ship.rightBooster = value;
 }
};
Game.prototype.keyDown = function(e) {
 this.ApplyKey(e, true);
};
Game.prototype.keyUp = function(e) {
 this.ApplyKey(e, false);
};
// Generic MASS===========================
function Mass(canvas, mass, density, position, velocity, rotation_speed) {
 this.canvas = canvas;
 this.mass = mass;
 this.density = density;
 this.position = position;
 this.velocity = velocity;
 this.angle = Math.PI;
 this.rotation_speed = rotation_speed;
 this.delete_me = false;
}
Mass.prototype.radius = function() {
 this.area = this.mass / this.density;
 //r = sqrt(area/PI)
 return Math.pow(this.area / Math.PI, 1/2);
}
Mass.prototype.apply_force = function(angle, magnitude) {
 //sohcahtoa
 var force = {
 x: Math.sin(angle) * magnitude,
 y: Math.cos(angle) * magnitude
 }
 // f = ma
 this.velocity.x += force.x / this.mass;
 this.velocity.y += force.y / this.mass;
}
Mass.prototype.apply_torque = function(torque) {
 // f = ma
 this.rotation_speed += torque / this.mass;
}
Mass.prototype.update = function(elapsed) {
 this.position.x += this.velocity.x * elapsed;
 this.position.y += this.velocity.y * elapsed;
 if (this.position.x > this.canvas.width) {
 this.position.x -= this.canvas.width;
 } else if (this.position.x < 0) {
 this.position.x += this.canvas.width;
 }
 if (this.position.y > this.canvas.height) {
 this.position.y -= this.canvas.height;
 } else if (this.position.y < 0) {
 this.position.y += this.canvas.height;
 }
 this.angle += this.rotation_speed * elapsed;
 if(this.angle > 2 * Math.PI) {
 this.angle -= 2 * Math.PI;
 }
 if(this.angle < 0) {
 this.angle += 2 * Math.PI;
 }
}
Mass.prototype.refresh = function(c) {
 c.beginPath();
 c.arc(this.position.x, this.position.y, this.radius(), 0, 2 * Math.PI, false);
 c.lineWidth = 2;
 c.strokeStyle = '#007300';
 c.stroke();
}
// ASTEROIDS===========================
Asteroid = function(game, mass, position, velocity, rotation_speed) {
 this.super(game.canvas, mass, 0.3 + Math.random() * 0.2, position, velocity, rotation_speed);
 this.game = game;
 this.explosion = new Audio('Explosion.mp3');
 this.bumpiness = 0.25 + Math.random() * 0.5;
 this.randoms = [];
 var radius = this.radius();
 var vertices = (5 + Math.random() * 10).toFixed()
 for(var i=0; i<vertices; i++) {
 this.randoms.push(radius * (1 - this.bumpiness) + radius * this.bumpiness * Math.random());
 }
}
extend(Asteroid, Mass);
Asteroid.prototype.refresh = function(c) {
 var angle = Math.PI * 2 / this.randoms.length;
 c.save();
 c.translate(this.position.x, this.position.y);
 c.rotate(this.angle);
 c.beginPath();
 c.moveTo(0, this.randoms[this.randoms.length - 1]);
 for (var i=0;i<this.randoms.length;i++) {
 c.rotate(angle);
 c.lineTo(0, this.randoms[i]);
 }
 c.lineWidth = 2;
 c.strokeStyle = '#00a300';
 c.fillStyle = '#00a300';
 c.font = "10px Arial";
 c.fillText(this.mass.toString(), -5, 0);
 c.stroke();
 c.restore();
}
Asteroid.prototype.update = function(elapsed) {
 if(this.mass < 100) this.delete_me = true;//TODO: remove this?
 Mass.prototype.update.apply(this, arguments);
}
Asteroid.prototype.explode = function(projectile) {
 if(!this.game.muted) {
 this.explosion.pause();
 this.explosion.currentTime=0;
 this.explosion.play();
 }
 this.delete_me = true;
 var mass_taken = projectile.impact;
 var new_mass = Math.ceil((this.mass - mass_taken) / 2);
 if(new_mass >= 100) {
 var new_vel = copy_coords(this.velocity);
 new_vel.x *= 0.95;
 new_vel.y *= 0.95;
 var a1 = new Asteroid(this.game, new_mass, copy_coords(this.position), new_vel, this.rotation_speed);
 var a2 = new Asteroid(this.game, new_mass, copy_coords(this.position), copy_coords(new_vel), this.rotation_speed);
 a1.density = this.density;
 a2.density = this.density;
 var split_angle1 = random_angle();
 var split_angle2 = (split_angle1 + Math.PI) % (Math.PI * 2);
 a1.apply_force(split_angle1, projectile.force);
 a2.apply_force(split_angle2, projectile.force);
 a1.apply_torque(projectile.force/10);
 a2.apply_torque(-projectile.force/10);
 this.game.add_asteroid(a1);
 this.game.add_asteroid(a2);
 } else {
 mass_taken = this.mass;
 }
 this.game.score += mass_taken;
 do {
 var p_mass = Math.min(mass_taken, projectile.impact * 0.25 * Math.random());
 var p = new Particle(this.game, p_mass, this.density, this.position, this.velocity, 10);
 var angle = random_angle();
 p.apply_force(angle, projectile.force * (1 + Math.random()));
 mass_taken -= p_mass;
 } while (mass_taken > 0)
};
// THE SHIP===========================
Ship = function(game) {
 this.super(game.canvas, 10, 0.1, {x: game.canvas.width / 2, y: game.canvas.height / 2}, {x: 0, y: 0}, 0)
 this.game = game;
 this.laser = new Audio('short_laser.mp3');
 this.reset();
}
extend(Ship, Mass);
Ship.prototype.reset = function() {
 this.max_life = 100;
 this.life = this.max_life;
 //position and movement
 this.position = middle(this.canvas);
 this.velocity = {x: 0, y: 0};
 this.rotation_speed = 0;
 this.angle = Math.PI;
 //basic movement stuff
 this.power = 750;
 this.torque = 60;
 this.friction = 0.001;
 this.turning_friction = 1.2;
 //weapon
 this.shooting_power = 25;
 this.projectile_mass = 0.25;
 this.projectile_life = 5;
 this.projectile_impact = 100;
 //ammo
 this.max_ammo = 50;
 this.ammo = this.max_ammo;
 this.reload_time = 1.5;
 this.reload_in = 0;
 this.shoot_time = 0.5;
 this.shoot_in = 0;
 this.thrust_spread = 0.25;
 this.particle_time = 0.01;
 this.particle_in = 0;
 this.mainThruster = false;
 this.leftBooster = false;
 this.rightBooster = false;
 this.retroThruster = false;
 this.trigger = false;
}
Ship.prototype.refresh = function(c) {
 var unit = this.radius();
 c.save();
 c.translate(this.position.x, this.position.y);
 c.rotate(-this.angle);
 c.beginPath();
 c.moveTo(0, 2*unit);
 c.rotate(Math.PI * 2 / 3);
 c.lineTo(0, 1*unit);
 c.rotate(Math.PI * 2 / 3);
 c.lineTo(0, 1*unit);
 c.rotate(Math.PI * 2 / 3);
 c.lineTo(0, 2*unit);
 c.lineWidth = 2;
 c.strokeStyle = '#00a300';
 c.fillStyle = '#000000';
 c.fill();
 c.stroke();
 c.restore();
}
Ship.prototype.apply_friction = function(elapsed) {
 //Fd = 1⁄2ρv2ACd 
 //drag = v * v * this.friction
 this.velocity.x += ((this.velocity.x > 0) ? -1 : 1) * elapsed * this.friction * this.velocity.x * this.velocity.x;
 this.velocity.y += ((this.velocity.y > 0) ? -1 : 1) * elapsed * this.friction * this.velocity.y * this.velocity.y;
 this.rotation_speed += ((this.rotation_speed > 0) ? -1 : 1) * elapsed * this.turning_friction * this.rotation_speed * this.rotation_speed; //retard the rotation for gameplay reasons 
}
Ship.prototype.update = function(elapsed) {
 this.apply_torque(this.torque * (this.leftBooster - this.rightBooster) * elapsed);
 var force_magnitude = this.power * (this.mainThruster - this.retroThruster) * elapsed;
 this.apply_force(this.angle, force_magnitude);
 if (force_magnitude && this.particle_in <= 0) {
 var p = new Particle(this.game, Math.random() * 2, 0.05, this.position, this.velocity, 1);
 var angle = (this.angle + Math.PI + (Math.random() - 0.5) * Math.PI * this.thrust_spread) % (Math.PI * 2);
 p.apply_force(angle, force_magnitude);
 this.particle_in = this.particle_time;
 }
 if(this.reload_in === 0) {
 this.ammo = Math.min(this.ammo + 1, this.max_ammo);
 this.reload_in = this.reload_time;
 }
 if (this.trigger && this.ammo > 0 && this.shoot_in === 0) {
 var projectile = new Projectile(this);
 if(!this.game.muted) {
 this.laser.pause();
 this.laser.currentTime=0;
 this.laser.play(); 
 }
 projectile.apply_force(this.angle, this.shooting_power);
 this.apply_force(this.angle - Math.PI, this.shooting_power);
 this.game.projectiles.push(projectile);
 this.ammo--;
 this.shoot_in = this.shoot_time;
 }
 if(this.reload_in > 0) this.reload_in -= Math.min(elapsed, this.reload_in);
 if(this.shoot_in > 0) this.shoot_in -= Math.min(elapsed, this.shoot_in);
 if(this.particle_in > 0) this.particle_in -= elapsed;
 this.apply_friction(elapsed);
 Mass.prototype.update.apply(this, arguments);
}
Ship.prototype.launch_position = function() {
 var r = this.radius();
 return {
 x: this.position.x + Math.sin(this.angle) * r * 2,
 y: this.position.y + Math.cos(this.angle) * r * 2
 };
}
Ship.prototype.powerup = function(bonus) {
// console.log("bonus: " + bonus);
 switch (bonus) {
 case "W": //weapon
 this.shooting_power += 1;
 this.projectile_impact += 2;
 break;
 case "M": //manouverability
 this.power *= 1.1;
 this.torque *= 1.1;
 break;
 case "A": //ammo
 this.max_ammo += 10;
 this.ammo += 10;
 break;
 case "F": //friction
 this.friction *= 0.999;
 this.turning_friction *= 0.999;
 case "L": //life
 this.life += this.max_life / 4;
 this.life = Math.min(this.max_life, this.life);
 case "R": //rate
 this.shoot_time *= 0.9;
 this.reload_time *= 0.9;
 }
}
// PROJECTILES===========================
Projectile = function(ship) {
 this.super(ship.canvas, ship.projectile_mass, 0.05, ship.launch_position(), {x: ship.velocity.x, y: ship.velocity.y}, 0)
 this.impact = ship.projectile_impact;
 this.life = ship.projectile_life;
 this.force = 1000 + Math.random() * 2000;
}
extend(Projectile, Mass);
Projectile.prototype.update = function(elapsed) {
 this.life -= Math.min(elapsed, this.life);
 if (this.life <= 0) {
 this.delete_me = true;
 }
 Mass.prototype.update.apply(this, arguments);
}
// PARTICLES===========================
Particle = function(game, mass, density, position, velocity, decay) {
 this.super(game.canvas, mass, density, copy_coords(position), copy_coords(velocity), 0)
 this.decay = decay;
 game.objects.push(this);
 var r = Math.floor(Math.pow(Math.random(), 3) * 255).toString(16);
 r = (r.length == 1) ? "0" + r : r;
 this.colour = '#ff' + r + "00";
 this.rotation_speed = random_rotation() * 2;
 this.angle = 0;
}
extend(Particle, Mass);
Particle.prototype.update = function(elapsed) {
 this.mass -= this.decay * elapsed;
 if (this.mass <= 0) {
 this.delete_me = true;
 }
 this.angle += this.rotation_speed * elapsed;
 Mass.prototype.update.apply(this, arguments);
}
Particle.prototype.refresh = function(c) {
 var radius = this.radius();
 c.save();
 c.translate(this.position.x, this.position.y);
 c.rotate(-this.angle);
 c.fillStyle = this.colour;
 c.strokeStyle = this.colour;
 c.lineWidth = 1;
 c.beginPath();
 for(var i = 0; i < 3; i++) {
 var r = radius * (0.5 + Math.random());
 c.rotate(2 * Math.PI / 3);
 c.moveTo(r, r);
 c.lineTo(-r, -r);
 }
 c.stroke();
 c.restore();
}
var PowerUp = function(game, bonus) {
 this.super(game.canvas, 100, 0.5, random_position(game.canvas), random_velocity(game.canvas));
 this.bonus = bonus;
 this.life = 20;
 game.objects.push(this);
}
extend(PowerUp, Mass);
PowerUp.prototype.update = function(elapsed) {
 this.life -= Math.min(elapsed, this.life);
 if (this.life <= 0) {
 this.delete_me = true;
 }
 Mass.prototype.update.apply(this, arguments);
}
PowerUp.prototype.refresh = function(c) {
 c.save();
 c.translate(this.position.x, this.position.y);
 c.beginPath();
 c.arc(0,0, this.radius(),0,2*Math.PI);
 c.fillStyle = '#00a300';
 c.fill();
 c.font = "bold 12px Arial";
 c.fillStyle = '#000000';
 c.fillText(this.bonus, -4, 4);
 c.stroke();
 c.restore();
}
window.Asteroids = Game;
}(this));
<!doctype html>
<html>
 <head>
 <title>Asteroids 2</title>
 <style type="text/css">
 body {
 text-align: center;
 margin: 0;
 }
 canvas {
 background-color: rgba(0, 0, 0, 0.9);
 width: 600px;
 height: 600px;
 }
 h1 {
 margin: 0;
 margin-bottom: -0.21em;
 font-family: helvetica;
 letter-spacing: 2.5em;
 padding-left: 2.5em;
 font-size: 1.5em;
 font-variant: small-caps;
 }
 </style>
 </head>
 <body>
 <h1>AsteroidS</h1>
 <p><label>Mute</label><input type="checkbox" checked onclick="game.muted = !game.muted"></p>
 <canvas id="asteroids" width="600" height="600"></canvas>
 <script type="application/javascript" src="asteroids.js"></script>
 <script type="application/javascript">
 window.onload = function() { // MOD: wrapped code in onload()
 var fps = 60;//frames per second
 var canvas = document.getElementById("asteroids");
 var game = new window.Asteroids(canvas);
 var framerate = 1000 / fps;//milliseconds between frames
 window.onkeydown = game.keyDown.bind(game);
 window.onkeyup = game.keyUp.bind(game);
 var previous;
 function step(timestamp) {
 if (!previous) previous = timestamp;
 var elapsed = timestamp - previous;
 game.update(elapsed/1000);
 if (elapsed >= framerate) {
 game.refresh();
 previous = timestamp;
 }
 window.requestAnimationFrame(step);
 }
 window.requestAnimationFrame(step);
 };
 </script>
 </body>
</html>

answered Mar 24, 2015 at 22:22
\$\endgroup\$
3
  • \$\begingroup\$ You forgot one VERY ANNOYING detail: block the events! The game is almost unplayable if there is a scrollbar. \$\endgroup\$ Commented Mar 24, 2015 at 23:13
  • \$\begingroup\$ @IsmaelMiguel This is far from a comprehensive review. I just made the minimum changes to make the code work for me. You should write your suggestion as a separate answer. \$\endgroup\$ Commented Mar 25, 2015 at 0:43
  • \$\begingroup\$ Far? Just add e.preventDefault() on Game.prototype.ApplyKey \$\endgroup\$ Commented Mar 25, 2015 at 1:52

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.