JavaScript Game Tutorial - Space Invaders Part 4 - Collision Detection

This is the fourth 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 Part 3, we implemented the enemy movement and behavior and now we are going to bring it all together to finally make this a playable game. We will add the Projectile Entity, implement collision detection, scoring and game over. With these final components, the player will be able to kill enemies and be killed.


Goals

The goals for this part are:

  • Create the Projectile entity.
  • Update the Renderer to draw the projectiles.
  • Update the Player and Enemy fire functions.
  • Update Physics with collision detection.
  • Update Game to delete dead entities, keep score and check for game over.


Projectile

There are two types of projectiles: player and enemy. Enemy projectiles only hurt the player and player projectiles only hurt the enemies. This is determined by the Projectile type property.

Projectile
Property Type Description
type String either "player" or "enemy"
//
// Projectile
//
function Projectile(position, speed, direction, type) {
  Entity.call(this, position, speed, direction);

  this.width = 1;
  this.height = 5;
  this.type = type;
}
Projectile.prototype = Object.create(Entity.prototype);


Renderer

Update the Renderer to draw the projectiles, using two colors to distinguish the enemy and player projectile types.

_projectileColors = {"player": "rgb(196, 208, 106)",
                      "enemy": "rgb(96, 195, 96)"};

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);
        }
        else if( entity instanceof Projectile ) {
            _drawRectangle(_projectileColors[entity.type], entity);
        }
    }
}


Game

The Game object needs to keep track of the Projectile entities in the same way it tracks Enemy entities. Add a new property _projectiles to the Game object and initialize it to an empty array in _start(). Update the addEntity() and removeEntities() functions to keep track of projectiles. Finally, expose the projectiles property in the return statement.

Game
Property Type Description
_projectiles Array all the Projectile entities
// Add to the _start function
_projectiles = [];

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

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

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

    if( entity instanceof Projectile ) {
        _projectiles.push(entity);
    }
}

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

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

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

// Add to the return statement
projectiles: function () { return _projectiles; }

Now we have the Projectile Entity fully integrated and ready to use. So let's implement the fire commands of both the Player and Entity.


Player

The player can only have one projectile on screen at a time, so it has to check for the existence of a player projectile before firing. If there are no player projectiles, it will create one.

Player.prototype.fire = function () {
    function isPlayerType(proj) {
        return (proj.type === "player");
    }

    var playerProjectileCount = game.projectiles().filter(isPlayerType).length;
    if( playerProjectileCount === 0 ) {
        game.addEntity( new Projectile( this.position,
                                        180,
                                        new Vector2d(0, -1),
                                        "player" ));
    }
};


Enemy

The enemy has no restrictions on its projectiles, so its fire function just creates a projectile.

Enemy.prototype.fire = function (position) {
    game.addEntity( new Projectile( position,
                                    60,
                                    new Vector2d(0, 1),
                                    "enemy") );
};


Physics

Collision Detection is a major part of a game engine. Faulty collisions are one of the most noticeable bugs that a player will see. As you move on to make more complex games, the physics and collision detection systems are the first things you will have to improve. A very general overview of a collision system is that first you determine a list of items that need to be checked if they have collided. Then you actually check for the collisions. Lastly you resolve the collisions. Each of these parts can become very complex, especially in 3D games.

Luckily, we only need a simple system for a Space Invaders clone. We will be checking for a few different collision scenarios:

  • Enemy Projectile hits Player
  • Player Projectile hits Enemy
  • Enemy hits Player
  • Enemy hits bottom of game field
  • Projectile leaves game field

Resolving the collisions is as simple as damaging the entities involved.

function _update(dt) {
    var i, j,
        entities = game.entities(),
        enemies = game.enemies(),
        projectiles = game.projectiles(),
        player = game.player();

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

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

    // Collision Detection
    var collisionPairs = [];

    // Enemies vs Player
    for( i=enemies.length-1; i>=0; i-- ) {
        collisionPairs.push( { entity0: enemies[i],
                               entity1: player } );
    }

    // Projectiles vs other Entities
    for( i=projectiles.length-1; i>=0; i-- ) {

        // Enemy Projectiles vs Player
        if( projectiles[i].type === "enemy") {
            collisionPairs.push( { entity0: projectiles[i],
                                   entity1: player } );
        }

        // Player Projectiles vs Enemies
        if( projectiles[i].type === "player" ) {
            for( j=enemies.length-1; j>=0; j-- ) {
                collisionPairs.push( { entity0: projectiles[i],
                                       entity1: enemies[j] } );
            }
        }
    }

    // Collision Check
    for( i=collisionPairs.length-1; i>=0; i-- ) {
        var e0 = collisionPairs[i].entity0;
        var e1 = collisionPairs[i].entity1;

        if( e0 && e1 && e0.collisionRect().intersects(e1.collisionRect()) ) {
            // Resolve Collision
            e0.hp -= 1;
            e1.hp -= 1;
        }
    }

    // Enemy vs floor (special case)
    if( game.enemiesRect() && player &&
        game.enemiesRect().bottom() > player.collisionRect().bottom() ) {
        game.setGameOver();
    }

    // Projectile leaves game field (special case)
    for( i=projectiles.length-1; i>=0; i-- ) {
        var proj = projectiles[i];
        if( !game.gameFieldRect().intersects(proj.collisionRect()) ) {
            proj.hp -= 1;
        }
    }
}

With collision detection in place, we just have to tie everything together in the Game object's update() function.


Game

Now that entities can actually be killed with collisions, the Game object has to remove the dead entities, track the player's remaining lives, keep score and determine game over. We need a few new properties for this.

Game
Property Type Description
_livesRemaining Number the remaining lives of the player
_gameOver Boolean true if the game over condition has been met
_score Number current score
_highScores Array array of high scores
Function Return Type Description
_addScore( score ) none adds score to the list of high scores
_setGameOver() none set the game over state and record the high score

Add the new properties to Game and initialize them in _start(). The high scores are stored in localStorage if available. This saves the scores between page loads. JSON.parse() throws an exception if localStorage.invadersScores doesn't exist or is otherwise corrupted. In that case, we just create an empty high scores array.

_livesRemaining = 2;
_gameOver = false;
_score = 0;
_highScores = [];

if (typeof(Storage) !== "undefined") {
    try {
        _highScores = JSON.parse(localStorage.invadersScores);
    }
    catch(e) {
        _highScores = [];
    }
}

Create the _addScore() function to insert a score into the list of high scores. The list is sorted and only the top 10 scores are saved.

function _addScore(score) {
    _highScores.push(score);
    _highScores.sort(function(a, b){return b-a});
    _highScores = _highScores.slice(0, 10);

    if (typeof(Storage) !== "undefined") {
        localStorage.invadersScores = JSON.stringify(_highScores);
    }
}

Create the _setGameOver() function that simply sets the _gameOver variable and records the current score.

function _setGameOver() {
    _gameOver = true;
    _addScore(Math.round(game.score()));
}

The update function has to clean up the entities and perform some bookkeeping. If a projectile dies, they are removed. If an enemy dies, they are removed and the score increases based on the enemy rank. If the player dies, they lose a life. If the remaining lives is less than zero, the game is over. When the game is over, we interrupt the entire game loop, which literally stops the game.

function _update(time) {
    var i, j,
        dt = Math.min((time - _lastFrameTime) / 1000, 3/60);

    _lastFrameTime = time;

    if( _gameOver ) {
        _started = false;
        return;
    }

    // Update Physics
    physics.update(dt);

    // Calculate the bounding rectangle around the enemies
    _enemiesRect = _enemies.reduce(function(rect, e) {
                        return rectUnion(rect, e.collisionRect());
                    },
                    undefined);

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

    // Delete dead objects.
    var removeEntities = [];
    for( i=_entities.length-1; i>=0; i-- ) {
        var e = _entities[i];
        if( e.hp <= 0 ) {
            removeEntities.push(e);

            if( e instanceof Enemy ) {
                _score += e.rank + 1;
            }

            else if( e instanceof Player ) {
                _livesRemaining--;
                this.addEntity( new Player( new Vector2d(100, 175), 90, new Vector2d(0, 0) ));
            }
        }
    }
    _removeEntities(removeEntities);

    // Update Enemy Speed
    var speed = _enemySpeed + (_enemySpeed*(1-(_enemies.length/50)));
    for( i=_enemies.length-1; i>=0; i-- ) {
        _enemies[i].speed = speed;
    }

    // Create new Enemies if there are 0
    if( _enemies.length === 0 ) {
        for( i=0; i<10; i++) {
            for( j=0; j<5; j++) {
                var dropTarget = 10+j*20,
                    position = new Vector2d(50+i*20, dropTarget-100),
                    direction = new Vector2d(1, 0),
                    rank = 4-j,
                    enemy = new Enemy(position,
                                    _enemySpeed,
                                    direction,
                                    rank);

                enemy.dropTarget = dropTarget;
                enemy.firePercent = _enemyFirePercent;
                enemy.dropAmount = _enemyDropAmount;

                this.addEntity( enemy );
            }
        }

        _enemySpeed += 5;
        _enemyFirePercent += 5;
        _enemyDropAmount += 1;
    }

    // Check for Game Over
    if( _livesRemaining < 0 && !_gameOver ) {
        _setGameOver();
    }

    // Render the frame
    renderer.render(dt);

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

// Expose in the return statement
score: function () { return _score; },
highScores: function () { return _highScores; },
livesRemaining: function () { return _livesRemaining; },
gameOver: function () { return _gameOver; },
setGameOver: _setGameOver



Conclusion

As you can see, the game is actually playable! We've built up to this point one piece at a time, but now that it is all working, I think you can see how important having the proper structure has been to making this simple game. Each major component operates independently of the others, allowing great flexibility and later code reuse.

The road map for the rest of the series is:

  • Part 5. Focus on the Renderer, adding Sprites and all the UI elements, as well as general polish to the game.
  • 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 have packaged the code for the full tutorial series for anyone interested in downloading it.

Question or Comment?