Brian Koponen

Programming and Tech Tips

JavaScript Game Tutorial - Space Invaders Part 3 - Enemy Behavior

This is the third 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 2, we implemented the user input system and got the player moving on screen. In this part, we will implement the Enemy behavior.


Goals

The goals for this part are:


Game

The Game object controls the waves of enemies to make them more difficult over time. We add a few properties to Game to deal with this. We also need to keep track of the bounding box around the enemies.

Game
Property Type Description
_lastFrameTime Number the time at the previous frame
_enemiesRect Rectangle rectangle that surrounds all existing enemies
_enemySpeed Number base movement speed of all enemies
_enemyFirePercent Number percent that the enemy will fire weapon
_enemyDropAmount Number distance (in world units) that enemies drop when they hit an edge
Function Return Type Description
update( time ) none create enemies in grid, update enemiesRect

Add these properties to the Game object and set their values in the _start() function. Remove the enemy creation from _start() as we are moving the enemy creation code into _update().

function _start() {
    _lastFrameTime = 0;

    _entities = [];
    _enemies = [];
    _gameFieldRect = new Rectangle(0, 0, 300, 180);
    _enemiesRect = new Rectangle(0, 0, 0, 0);
    _enemySpeed = 10;
    _enemyFirePercent = 10;
    _enemyDropAmount = 1;

    this.addEntity( new Player( new Vector2d(100, 175), 90, new Vector2d(0, 0)) );

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

The rest of the changes are in the _update( time ) function. Once you have a lot of things on screen, you can start to notice frame rate issues. So I've updated from a fixed time step to a calculated, but limited, one. This allows the occasional frame drop without slowing down the game, but a larger drop will cap at 3/60 of a second, so the game clock will actually slow down. This should rarely happen, but it's nice to smooth out frame drops like this.

function _update( time ) {

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

    _lastFrameTime = time;

    // Update Physics
    physics.update(dt);

To calculate the enemiesRect, we use the rectUnion function over all the existing enemy collision rectangles. This will give us one giant rectangle that encompasses every enemy on screen. The Enemy will use this information to determine when the whole group has hit the edge of the screen.

    // 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);
    }

We set the speed of the enemies so that they get faster as the enemies die. When we add the projectiles in the next part, we will be removing the dead enemies and the remaining enemies will naturally get faster as _enemies.length gets smaller.

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

The creation of the enemies happens when there are no enemies left. We calculate a grid placement. We set the position 100 above the dropTarget so the enemies will descend from the top of the screen. By increasing _enemySpeed, _enemyFirePercent and _enemyDropAmount, the game gets increasingly more difficult with each round of enemies. The amount you raise these will dictate how fast the game increases in difficulty.

    // Create the grid of 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;
    }

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

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

Finally, expose enemiesRect in the return statement:

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


Enemy

The enemies move as a group because each individual enemy is performing the same movement calculation. It would be just as easy at this point to have different enemies have different movement patterns. In this way you could implement Galaxian instead of Space Invaders.

Enemy
Property Type Description
dropTarget Number the y-value of vertical position we should be
dropAmount Number how much we drop each time an edge is hit
timer Number time elapsed
firePercent Number percent chance to fire weapon
fireWait Number seconds to wait between chance to fire
Function Return Type Description
update( dt ) none check for the dropTarget, hitting edges, firing weapon
fire(position) none fire weapon with projectile starting at position

Add these new variables to the Enemy constructor:

function Enemy(position, speed, direction, rank) {
    Entity.call(this, position, speed, direction);

    this.width = 13;
    this.height = 10;
    this.rank = rank;

    this.dropTarget = 0;
    this.dropAmount = 1;
    this.timer = 0;
    this.firePercent = 10;
    this.fireWait = Math.random() * 5;
}

The big changes occur in update( dt ). First we determine what direction to move. If the enemiesRect is within a margin of either edge of the gameFieldRect, we set a new dropTarget. If the current position is above the dropTarget, we set the direction downwards. Once we have hit the dropTarget, we set the direction either left or right depending on which edge of the screen the enemiesRect hit.

Enemy.prototype.update = function (dt) {

    // Edge collision
    var enemiesLeft = game.enemiesRect().left(),
        enemiesRight = game.enemiesRect().right(),
        edgeMargin = 5,
        gameLeftEdge = game.gameFieldRect().left() + edgeMargin,
        gameRightEdge = game.gameFieldRect().right() - edgeMargin;

    Entity.prototype.update.call(this, dt);

    // Drop if the enemiesRect hits an edge margin
    if( (this.direction.x < 0 && enemiesLeft < gameLeftEdge) ||
        (this.direction.x > 0 && enemiesRight > gameRightEdge) ) {
        this.dropTarget += this.dropAmount;
    }

    // Determine Direction
    if( this.position.y < this.dropTarget ) {
        this.direction = new Vector2d(0, 1);
    }
    else if( this.direction.y > 0 ) {
        this.direction = (enemiesRight > gameRightEdge) ?
                            new Vector2d(-1, 0) :
                            new Vector2d(1, 0);
    }

We don't want the firing to be a predictable pattern, so to make it interesting, we introduce two random factors: fireWait and firePercent. First we have to wait between shots. Once the wait has been satisfied, we will only fire a certain percentage of the time. In addition to these random factors, we check that there isn't another enemy below. Only the bottom enemy in a column can fire. That's what existsUnderneath() is checking. Much like in the Player object, we have to wait to implement the firing until we add projectiles in the next part of this series.

    // Determine Firing Weapon
    var p = vectorAdd(this.position, new Vector2d(0, 5));

    function existsUnderneath(e) {
        var rect = e.collisionRect();
        return p.y <= rect.top() &&
               rect.left() <= p.x && p.x <= rect.right();
    }

    this.timer += dt;
    if( this.timer > this.fireWait ) {
        this.timer = 0;
        this.fireWait = 1 + Math.random() * 4;

        if( randomInt(100) < this.firePercent &&
            !game.enemies().find(existsUnderneath) ) {
            this.fire(p);
        }
    }
};

Enemy.prototype.fire = function (position) {
    console.log("Fire to be implemented");
};


Conclusion

In this part we updated the Game and Enemy objects to create the enemy behavior. You can see the game beginning to take form now, though there isn't anything the player can actually do. With the addition of projectiles and collision detection, the whole game will come together.

The road map for the rest of the series is:

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

Question or Comment?