Click here to Skip to main content
15,881,455 members
Articles / Web Development / HTML

Learn JavaScript Part 2 - Space Invaders

Rate me:
Please Sign up or sign in to vote.
4.93/5 (47 votes)
31 Jan 2014CPOL16 min read 153.2K   2.3K   86   41
In part two of the learn JavaScript series we'll create the classic space invaders game.

Introduction   

Welcome to part two of my series on learning Javascript. In this article, we'll create the classic Space Invaders game, step-by-step. For now we're keeping it really simple - no frameworks, just raw JavaScript and HTML. Here's a screenshot of what we're going to make. Click it to try it in your browser

Image 1

The Learn JavaScript Series

This is part two of my Learn JavaScript series:

Learn JavaScript Part 1 - Create a  StarField 
Learn JavaScript Part 2 - Space Invaders  
Learn JavaScript Part 3 - AngularJS and Langton's Ant 

The series is all about learning JavaScript and the HTML5 tech stack with hands on projects. 

Step 1 - Folder Structure

As we're going through JavaScript development from the beginning, let's briefly talk about the folder structure for a typical webpage. Here's what we'd normally set up: 

spaceinvaders
 - js
 - css
 - img
 - lib 

That's pretty lean and standard. We've got a 'js' folder for our JavaScript files, a 'css' folder for cascading style sheets, a 'lib' folder for third party libraries such as bootstrap, and an 'img' folder for images.

Put the folder structure together as shown - we'll use the same layout in the future.

Step 2 - The HTML

Just like in the previous tutorial, we'll have a very simple webpage for our game - mostly what it will do is include some JavaScript and start the game running.

Here's how we'll start. 

HTML
<!DOCTYPE html>
<html>
	<head>
		<title>Space Invaders</title>
		<link rel="stylesheet" type="text/css" href="css/core.css">
		<link rel="stylesheet" type="text/css" href="css/typeography.css">
		<style>
	
			/* Styling needed for a fullscreen canvas and no scrollbars. */
			body, html { 
		    width: 100%;
		    height: 100%;
		    margin: 0;
		    padding: 0;
		    overflow: hidden;
			}
 
			/* Here's where we'll put Space Invader styles... */
		</style>
	</head>
	<body>
		<!-- this is where the Space Invaders HTML will go... */ -->
		<script src="js/starfield.js"></script>
		<script src="js/spaceinvaders.js"></script>
		<script>
 
			/* And this is where we'll put our JS. */
		</script>
	</body>
</html> 

We start with the html doctype - this is an HTML5 page. Then we set the title and include two stylesheets. These stylesheets I use a lot for simple tutorials, they just clean up some of the default browser styles, they've very simple, you can get them at github.com/dwmkerr/html5base, or you can get them from the download. 

Next we have a little css for the body and html elements - what we're doing here is making sure we're not going to have any scrollbars, as this game will just fill the window. We've also got a placeholder for styles we'll add later.

After this there's an HTML comment showing where we'll put the game elements.

To finish up, we've included the starfield script from last time (this will be the background of the game) and a 'spaceinvaders.js' file, which we'll create shortly. Then there's a script block for anything else we need. Now we're good to go!

Step 3 - A Starfield Background 

See how wildly useful these tutorials are? We've already found a use for the first one. We'll start by adding a starfield, the same one we made in the last tutorial. (If you didn't follow that tutorial, you can get the starfield here github.com/dwmkerr/starfield). 

We'll add a div that'll hold the starfield. Add the following HTML to the page: 

HTML
<!-- this is where the Space Invaders HTML will go...  -->
 
<!-- Here's a starfield for the background. -->
<div id="starfield"></div>

This div will contain the starfield. Now we can style it in the style element of the header.

CSS
 #starfield {
    width:100%;
    height:100%;
    z-index: -1;
    position: absolute;
    left: 0px;
    top: 0px;
} 

Finally, in the script block we can actually create the starfield.

JavaScript
/* And this is where we'll put our JS. */
 
//  Create the starfield.
var container = document.getElementById('starfield');
var starfield = new Starfield();
starfield.initialise(container);
starfield.start(); 

As we went through the starfield in detail in the last tutorial, we don't need to see it again here.

At this stage we have a simple animated starfield. Now to get to work on the game.

Image 2 

Step 4 - A Game Engine 

Now we're coming to the fun stuff. We are going to need to do a few different things in our game - show a welcome screen, show a screen for when the game is over, run the game, handle input and so on.

When we're writing the code, the first thing we should try and do is make sure that we can separate the game into different 'states', and have a simple game engine that can transition from one state to another, delegate user input to a state and run the drawing and update loop. That's exactly what we're going to do.

To keep things simple, I'm making this a Space Invaders Game Engine, not a general, re-usable game engine, that's fine as we're learning, rewriting code is good and trying things in different ways is good.

So let's have a think about what a game engine needs:

  1. We should be able to have multiple states.
  2. We should be able to move from one state to another (e.g. the 'welcome' screen to the 'play' screen).
  3. A state should be able to draw itself.
  4. A state should be able to update itself (e.g. on some arbitrary tick we advance the invaders etc).
  5. A state should be able to be 'pushed' - for example a paused screen is a state on top of whatever state is below it. Unpausing simply pops the state.
With these as some initial requirements we can get to work. Create a 'spaceinvaders.js' file in the 'js' folder. Now create a 'game' class.

JavaScript
// Creates an instance of the Game class.
function Game() {
 
 // Set the initial config.
 this.config = {
    gameWidth: 400,
    gameHeight: 300,
    fps: 50
};
 
// All state is in the variables below.
this.lives = 3;
this.width = 0;
this.height = 0;
this.gameBound = {left: 0, top: 0, right: 0, bottom: 0};
 
//  The state stack.
this.stateStack = [];
 
//  Input/output
this.pressedKeys = {};
this.gameCanvas =  null;
}

This class is so far just data. We have some config (which we'll add to). The config is the game settings - how fast the invaders will move and so on. The actual state of the whole game then follows (the size of the viewport etc).

Last but not least, we have an array that we'll use as a stack for the game states, as well as an object to hold the keys that are currently pressed, and the canvas to render the game.

We can now make a function that initialises the game. All we need as input is a canvas to render to.

JavaScript
//  Initialis the Game with a canvas.
Game.prototype.initialise = function(gameCanvas) {
 
    //  Set the game canvas.
    this.gameCanvas = gameCanvas;
 
    //  Set the game width and height.
    this.width = gameCanvas.width;
    this.height = gameCanvas.height;
 
    //  Set the state game bounds.
    this.gameBounds = {
        left: gameCanvas.width / 2 - this.config.gameWidth / 2,
        right: gameCanvas.width / 2 + this.config.gameWidth / 2,
        top: gameCanvas.height / 2 - this.config.gameHeight / 2,
        bottom: gameCanvas.height / 2 + this.config.gameHeight / 2,
    };
};

In the initialise function, we store the game canvas (as we're going to want to use it later on) and set the width and height of the game. We also create the 'bounds' of the game - think of these as a rectangle that the game is played in. We set the dimensions of the game in the config, and then we plot things relative to the game bounds.

Right - our game class needs to be able to return it's state. Let's create a currentState function:

JavaScript
//  Returns the current state.
Game.prototype.currentState = function() {
    return this.stateStack.length > 0 ? this.stateStack[this.stateStack.length - 1] : null

If we have anything in the stack (which is actually an array, but arrays are flexible enough in JavaScript to use as stacks at a pinch), we return the top item (i.e. the last item in the array). Otherwise we return null.

We can get a state object, now we want to be able to move to a state.

JavaScript
Game.prototype.moveToState = function(state) {
 
    //  Are we already in a state?
    if(this.currentState()) {
 
        //  Before we pop the current state, see if the 
        //  state has a leave function. If it does we can call it.
        if(this.currentState().leave) {
           this.currentState().leave(game);
        }
        
        this.stateStack.pop();
    }
    
    //  If there's an enter function for the new state, call it.
    if(state.enter) {
        state.enter(game);
    }
 
    //  Set the current state.
    this.stateStack.push(state);
}; 

This is where things are getting smarter. This is what moving to a state does:

  1. If we're already in a state, we check if the state object has a function called 'leave'. If it does, we call it. This means our state objects can choose to be notified if they're about to exit.
  2. If we're already in a state, pop it from the state stack.
  3. If there's a function named 'enter' for the new state, call it. This means states can choose to be notified if they're about to be entered.
  4. Now we push our new state onto the stack.
So the take-away here is this - moveToState replaces the top of the state stack with a new state - and states can know when they're entering or leaving. 

We can use exactly the same principals to quickly wire up pushState and popState functions:

JavaScript
Game.prototype.pushState = function(state) {
 
    //  If there's an enter function for the new state, call it.
    if(state.enter) {
        state.enter(game);
    }
    //  Set the current state.
    this.stateStack.push(state);
};
 
Game.prototype.popState = function() {
 
    //  Leave and pop the state.
    if(this.currentState()) {
        if(this.currentState().leave) {
            this.currentState().leave(game);
        }
 
        //  Set the current state.
        this.stateStack.pop();
    }
}; 

These functions work with the same principals as the moveToState function.

For our game to do anything, we'll need some kind of loop that's running, telling the active state that it needs to draw and so on. So let's put together a global gameLoop function that does just this:

JavaScript
// The main loop.
function gameLoop(game) {
    var currentState = game.currentState();
    if(currentState) {
 
        //  Delta t is the time to update/draw.
        var dt = 1 / game.config.fps;
 
        //  Get the drawing context.
        var ctx = game.gameCanvas.getContext("2d");
		
        //  Update if we have an update function. Also draw
        //  if we have a draw function.
        if(currentState.update) {
            currentState.update(game, dt);
        }
        if(currentState.draw) {
            currentState.draw(game, dt, ctx);
        }
    }
} 

This function is key.

  1. First, get the current game state.
  2. Now work out how much time is in one 'tick' of the loop. This is one over the FPS - if we loop ten times per second, each tick is 100 milliseconds.
  3. Get a drawing context from the canvas (this is explained in Part 1).
  4. If there is a function called 'update' in the state, call it, passing the game object and the amount of time that's passed.
  5. If there's a function called 'draw' in the state, call it, passing the game object, the amount of time that's passed and the drawing context.
Now we just need to call this function on a timer. We can create a 'start' method in the Game for that:

//  Start the Game.
Game.prototype.start = function() {
 
    //  Move into the 'welcome' state.
    this.moveToState(new WelcomeState());
 
    //  Set the game variables.
    this.lives = 3;
    this.config.debugMode = /debug=true/.test(window.location.href);
 
    //  Start the game loop.
    var game = this;
    this.intervalId = setInterval(function () { gameLoop(game);}, 1000 / this.config.fps);
 
}; 

By now with what we know about states, we can see how this goes. We start the game by moving into a new instance of the 'WelcomeState' class (which we'll create next!), we set the number of lives to three, then set a timer to call the gameLoop based on the FPS config setting. Let's see how the Welcome State looks.

Step 5 - The Welcome State

The first state is one of the easiest, because all it will do is show the title of the game. We start by creating a class for the state:

JavaScript
function WelcomeState() {
 
} 

The Welcome State is so simple it doesn't even have any data members. Now we can create a draw function:

JavaScript
WelcomeState.prototype.draw = function(game, dt, ctx) {
 
    //  Clear the background.
    ctx.clearRect(0, 0, game.width, game.height);
 
    ctx.font="30px Arial";
    ctx.fillStyle = '#ffffff';
    ctx.textBaseline="center";
    ctx.textAlign="center";
    ctx.fillText("Space Invaders", game.width / 2, game.height/2 - 40);
    ctx.font="16px Arial";
 
    ctx.fillText("Press 'Space' to start.", game.width / 2, game.height/2);
}; 

Again, we can look back to Part 1 for more detail on the canvas context, but there's nothing complicated here - we clear the drawing surface, write out "Space Invaders" and ask the user to press the spacebar.

JavaScript
WelcomeState.prototype.keyDown = function(game, keyCode) {
    if(keyCode == 32) /*space*/ {
        //  Space starts the game.
        game.moveToState(new LevelIntroState(game.level));
    }
};

Now we can create a keyDown function for the state - if the keycode is space, we move to the LevelIntroState. 

The only problem here is that keyDown is never called, because it's not in the game engine. That's something we can add now:

JavaScript
 //  Inform the game a key is down.
Game.prototype.keyDown = function(keyCode) {
    this.pressedKeys[keyCode] = true;
    //  Delegate to the current state too.
    if(this.currentState() && this.currentState().keyDown) {
        this.currentState().keyDown(this, keyCode);
    }
};
 
//  Inform the game a key is up.
Game.prototype.keyUp = function(keyCode) {
    delete this.pressedKeys[keyCode];
    //  Delegate to the current state too.
    if(this.currentState() && this.currentState().keyUp) {
        this.currentState().keyUp(this, keyCode);
    }
};

The GameEngine can be notified that a key has been pressed or released. Once that happens, we see if the current state has a keyDown or keyUp function - if so we call it. We also keep track of each key that is pressed in an object, so that if the user pressed multiple keys, states can look at the game.pressedKeys object and see what is pressed.

JavaScript Tip: The 'delete' keyword can be used to remove a property from an object.

We've created the welcome state and we've got a game start function, so let's go back to the index and actually add the game.

Here's the HTML (what's new is in bold):

HTML
<div id="starfield"></div>
<div id="gamecontainer">
    <canvas id="gameCanvas"></canvas>
</div>  

 Here's the CSS:

CSS
#gamecontainer {
    width: 800px;
    margin-left: auto;
    margin-right: auto;
} 

Lastly, here's the JavaScript:

JavaScript
//  Create the starfield.
var container = document.getElementById('starfield');
var starfield = new Starfield();
starfield.initialise(container);
starfield.start();
 
//  Setup the canvas.
var canvas = document.getElementById("gameCanvas");
canvas.width = 800;
canvas.height = 600;
 
//  Create the game.
var game = new Game();
 
//  Initialise it with the game canvas.
game.initialise(canvas);
 
//  Start the game.
game.start();
 
//  Listen for keyboard events.
window.addEventListener("keydown", function keydown(e) {
    var keycode = e.which || window.event.keycode;
    //  Supress further processing of left/right/space (37/29/32)
    if(keycode == 37 || keycode == 39 || keycode == 32) {
        e.preventDefault();
    }
    game.keyDown(keycode);
});
window.addEventListener("keyup", function keydown(e) {
    var keycode = e.which || window.event.keycode;
    game.keyUp(keycode);
}); 

This is not as complex as it might seem. We create the Game object, initialise it with a canvas, start the game and tell it when keys are pressed. We don't let the window process space, left or right otherwise it tries to move the viewport around, and we don't want that.

Our game now has a welcome screen - we're getting there!

Image 3

Step 6 - The Level Intro  

The Welcome State was trivial - it didn't need the update function or enter or leave. Now we'll create a state that we use to show a three second countdown before the level starts. Again, we can create a class for the state:

JavaScript
/*  
    Level Intro State
 
    The Level Intro state shows a 'Level X' message and
    a countdown for the level.
*/
function LevelIntroState(level) {
    this.level = level;
    this.countdownMessage = "3";
} 

This state actually has state of its own, it knows what level it's counting down for and the message it's showing. We can create a draw function next:

JavaScript
LevelIntroState.prototype.draw = function(game, dt, ctx) {
 
    //  Clear the background.
    ctx.clearRect(0, 0, game.width, game.height);
 
    ctx.font="36px Arial";
    ctx.fillStyle = '#ffffff';
    ctx.textBaseline="middle"; 
    ctx.textAlign="center"; 
    ctx.fillText("Level " + this.level, game.width / 2, game.height/2);
    ctx.font="24px Arial";
    ctx.fillText("Ready in " + this.countdownMessage, game.width / 2, game.height/2 + 36);
}; 

This is trivial - we just show a message saying 'Ready in X' with X being the countdown message.

Now we can create an update function.

Game Tip: What's the update function? Update is called in the loop like Draw, but it doesn't draw anything - it updates the state of the game. We could do this in draw, but draw should just faithfully render the current state untouched. Why is this? Well we could actually call draw and update at different frequencies - for example if drawing is expensive we can call it ten times less often than update - but still have the state of the system be updated at more regular intervals.

Java
LevelIntroState.prototype.update = function(game, dt) {
 
    //  Update the countdown.
    if(this.countdown === undefined) {
        this.countdown = 3; // countdown from 3 secs
    }
    this.countdown -= dt;
 
    if(this.countdown < 2) { 
        this.countdownMessage = "2"; 
    }
    if(this.countdown < 1) { 
        this.countdownMessage = "1"; 
    } 
    if(this.countdown <= 0) {
        //  Move to the next level, popping this state.
        game.moveToState(new PlayState(game.config, this.level));
    }
 
}; 

If we don't have a countdown number, set it to three (seconds). Now remove the time that's passed (dt is in seconds). Every time the countdown number gets below 2 or 1, we can update the countdown message. When it gets to zero, we can move into the Play State - the actual game, passing the level we've been told we're counting down for.

That's it - the level intro state is done. As we already transition to it when space is pressed on the welcome state, we can run up the page, press space and see it working:

Image 4

Step 7 - The Play State

This is the big one. The play state knows what level it is when it is created, and that's about it. We've got to make sure that we create the invaders, position them, create the ship, position it, respond to mouse movement, respond to time passing, handle bombs from the invaders, rockets from the ship and the score. But hey - this is JavaScript and knocking stuff together quickly is one of the things this language is good at.

First - the lie. When we made the game state I didn't show a bunch of the properties in the Game config. These are used to adjust how fast things move, or accelerate, or change and so on. It wasn't needed at the time but now we'd better take a look, because from the Play State constructor we can see we take a copy of the Game config cause we use so much of it:

JavaScript
//  Create a PlayState with the game config and the level you are on.
function PlayState(config, level) {
    this.config = config;
    this.level = level;
 
    //  Game state.
    this.invaderCurrentVelocity =  10;
    this.invaderCurrentDropDistance =  0;
    this.invadersAreDropping =  false;
    this.lastRocketTime = null;
 
    //  Game entities.
    this.ship = null;
    this.invaders = [];
    this.rockets = [];
    this.bombs = [];
}

The actual constructor isn't too bad - we take a copy of the game config reference, set the current velocity of the invaders, the current drop distance (that's how far they've moved downwards when they hit the edge of the screen), a flag for whether they're dropping, a time for when the last rocket was fired (so we can limit the number of rockets per second) and then create a ship, set of invaders, set of rockets and set of bombs.

Just as as reference, here's the actual game config - we'll see what most of it is for as we go through, but essentially we can adjust aspects of how the game runs with them: 

JavaScript
//  Creates an instance of the Game class.
function Game() {
 
    //  Set the initial config.
    this.config = {
        bombRate: 0.05,
        bombMinVelocity: 50,
        bombMaxVelocity: 50,
        invaderInitialVelocity: 25,
        invaderAcceleration: 0,
        invaderDropDistance: 20,
        rocketVelocity: 120,
        rocketMaxFireRate: 2,
        gameWidth: 400,
        gameHeight: 300,
        fps: 50,
        debugMode: false,
        invaderRanks: 5,
        invaderFiles: 10,
        shipSpeed: 120,
        levelDifficultyMultiplier: 0.2,
        pointsPerInvader: 5
    };
} 

We're going to be dealing with a ship, rockets, bombs and invaders. Coming from a static background this makes me want types for them, so that's what I'll make:

JavaScript
/*
 
  The ship has a position and that's about it.
 
*/
function Ship(x, y) {
    this.x = x;
    this.y = y;
    this.width = 20;
    this.height = 16;
}
 
/*
    Fired by the ship, they've got a position and velocity.
 
    */
function Rocket(x, y, velocity) {
    this.x = x;
    this.y = y;
    this.velocity = velocity;
}
 
/*
    Dropped by invaders, they've got position and velocity.
*/
function Bomb(x, y, velocity) {
    this.x = x;
    this.y = y;
    this.velocity = velocity;
}
 
/*
    Invaders have position, type, rank/file and that's about it. 
*/
 
function Invader(x, y, rank, file, type) {
    this.x = x;
    this.y = y;
    this.rank = rank;
    this.file = file;
    this.type = type;
    this.width = 18;
    this.height = 14;
} 

So each entity has a position, and the larger ones have a size. The invader also knows its rank and file (where it is in the grid).

Now we'll go into the first big function of the state, enter. This is called when we start each level:

JavaScript
PlayState.prototype.enter = function(game) {
 
    //  Create the ship.
    this.ship = new Ship(game.width / 2, game.gameBounds.bottom); 

We create the ship at the bottom middle of the game bounds.

The code below looks complex but isn't too much - it makes sure things like the invader speed and bomb speed gets a bit faster each level, but that's it. 

JavaScript
//  Set the ship speed for this level, as well as invader params.
var levelMultiplier = this.level * this.config.levelDifficultyMultiplier;
this.shipSpeed = this.config.shipSpeed;
this.invaderInitialVelocity = this.config.invaderInitialVelocity + (levelMultiplier * this.config.invaderInitialVelocity);
this.bombRate = this.config.bombRate + (levelMultiplier * this.config.bombRate);
this.bombMinVelocity = this.config.bombMinVelocity + (levelMultiplier * this.config.bombMinVelocity);
this.bombMaxVelocity = this.config.bombMaxVelocity + (levelMultiplier * this.config.bombMaxVelocity);

The game is actually very highly configurable because of code like this - it had to be so I could find sensible values and multipliers to get a good feel and increase in difficulty as the levels progress.

JavaScript
//  Create the invaders.
    var ranks = this.config.invaderRanks;
    var files = this.config.invaderFiles;
    var invaders = [];
    for(var rank = 0; rank < ranks; rank++){
        for(var file = 0; file < files; file++) {
            invaders.push(new Invader(
                (game.width / 2) + ((files/2 - file) * 200 / files),
                (game.gameBounds.top + rank * 20),
                rank, file, 'Invader'));
        }
    }
    this.invaders = invaders;
    this.invaderCurrentVelocity = this.invaderInitialVelocity;
    this.invaderVelocity = {x: -this.invaderInitialVelocity, y:0};
    this.invaderNextVelocity = null;
};

We finish the enter function by creating an invader at each rank and file. There's some arithmatic to position and space them nicely. We also store the current velocity of the invaders. There's a 'next velocity' too - we use that when we move them downwards and need to decide where to move them afterwards.

Now for update. This is where all of the state for the state is updated.

JavaScript
 PlayState.prototype.update = function(game, dt) {
    
    //  If the left or right arrow keys are pressed, move
    //  the ship. Check this on ticks rather than via a keydown
    //  event for smooth movement, otherwise the ship would move
    //  more like a text editor caret.
    if(game.pressedKeys[37]) {
        this.ship.x -= this.shipSpeed * dt;
    }
    if(game.pressedKeys[39]) {
        this.ship.x += this.shipSpeed * dt;
    }
    if(game.pressedKeys[32]) {
        this.fireRocket();
    }
 
    //  Keep the ship in bounds.
    if(this.ship.x < game.gameBounds.left) {
        this.ship.x = game.gameBounds.left;
    }
    if(this.ship.x > game.gameBounds.right) {
        this.ship.x = game.gameBounds.right;
    }

The first thing we do is see if the left or right keys are pressed. If so, we nudge the ship. If space is pressed, we call the fireRocket function, which we'll come to later. We also make sure we don't ever move the ship past the game bounds. 

Now we can move each bomb downwards (unless it has gone out of bounds in which case we remove it). We can also move each rocket upwards (again, unless it's out of bounds, when we can remove it).

JavaScript
//  Move each bomb.
    for(var i=0; i<this.bombs.length; i++) {
        var bomb = this.bombs[i];
        bomb.y += dt * bomb.velocity;
 
        //  If the bomb has gone off the screen remove it.
        if(bomb.y > this.height) {
            this.bombs.splice(i--, 1);
        }
    }
 
    //  Move each rocket.
    for(i=0; i<this.rockets.length; i++) {
        var rocket = this.rockets[i];
        rocket.y -= dt * rocket.velocity;
 
        //  If the rocket has gone off the screen remove it.
        if(rocket.y < 0) {
            this.rockets.splice(i--, 1);
        }
    }

So we've handled the ship movement and the bomb and rocket movement.

Now for the really ugly part - moving the invaders.

JavaScript
 //  Move the invaders.
    var hitLeft = false, hitRight = false, hitBottom = false;
    for(i=0; i<this.invaders.length; i++) {
        var invader = this.invaders[i];
        var newx = invader.x + this.invaderVelocity.x * dt;
        var newy = invader.y + this.invaderVelocity.y * dt;
        if(hitLeft === false && newx < game.gameBounds.left) {
            hitLeft = true;
        }
        else if(hitRight === false && newx > game.gameBounds.right) {
            hitRight = true;
        }
        else if(hitBottom === false && newy > game.gameBounds.bottom) {
            hitBottom = true;
        }
 
        if(!hitLeft && !hitRight && !hitBottom) {
            invader.x = newx;
            invader.y = newy;
        }
    }
 
    //  Update invader velocities.
    if(this.invadersAreDropping) {
        this.invaderCurrentDropDistance += this.invaderVelocity.y * dt;
        if(this.invaderCurrentDropDistance >= this.config.invaderDropDistance) {
            this.invadersAreDropping = false;
            this.invaderVelocity = this.invaderNextVelocity;
            this.invaderCurrentDropDistance = 0;
        }
    }
    //  If we've hit the left, move down then right.
    if(hitLeft) {
        this.invaderCurrentVelocity += this.config.invaderAcceleration;
        this.invaderVelocity = {x: 0, y:this.invaderCurrentVelocity };
        this.invadersAreDropping = true;
        this.invaderNextVelocity = {x: this.invaderCurrentVelocity , y:0};
    }
    //  If we've hit the right, move down then left.
    if(hitRight) {
        this.invaderCurrentVelocity += this.config.invaderAcceleration;
        this.invaderVelocity = {x: 0, y:this.invaderCurrentVelocity };
        this.invadersAreDropping = true;
        this.invaderNextVelocity = {x: -this.invaderCurrentVelocity , y:0};
    }
    //  If we've hit the bottom, it's game over.
    if(hitBottom) {
        this.lives = 0;
    } 

I apologise! This is really rather ugly, and I'm sure it could be done better. We move each invader by the current velocity, checking to see if we've hit the left, right or bottom bounds. If it's left or right, we start moving the invaders down and tell them where to move after that. If they hit the bottom we're dead. This looks complex but isn't really, it's just fiddly. But I'm sure it could be done more cleanly.

Now we can do some good old fashioned collision detection:

JavaScript
//  Check for rocket/invader collisions.
    for(i=0; i<this.invaders.length; i++) {
        var invader = this.invaders[i];
        var bang = false;
 
        for(var j=0; j<this.rockets.length; j++){
            var rocket = this.rockets[j];
 
            if(rocket.x >= (invader.x - invader.width/2) && rocket.x <= (invader.x + invader.width/2) &&
                rocket.y >= (invader.y - invader.height/2) && rocket.y <= (invader.y + invader.height/2)) {
                
                //  Remove the rocket, set 'bang' so we don't process
                //  this rocket again.
                this.rockets.splice(j--, 1);
                bang = true;
                game.score += this.config.pointsPerInvader;
                break;
            }
        }
        if(bang) {
            this.invaders.splice(i--, 1);
        }
    } 

We go through every invader and see if it's been hit by a rocket. If so, we remove the rocket and invader then update the score.

As we've been a bit cruel to the invaders there, we'll next find each front rank invader and give it a chance to drop a bomb:

JavaScript
//  Find all of the front rank invaders.
    var frontRankInvaders = {};
    for(var i=0; i<this.invaders.length; i++) {
        var invader = this.invaders[i];
        //  If we have no invader for game file, or the invader
        //  for game file is futher behind, set the front
        //  rank invader to game one.
        if(!frontRankInvaders[invader.file] || frontRankInvaders[invader.file].rank < invader.rank) {
            frontRankInvaders[invader.file] = invader;
        }
    }
 
    //  Give each front rank invader a chance to drop a bomb.
    for(var i=0; i<this.config.invaderFiles; i++) {
        var invader = frontRankInvaders[i];
        if(!invader) continue;
        var chance = this.bombRate * dt;
        if(chance > Math.random()) {
            //  Fire!
            this.bombs.push(new Bomb(invader.x, invader.y + invader.height / 2,
                this.bombMinVelocity + Math.random()*(this.bombMaxVelocity - this.bombMinVelocity)));
        }
    } 

Again, the game settings come in here as the rate of bomb drops increases with each level.

We've checked for rockets and invaders, now we can check for bombs and the ship:

//  Check for bomb/ship collisions.
    for(var i=0; i<this.bombs.length; i++) {
        var bomb = this.bombs[i];
        if(bomb.x >= (this.ship.x - this.ship.width/2) && bomb.x <= (this.ship.x + this.ship.width/2) &&
                bomb.y >= (this.ship.y - this.ship.height/2) && bomb.y <= (this.ship.y + this.ship.height/2)) {
            this.bombs.splice(i--, 1);
            game.lives--;
        }
                
    }

 And while we're at it - let's make sure that if an invader touches the ship it's game over too:

JavaScript
//  Check for invader/ship collisions.
    for(var i=0; i<this.invaders.length; i++) {
        var invader = this.invaders[i];
        if((invader.x + invader.width/2) > (this.ship.x - this.ship.width/2) && 
            (invader.x - invader.width/2) < (this.ship.x + this.ship.width/2) &&
            (invader.y + invader.height/2) > (this.ship.y - this.ship.height/2) &&
            (invader.y - invader.height/2) < (this.ship.y + this.ship.height/2)) {
            //  Dead by collision!
            game.lives = 0;
            game.sounds.playSound('explosion');
        }
    }

Programming Tip: In this code I'm repeatedly looping through sets. It could be improved greatly by jamming more logic into less loops (this is called loop jamming) but it would make the tutorial harder to read. But always be suspicious of code like the above - with large numbers of invaders or bombs we're looping too many times. Always keep an eye out for redundant looping. 

We've just about finished, the last thing is to see if we've run out of lives and end the game if so, or see if we've run out of invaders and countdown the next level if so:

JavaScript
//  Check for failure
    if(game.lives <= 0) {
        game.moveToState(new GameOverState());
    }
 
    //  Check for victory
    if(this.invaders.length === 0) {
        game.score += this.level * 50;
        game.level += 1;
        game.moveToState(new LevelIntroState(game.level));
    } 
}; 

That's our update function. I promised I'd show the 'fireRocket' function too:

JavaScript
 PlayState.prototype.fireRocket = function() {
    //  If we have no last rocket time, or the last rocket time 
    //  is older than the max rocket rate, we can fire.
    if(this.lastRocketTime === null || ((new Date()).valueOf() - this.lastRocketTime) > (1000 / this.config.rocketMaxFireRate))
    {   
        //  Add a rocket.
        this.rockets.push(new Rocket(this.ship.x, this.ship.y - 12, this.config.rocketVelocity));
        this.lastRocketTime = (new Date()).valueOf();
    }
};

Surprisingly clunky - we need to make sure that the user can't just hold down the space key and fire as many rockets as they want, so we limit the fire rate in this function.

OK so we have Space Invaders, but only as a system in memory, now we need to render it. As we're just looping through the game entities and drawing primitives, I'm not going through this blow-by-blow:

JavaScript
PlayState.prototype.draw = function(game, dt, ctx) {
 
    //  Clear the background.
    ctx.clearRect(0, 0, game.width, game.height);
    
    //  Draw ship.
    ctx.fillStyle = '#999999';
    ctx.fillRect(this.ship.x - (this.ship.width / 2), this.ship.y - (this.ship.height / 2), this.ship.width, this.ship.height);
 
    //  Draw invaders.
    ctx.fillStyle = '#006600';
    for(var i=0; i<this.invaders.length; i++) {
        var invader = this.invaders[i];
        ctx.fillRect(invader.x - invader.width/2, invader.y - invader.height/2, invader.width, invader.height);
    }
 
    //  Draw bombs.
    ctx.fillStyle = '#ff5555';
    for(var i=0; i<this.bombs.length; i++) {
        var bomb = this.bombs[i];
        ctx.fillRect(bomb.x - 2, bomb.y - 2, 4, 4);
    }
 
    //  Draw rockets.
    ctx.fillStyle = '#ff0000';
    for(var i=0; i<this.rockets.length; i++) {
        var rocket = this.rockets[i];
        ctx.fillRect(rocket.x, rocket.y - 2, 1, 4);
    }
 
}; 

We now have the core part of the game working;  

Image 5

Step 8 - The Other Stuff  

There are a few bits and pieces that we don't need to go into. There's a game over state and a pause state, there's also some code to play sounds, but we've really seen the core of what's going on - any more would certainly be overkill. 

The code is on GitHub - fork it, play with it and have fun. In the next tutorial we'll work with a canvas but also introduce some more HTML elements and use our first framework - AngularJS.

As always, questions and comments are most welcome! 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United Kingdom United Kingdom
Follow my blog at www.dwmkerr.com and find out about my charity at www.childrenshomesnepal.org.

Comments and Discussions

 
QuestionQuestion (Problem) Pin
KryDizzel z10-Nov-20 2:24
KryDizzel z10-Nov-20 2:24 
QuestionMute By Default Pin
Anjan Bhattrai27-Sep-20 18:41
Anjan Bhattrai27-Sep-20 18:41 
Questionadding sprites and max level?? Pin
Member 1269669425-Aug-16 23:44
Member 1269669425-Aug-16 23:44 
AnswerRe: adding sprites and max level?? Pin
Member 1271317031-Aug-16 4:29
Member 1271317031-Aug-16 4:29 
GeneralMy vote of 5 Pin
Alexandra Christina16-Jul-14 11:28
Alexandra Christina16-Jul-14 11:28 
QuestiondebugMode property Pin
Member 109173811-Jul-14 8:16
Member 109173811-Jul-14 8:16 
AnswerRe: debugMode property Pin
Dave Kerr9-Jul-14 9:33
mentorDave Kerr9-Jul-14 9:33 
GeneralMy 5 Pin
Arnaldo Pinheiro22-Jan-14 6:29
Arnaldo Pinheiro22-Jan-14 6:29 
GeneralRe: My 5 Pin
Dave Kerr25-Jan-14 5:04
mentorDave Kerr25-Jan-14 5:04 
QuestionWhy style 'gameCanvas'? Pin
Sandeep Tamhankar19-Jan-14 15:41
Sandeep Tamhankar19-Jan-14 15:41 
AnswerRe: Why style 'gameCanvas'? Pin
Dave Kerr31-Jan-14 22:21
mentorDave Kerr31-Jan-14 22:21 
BugGood Walkthrough... But I Have Some Recommendations Pin
Sandeep Tamhankar19-Jan-14 15:37
Sandeep Tamhankar19-Jan-14 15:37 
QuestionRe: Good Walkthrough... But I Have Some Recommendations Pin
Aaron Jewell22-Jan-14 1:26
Aaron Jewell22-Jan-14 1:26 
AnswerRe: Good Walkthrough... But I Have Some Recommendations Pin
Dave Kerr31-Jan-14 22:20
mentorDave Kerr31-Jan-14 22:20 
GeneralRe: Good Walkthrough... But I Have Some Recommendations Pin
Dave Kerr31-Jan-14 22:19
mentorDave Kerr31-Jan-14 22:19 
QuestionMy Vote Of 5 Pin
Richard Waddell17-Jan-14 18:23
Richard Waddell17-Jan-14 18:23 
AnswerRe: My Vote Of 5 Pin
Dave Kerr26-Jan-14 4:25
mentorDave Kerr26-Jan-14 4:25 
QuestionProblem when using IE11 Pin
vkurin9-Jan-14 11:55
vkurin9-Jan-14 11:55 
QuestionFF vs IE Pin
ramarch21-Dec-13 1:24
ramarch21-Dec-13 1:24 
AnswerRe: FF vs IE Pin
Dave Kerr22-Dec-13 22:23
mentorDave Kerr22-Dec-13 22:23 
Questionunable to run spaceinvaders Pin
ramarch20-Dec-13 20:15
ramarch20-Dec-13 20:15 
AnswerRe: unable to run spaceinvaders Pin
Dave Kerr20-Dec-13 21:43
mentorDave Kerr20-Dec-13 21:43 
GeneralRe: unable to run spaceinvaders Pin
ramarch21-Dec-13 1:38
ramarch21-Dec-13 1:38 
GeneralRe: unable to run spaceinvaders Pin
Dave Kerr22-Dec-13 22:19
mentorDave Kerr22-Dec-13 22:19 
Suggestiontypo Pin
_Noctis_15-Dec-13 19:31
professional_Noctis_15-Dec-13 19:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.