How to code Space Invaders (1978) – 7

Let’s code Space Invaders 👽

By far the most popular game released in 1978 was Space Invaders. In just 4 years it had grossed 3.8 billion! Space invaders was developed by Tomorohiro Nishikado who took a lot of inspiration from the Atari game Breakout which was released in 1976 which is a game I contemplated coding for my blog two weeks ago but I thought it was too similar to pong to bring any new concepts that haven’t already been covered in the series; I did Blockade instead.

I chose Space Invaders because nearly everyone on the planet has played it at some point, and if you haven’t, go bloody play it!

The Invaders!

The first thing we need to code up is the aliens, but first there are few rules about them that we must consider.

  • The aliens move in unison.
  • When they hit a wall they shift down and move the opposite direction.
  • Only the bottom most alien can shoot for a particular column.

Making the aliens move together!

moving aliens

Cute little alien class

The job of the alien class is to display the alien at specified position, so it will take the x, y location and the image for that alien. In my game we’re only going to use one alien image but you can go ahead and use as many as you wish!

class Alien {
    constructor(x, y, image) {
        this.x = x;
        this.y = y;
        this.image = image;
    }
    draw() {
        image(this.image, this.x, this.y, this.image.width/30, this.image.height/30);
    }
}

Oh yeah, and here is the alien image I’ve used for the game. I chose a stupidly large alien to show how you can scale images in P5. Take a look at the line of code in the draw() function, you can see that I’ve added an extra 2 optional arguments to the image() function, this is the width and height of the image, in our example I have divided the width and height of the image by 30. Take a look here for more details.

image of alien

Let’s create a new class called Invaders, which is going to take the image of the alien and the number of rows as a constructor argument. The number of rows is to potentially be able to make the game more difficult later on.

class Invaders {
    constructor(alienImage, rowsCount) {
        this.alienImage = alienImage;
        this.rowsCount = rowsCount;
        this.direction = 0;
        this.y = 40;
        this.aliens = this.initialiseAliens();
        this.bullets = [];
    
        this.speed = 0.2;
    }
    update() {
        for (let alien of this.aliens) {
            if (this.direction == 0) {
                alien.x+= this.speed;
            } else if (this.direction == 1) {
                alien.x-= this.speed;
            }
        }
   
        if (this.hasChangedDirection()) {
            this.moveAlienDown();
        }
        
    }
    hasChangedDirection() {
        for (let alien of this.aliens) {
            if (alien.x >= width - 40) {
                this.direction = 1;
                return true;
            } else if (alien.x <= 20) {
                this.direction = 0;
                return true;
            }
        }
        return false;
    }
    moveAlienDown() {
        for (let alien of this.aliens) {
            alien.y += 10;
        }
    }
    initialiseAliens() {
        let aliens = [];
        let y = 40;
        for (let i = 0; i < this.rowsCount; i++) {
            for (let x = 40; x < width - 40; x += 30) {
                aliens.push(new Alien(x, y, this.alienImage));
            }
            y += 40;
        }
        return aliens;
    }
    draw() {
        for (let alien of this.aliens) {
            alien.draw();
        }
    }
 
}

Making the bottom alien shoot.

Removing an alien

In order to make the bottom alien shoot, first we need to add a way to remove aliens from the game. For testing purposes I’m going to add a click handler to delete an alien.

So in our Invaders class let’s add a new function for checking the collision, and if there is a collision remove the alien from the game.

    checkCollision(x, y) {
      for (let i = this.aliens.length - 1; i >= 0; i--) {
          let currentAlien = this.aliens[i];
          // the numbers are hard-coded for the width of the image
          if (dist(x, y, currentAlien.x + 11.5, currentAlien.y + 8) < 10) {
              this.aliens.splice(i, 1);
              return true;
          }
      }
      return false;
    }

And then in sketch we can simply add the mousePressed() handler.

function mousePressed() {
    invaders.checkCollision(mouseX, mouseY);
}

Shooting

Now we need to get the bottom alien from each column and make the buggers shoot. I’ve added some comments to the new code.

class Invaders {
    constructor(alienImage, rowsCount) {
        this.alienImage = alienImage;
        this.rowsCount = rowsCount;
        this.direction = 0;
        this.y = 40;
        this.aliens = this.initialiseAliens();
        this.bullets = [];
    
        this.speed = 0.2;
      
      	// to make sure the aliens dont spam
        this.timeSinceLastBullet = 0;
    }
    update() {
        for (let alien of this.aliens) {
            if (this.direction == 0) {
                alien.x+= this.speed;
            } else if (this.direction == 1) {
                alien.x-= this.speed;
            }
        }
   
        if (this.hasChangedDirection()) {
            this.moveAlienDown();
        }
        if (this.aliens.length == 0) {
            this.nextLevel();
        }
      
      
       if (this.timeSinceLastBullet >= 40) {
          let bottomAliens = this.getBottomAliens();
          if (bottomAliens.length) {
              this.makeABottomAlienShoot(bottomAliens);
          }  
        }
      	this.timeSinceLastBullet++;
      
      
      // to move the bullets
      this.updateBullets();
        
    }
  
    hasChangedDirection() {
        for (let alien of this.aliens) {
            if (alien.x >= width - 40) {
                this.direction = 1;
                return true;
            } else if (alien.x <= 20) {
                this.direction = 0;
                return true;
            }
        }
        return false;
    }
    moveAlienDown() {
        for (let alien of this.aliens) {
            alien.y += 10;
        }
    }
  
   // to make sure only the bottom row will shoot
   getBottomAliens() {
        let allXPositions = this.getAllXPositions();
        let aliensAtTheBottom = [];
        for (let alienAtX of allXPositions) {
            let bestYPosition = 0;
            let lowestAlien;
            for (let alien of this.aliens) {
                if (alien.x == alienAtX) {
                    if (alien.y > bestYPosition) {
                        bestYPosition = alien.y;
                        lowestAlien = alien;
                    }
                }
            }
            aliensAtTheBottom.push(lowestAlien);
        }
        return aliensAtTheBottom;
    }
    nextLevel() {
        this.speed += 0.5;
        this.aliens = this.initialiseAliens();
    }
		// get all the x positions for a single frame
    getAllXPositions() {
        let allXPositions = new Set();
        for (let alien of this.aliens) {
            allXPositions.add(alien.x);
        }
        return allXPositions
    }
    
    initialiseAliens() {
        let aliens = [];
        let y = 40;
        for (let i = 0; i < this.rowsCount; i++) {
            for (let x = 40; x < width - 40; x += 30) {
                aliens.push(new Alien(x, y, this.alienImage));
            }
            y += 40;
        }
        return aliens;
    }
    draw() {
      
    	// draw the bullets first so they're underneath
      for (let bullet of this.bullets) {
          rect(bullet.x, bullet.y,  3, 10);
      }
      
      for (let alien of this.aliens) {
          alien.draw();
      }
      
  
    }
  
    checkCollision(x, y) {
      for (let i = this.aliens.length - 1; i >= 0; i--) {
          let currentAlien = this.aliens[i];
          if (dist(x, y, currentAlien.x + 11.5, currentAlien.y + 8) < 10) {
              this.aliens.splice(i, 1);
              return true;
          }
      }
      return false;
    }
  
  
    makeABottomAlienShoot(bottomAliens) {
      let shootingAlien = random(bottomAliens);
      let bullet = new AlienBullet(shootingAlien.x + 10, shootingAlien.y + 10);
    
      this.bullets.push(bullet);
      this.timeSinceLastBullet = 0;
    }
  
     updateBullets() {
        for (let i = this.bullets.length - 1; i >= 0; i-- ) {
            this.bullets[i].y  += 2;
        }
    }
}

Also I added a Bullet class and an Alien Bullet class, this is because we’re going to need some default functionality for the player. So when the player shoots the bullet will go the opposite direction.

Bullet

class Bullet {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    draw() {
        fill(255);
        rect(this.x, this.y, 3, 10);
    }
}

Alien Bullet

class AlienBullet extends Bullet {
    constructor(x, y) {
        super(x, y);
    }
    update() {
        this.y += 2;
    }
}

Adding the player!

shooting the aliens

Let’s add the player/alien fighter that the user will be controlling. Go ahead and copy the image below.

shooter.png
class Player {
    constructor(shooterImage) {
        this.image = shooterImage;
        this.x = width / 2;
        this.y = height -30;
        this.isMovingLeft = false;
        this.isMovingRight = false;
        this.bullets = [];
    }
    update() {
        if (this.isMovingRight) {
            this.x += 1;
        } else if (this.isMovingLeft) {
            this.x -= 1;
        }
        this.constrain();
        this.updateBullets();
    }
    updateBullets() {
        
        for (let i = this.bullets.length - 1; i >= 0; i--) {
            this.bullets[i].update();
            if (this.hasHitAlien(this.bullets[i])) {
                this.bullets.splice(i, 1);
                break;
            } else if (this.bullets[i].isOffScreen()) {
                this.bullets.splice(i, 1);
                break;
            }
        }
    }
    hasHitAlien(bullet) {
        return invaders.checkCollision(bullet.x, bullet.y);
    }
    constrain() {
        if (this.x <= 0) {
            this.x = 0;
        } else if(this.x > width - 23) {
            this.x = width - 23;
        }
    }
    draw() {
        image(this.image, this.x, this.y, this.image.width / 20, this.image.height/20);
        this.drawBullets();
    }
    drawBullets() {
        for (let bullet of this.bullets) {
            bullet.draw();
        }
    }
    drawLives() {
        fill(255);
        textSize(15);
        text("LIVES", 250, 25);
        for (let i = 0; i < this.lives; i++) {
            image(this.image, 300 + i * 30, 10, this.image.width / 20, this.image.height/20);
        }
    }
    drawScore() {
        text("SCORE", 50, 25);
        push();
        fill(100, 255, 100);
        text(this.score, 110, 25);
        pop();
    }
    moveLeft() {
        this.isMovingRight = false;
        this.isMovingLeft = true;
    }
    moveRight() {
        this.isMovingLeft = false;
        this.isMovingRight = true;
    }
    shoot() {
        this.bullets.push(new PlayerBullet(this.x + 12, this.y));
    }
}

And then we need to create another bullet class for the player, which will extend the Bullet class, so it takes all of the drawing behaviour from it.

class PlayerBullet extends Bullet {
    constructor(x, y) {
        super(x, y);
    }
    update() {
        this.y -= 6;
    }
}

Next add the mouse pressed handlers in sketch.js so that if you hit the arrow keys or space bar then an action will be performed, moving or shooting respectively.

// player moving and shooting
function keyPressed() {
  if (keyCode === RIGHT_ARROW || keyCode == 88) {
    player.moveRight();
  } else if (keyCode === LEFT_ARROW || keyCode == 90) {
    player.moveLeft();
  } else if (keyCode === 32) {
    player.shoot();
  }
}

What’s left to do

finished game

Obviously this gives us the core mechanics of the Space Invaders game, but there is so much more we can potentially add. What I’ve coded up that you can see on my github is a scoring system a maximum number of lives and a difficulty system (So when you clear the first wave, the next wave is faster). You could also add the destructible rocks that you can hide behind!

Finally

Thank you for getting this far in the blog, you’ve done well ;). If you have an ideas for future potential games that you’d like me to code up, please don’t hesitate to @ me on twitter.

10 comments

Leave a Reply