JavaScript Game Tutorial - Space Invaders Part 1 - Introduction

This is the first part in a series about creating a Space Invaders clone in JavaScript. It is highly recommended to start from the beginning as each part builds directly upon the previous.

Space Invaders Tutorial Series
Part 0 Beginner JavaScript Game Tutorial For Professional Use
Part 1 Math Classes and Game Entity Structure
Part 2 User Input
Part 3 Enemy Behavior
Part 4 Collision Detection and Projectiles
Part 5 Sprites and User Interface
Part 6 Optimization
Part 7 3D Renderer
Part 8 Events and Audio

Introduction

In my Beginner JavaScript Game Tutorial For Professional Use, we wrote the most basic game engine possible using the proper structure not often seen in tutorials. Now we are going to create a Space Invaders clone to see how this foundation easily builds out into a full game. This will all be written from scratch in raw JavaScript, no frameworks or libraries needed. The finished product can be seen in the Arcade section along with other games all built in this same manner.


Game Design

Making a game, even a simple one like Space Invaders, is still relatively complicated without a plan. So let's describe the game action and break it all down in pieces to figure out how to implement the whole thing.

The enemies will come down from the top of the screen in a grid and move left and right until they hit either edge at which point they will move down and move in the opposite direction away from the edge of the screen. The entire grid moves as a unit, so if one enemy hits an edge, they all move down and move in the other direction. The enemies on the bottom of each column occasionally shoot a projectile down towards the player. Each enemy has a rank which has its own graphic and points awarded when killed. The enemies closest to the player have a low rank and the ones on top have a high rank.

The player will be at the bottom of the screen and limited to moving left and right. They can shoot a projectile upwards, being limited to one shot on the screen at a time. If a player shot hits an enemy, the enemy is removed from the grid with a little explosion and the player awarded some points. All the remaining enemies speed up a little bit. When all the enemies have been killed, a new grid of enemies is created, now faster and more likely to shoot, and the player is awarded another ship.

When an enemy shot hits the player, the ship is destroyed. If there are remaining ships available, play continues with a new ship. Once all the extra ships have been destroyed, the game ends and the top 10 high scores are displayed. The high scores will be saved in the browser so the player can come back and beat their own high scores from previous sessions.

Over the course of this series, we will develop all of these features piece by piece.


Goals

The goals for this part are:

  • Add needed math functions, Vector2d, Rectangle and Random number generation.
  • Create an Entity class structure with Speed and Direction properties for movement.
  • Update the Renderer, Physics and Game objects to use the new Entity properties.


HTML Canvas

The grid of enemies is going to be 10 columns of 5 rows. We need a bigger canvas than we've been using so far. Let's update the HTML to this:

<canvas id="game-layer" width="300" height="180" style="background:black"></canvas>


2D Vector Math

We will use vectors all over the place for movement and position of game entities in the game world. This isn't a complete implementation of all vector functions, but enough for this game.

Vector2d
Function Return Type Description
vectorAdd( v1, v2 ) Vector2d adds vectors v1 and v2 together
vectorSubtract( v1, v2 ) Vector2d subtract vector v2 from v1
vectorScalarMultiply( v1, s ) Vector2d multiplies the components of v1 by the scalar number s
vectorLength( v ) Number returns the length of vector v
vectorNormalize( v ) Vector2d returns a normalized vector v (a vector with length == 1)
//
// Vector2d Object
//
var Vector2d = function (x, y) {
    this.x = x;
    this.y = y;
};

function vectorAdd(v1, v2) {
    return new Vector2d(v1.x + v2.x, v1.y + v2.y);
}

function vectorSubtract(v1, v2) {
    return new Vector2d(v1.x - v2.x, v1.y - v2.y);
}

function vectorScalarMultiply(v1, s) {
    return new Vector2d(v1.x * s, v1.y * s);
}

function vectorLength(v) {
    return Math.sqrt(v.x * v.x + v.y * v.y);
}

function vectorNormalize(v) {
    var reciprocal = 1.0 / (vectorLength(v) + 1.0e-037); // Prevent division by zero.
    return vectorScalarMultiply(v, reciprocal);
}

I prefer using functions instead of instance methods for math functions, because it is clearer that the vector itself isn't being modified.

var v3 = v1.add(v2);        // Unclear if v1 is changed
var v3 = vectorAdd(v1, v2); // Obvious v1 is not changed


Rectangle

We are going to need rectangles to check for collisions between entities as well as defining the boundary of the game area. This is also how we will keep track of the size of the entire group of enemies to make them move as a group.

Rectangle
Function Return Type Description
left() Number returns the left edge
right() Number returns the right edge
top() Number returns the top edge
bottom() Number returns the bottom edge
intersects( r2 ) Boolean returns true if the rectangle r2 intersects with this rectangle
rectUnion( r1, r2 ) Number returns a rectangle that contains both rectangles r1 and r2
//
// Rectangle Object
//
function Rectangle (x, y, width, height) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
}

Rectangle.prototype.left = function () {
    return this.x;
};

Rectangle.prototype.right = function () {
    return this.x + this.width;
};

Rectangle.prototype.top = function () {
    return this.y;
};

Rectangle.prototype.bottom = function () {
    return this.y + this.height;
};

Rectangle.prototype.intersects = function (r2) {
    return this.right() >= r2.left() && this.left() <= r2.right() &&
           this.top() <= r2.bottom() && this.bottom() >= r2.top();
};

function rectUnion(r1, r2) {
    var x, y, width, height;

    if( r1 === undefined ) {
        return r2;
    }
    if( r2 === undefined ) {
        return r1;
    }

    x = Math.min( r1.x, r2.x );
    y = Math.min( r1.y, r2.y );
    width = Math.max( r1.right(), r2.right() ) - Math.min( r1.left(), r2.left() );
    height = Math.max( r1.bottom(), r2.bottom() ) - Math.min( r1.top(), r2.top() );

    return new Rectangle(x, y, width, height);
}


Random Numbers

This is a simple way to get random number integers.

Function Return Type Description
randomInt( max ) Number returns an integer in the range [0, max)
function randomInt(max) {
    return Math.floor(Math.random() * Math.floor(max));
}

For example, randomInt(100) returns an integer value between 0 and 99.


Entity

Let's give proper structure to the Game Entities by creating an Entity root class with the following properties:

Entity
Property Type Description
position Vector2d position (in world coordinates) of the center of the entity
direction Vector2d direction of motion
speed Number speed of motion
width Number width of the entity
height Number height of the entity
hp Number hit points
Function Return Type Description
collisionRect() Rectangle collision rectangle centered at position (in world coordinates)
update(dt) none dt (delta time) is the time difference since the last frame

I am using speed and direction instead of velocity because we are going to be frequently changing the speed of the enemies and keeping direction and speed separate makes that very simple.

//
// Entity Object
//
function Entity(position, speed, direction) {
    this.position = position;
    this.speed = speed;
    this.direction = direction;
    this.time = 0;
    this.width = 5;
    this.height = 5;
    this.hp = 1;
}

Entity.prototype.update = function (dt) {
    this.time += dt;
};

Entity.prototype.collisionRect = function () {
    return new Rectangle(this.position.x - this.width/2,
                         this.position.y - this.height/2,
                         this.width,
                         this.height);
};


Enemy

The Enemy class needs a rank property to distinguish different types of enemies. We will have 5 ranks of enemies, corresponding to the 5 rows of enemies on the screen. Each will have their own graphic and the points earned will be based on the rank.

Enemy
Property Type Description
rank Number higher rank is worth more points
//
// Enemy Object
//
function Enemy(position, speed, direction, rank) {
    Entity.call(this, position, speed, direction);

    this.width = 13;
    this.height = 10;
    this.rank = rank;
}
Enemy.prototype = Object.create(Entity.prototype);

Enemy.prototype.update = function (dt) {
    Entity.prototype.update.call(this, dt);
    if( this.collisionRect().top() <= 0 ||
        this.collisionRect().bottom() >= game.gameFieldRect().bottom() ) {
        this.direction.y *= -1;
    }
};


Player

The Player class doesn't change too much from our base version. We will add quite a bit to the Player when we deal with user input.

//
// Player Object
//
function Player(position, speed, direction) {
    Entity.call(this, position, speed, direction);

    this.width = 20;
    this.height = 10;
}
Player.prototype = Object.create(Entity.prototype);

Player.prototype.update = function (dt) {
    Entity.prototype.update.call(this, dt);
    if( this.collisionRect().top() <= 0 ||
        this.collisionRect().bottom() >= game.gameFieldRect().bottom() ) {
        this.direction.y *= -1;
    }
};


Renderer

The Renderer has to be updated to accurately display the new Entity properties. The position is now the center of the Entity, so we update the drawing code appropriately. The Enemy rank will be shown by changing colors. Eventually we will be drawing sprites, but the code will be very similar to what is below.

Renderer
Property Type Description
_canvas Canvas the drawing canvas from the DOM
_context CanvasRenderingContext2D the 2D context from the canvas
_enemyColors Array array of colors corresponding to the enemy ranks
Function Return Type Description
_drawRectangle( color, entity ) none draw a rectangle with entity.width and height, centered at entity.position, filled with the color
_render( dt ) none render the scene
//
// Renderer Object
//
var renderer = (function () {
    var _canvas = document.getElementById("game-layer"),
        _context = _canvas.getContext("2d"),
        _enemyColors = ["rgb(150, 7, 7)",
                        "rgb(150, 89, 7)",
                        "rgb(56, 150, 7)",
                        "rgb(7, 150, 122)",
                        "rgb(46, 7, 150)"];


    function _drawRectangle(color, entity) {
        _context.fillStyle = color;
        _context.fillRect(entity.position.x-entity.width/2,
                          entity.position.y-entity.height/2,
                          entity.width,
                          entity.height);
    }

    function _render(dt) {
        _context.fillStyle = "black";
        _context.fillRect(0, 0, _canvas.width, _canvas.height);

        var i,
            entity,
            entities = game.entities();

        for( i=entities.length-1; i>=0; i-- ) {
            entity = entities[i];

            if( entity instanceof Enemy ) {
                _drawRectangle(_enemyColors[entity.rank], entity);
            }
            else if( entity instanceof Player ) {
                _drawRectangle("rgb(255, 255, 0)", entity);
            }
        }
    }

    return {
        render: _render
    };
})();


Physics

The physics is the main reason for using vector math and it makes the code very clean. Later, we will add the collision detection here as well.

//
// Physics Object
//
var physics = (function () {

    function _update(dt) {
        var i,
            e,
            velocity,
            entities = game.entities();

        for( i=entities.length-1; i>=0; i-- ) {
            e = entities[i];
            velocity = vectorScalarMultiply( e.direction, e.speed );

            e.position = vectorAdd( e.position, vectorScalarMultiply( velocity, dt ) );
        }
    }

    return {
        update: _update
    };
})();


Game

The Game object acts, in part, as a central data repository for the other objects. All other objects query the Game object for information they need. Already the Renderer and Physics do this by querying the Game object for the entities they need to process. One of the common needs will be getting subsets of entities, so we are going to add this right now with addEntity().

Game
Property Type Description
_entities Array array of all the Entity objects
_enemies Array array of all the Enemy objects
_player Player the Player object
_gameFieldRect Rectangle rectangle that defines the boundaries of the game field
_started Boolean true if the game loop has been started
Function Return Type Description
start() none resets all variables to start a brand new game
_addEntity(entity) none add the entity and keep track of enemies
_removeEntities(entities) none remove the entities from all of the arrays
_update() none update the game by one time step
//
// Game Object
//
var game = (function () {
    var _entities,
        _enemies,
        _player,
        _gameFieldRect,
        _started = false;

    function _start() {
        _entities = [];
        _enemies = [];
        _gameFieldRect = new Rectangle(0, 0, 300, 180);

        this.addEntity(new Player( new Vector2d(100, 175), 25, new Vector2d(0, -1)));
        this.addEntity(new Enemy(new Vector2d(20, 25), 20, new Vector2d(0, 1), 0));
        this.addEntity(new Enemy(new Vector2d(50, 25), 10, new Vector2d(0, 1), 1));
        this.addEntity(new Enemy(new Vector2d(80, 25), 15, new Vector2d(0, 1), 2));
        this.addEntity(new Enemy(new Vector2d(120, 25), 25, new Vector2d(0, 1), 3));
        this.addEntity(new Enemy(new Vector2d(140, 25), 30, new Vector2d(0, 1), 4));

        if( !_started ) {
            window.requestAnimationFrame(this.update.bind(this));
            _started = true;
        }
    }

    function _addEntity(entity) {
        _entities.push(entity);

        if( entity instanceof Player ) {
            _player = entity;
        }

        if( entity instanceof Enemy ) {
            _enemies.push(entity);
        }
    }

    function _removeEntities(entities) {
        if( !entities ) return;

        function isNotInEntities(item) { return !entities.includes(item); }
        _entities = _entities.filter(isNotInEntities);
        _enemies = _enemies.filter(isNotInEntities);

        if(entities.includes(_player)) {
            _player = undefined;
        }
    }

    function _update() {
        var dt = 1/60; // Fixed 60 frames per second time step
        physics.update(dt);

        var i;
        for( i=_entities.length-1; i>=0; i-- ) {
            _entities[i].update(dt);
        }

        renderer.render(dt);

        window.requestAnimationFrame(this.update.bind(this));
    }

    return {
        start: _start,
        update: _update,
        addEntity: _addEntity,
        entities: function () { return _entities; },
        enemies: function () { return _enemies; },
        player: function () { return _player; },
        gameFieldRect: function () { return _gameFieldRect; }
    };
})();


Conclusion

We have created the mathematical foundation we need, given structure to the Game Entities and updated the Renderer, Physics and Game objects to use these new features. In the coming parts, we will fill in all the implementation details for each object as we create a Space Invaders clone.

Here is the road map for the rest of this series:

  • Part 2. User input, both keyboard and touch, with the player moving around.
  • Part 3. Enemy behavior, including descending and moving as a group.
  • Part 4. Projectiles and collision detection. The game will be playable at this point.
  • Part 5. Sprites and UI, handling title screens, menu, etc.
  • Part 6. Optimize the memory usage to ensure a stable frame rate.
  • Part 7. Switch to a 3D Renderer.
  • Part 8. Add an Event System and Audio.

I hope you have found this tutorial useful so far, and look forward to seeing what creations you build off of this base.

I have packaged the code for the full tutorial series for anyone interested in downloading it.

Question or Comment?