Let’s Build Space Invader Game With Python

Developing a game is very interesting to do. Today we are going to build a space invader game with python’s library called “pygame“. In our game, there will be enemy ships which we need to destroy with our ship which can shoot a bullet. If a specific amount of enemy ship passes our ship we will lose.  Any enemy ship hits our ship, we will lose. We run out of ammo, we lose. So, this is what we are gonna build today with Python.

Prerequisites

  • Linux (mine is ubuntu 18.04)
  • Python3.7
  • pip
  • venv
  • pygame

Environment Setup & Installation

Always remember, whenever you are becoming a crazy horse to build a new python project, just hold that horse for a few seconds. Set up a virtual environment first and thank me later. Go to your favorite directory and follow the steps

python3 -m venv my_env
cd my_env

Now we need to activate the virtual environment. And then we need to install pygame.

. bin/activate
pip install pygame

Now create our project repository. We will call our project space_invader.

mkdir space_invader

Game Assets

I gave a GitHub link at the very bottom of this article. Visit there and download the assets we need in our game. We will need an image file for our ship, enemy ship, and bullet. And for some sound effects, we need explosion and firing sound.

Develop The Game

First, we need to import all the things we need.

import os
import random
import math
import pygame

Then we need to init the game.

pygame.mixer.pre_init(frequency=44100)
pygame.init()
pygame.mixer.init(frequency=44100)

Now, to make the game visible we need a window, right? Let’s create one with size, an icon, and a caption.

if __name__ == "__main__":

    # window config
    window_height = 800
    window_width = 800

    window = pygame.display.set_mode((window_width, window_height))
    pygame.display.set_caption('War Ship')
    game_icon = pygame.image.load(os.path.join("assets", "icon.png"))
    pygame.display.set_icon(game_icon)

Now, let’s load the assets and fonts we will use later on.

# assets loading and fonts
BACKGROUND = pygame.image.load(os.path.join("assets", "background.jpg"))
PLAYER_IMAGE = pygame.image.load(os.path.join("assets", "spaceship.png"))
RED_ENEMY_IMAGE = pygame.image.load(os.path.join("assets", "red-enemy.png"))
BULLET = pygame.image.load(os.path.join("assets", "bullet.png"))

explosion_sound = pygame.mixer.Sound(os.path.join("assets",
                                                  "explosion.WAV"))
font = pygame.font.Font("freesansbold.ttf", 20)
game_over_font = pygame.font.Font("freesansbold.ttf", 70)

Scoring

Initially, our score will be zero, and our life will be 100. Also, the total score to obtain in case of winning is 1000. We will set a rules for the ammo limit, which is in our case (winning score / 10) + 20. Of course, you can set your own rules here.

# scoring
score = 0
life = 100
winning_score = 1000
ammo = winning_score/10 + 20

Game property configuration

Let’s set the initial position and velocity of our fighter ship.

# player
player_x = 350
player_y = 730
player_velocity = 5

Also, we have the freedom to decide these values. But make sure the values make sense.

Let’s do the same for an enemy ship, but the enemy ship will be multiple.

# enemy ships
enemy_image = []
enemy_x = []
enemy_y = []
enemy_velocity = []
enemy_number = 5
for i in range(0, enemy_number):
    enemy_image.append(RED_ENEMY_IMAGE)
    enemy_x.append(random.randint(70, 730))
    enemy_y.append(70)
    enemy_velocity.append(random.uniform(0.1, 0.5))

Now, while we are configuring our bullet properties, we must make sure it’s faster than other ships.

# bullet
bullet_x = 0
bullet_y = 750
bullet_velocity = 15
bullet_state = "ready"

Actions

Now, let’s build a method to show the score, life, and ammo. And also the texts for winning and game-over event.

# actions
def show_score():
    score_text = font.render("Score : " + str(score), True, (255, 255, 255))
    ammo_text = font.render("Ammo : " + str(ammo), True, (255, 255, 255))
    color = (255, 255, 255)
    if 100 > life >= 60:
        color = (14, 214, 0)
    if 60 > life > 40:
        color = (177, 184, 0)
    if 40 > life >= 0:
        color = (184, 0, 0)

    life_text = font.render("Life : " + str(life), True, color)
    window.blit(score_text, (10, 10))
    window.blit(ammo_text, (400, 10))
    window.blit(life_text, (600, 10))
    

def game_over_text():
    text = font.render("GAME OVER", True, (255, 255, 255))
    window.blit(text, (400, 400))


def winner_text():
    text = font.render("YOY WIN", True, (184, 0, 0))
    window.blit(text, (400, 400))

And also methods to display the player ship, enemy ships, and bullet in the window.

def player(x, y):
    window.blit(PLAYER_IMAGE, (x, y))


def enemy(x, y, nth_enemy):
    window.blit(enemy_image[nth_enemy], (x, y))


def trigger_bullet(x, y):
    global bullet_state
    bullet_state = "fire"
    window.blit(BULLET, (x + 10, y + 10))

We also need a method to check collision. But to do that we need a little bit of help from mathematics. To find the distance between two points by using the distance formula, which is an application of the Pythagorean theorem. We can rewrite the Pythagorean theorem as d=√((x_2-x_1)²+(y_2-y_1)²) to find the distance between any two points

Also, we will play a sound when this action occurs.

def collision(ex, ey, bx, by):
    distance = math.sqrt(math.pow((ex - bx), 2) + math.pow((ey - by), 2))
    if distance < 30:
        explosion_sound.play()
        return True
    return False

The game loop

Now lets, build the game loop and check events in it for now. in our game loop we need to load a background image for a better view. And if we quit the window the game will get out from the while loop.

# Game Loop

run = True
while run:
    window.fill((0, 0, 0))
    window.blit(BACKGROUND, (0, 0))

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False

Now let’s catch the left and right keypress events and change the coordinate of our player ship.

keys = pygame.key.get_pressed()

if keys[pygame.K_LEFT] and player_x > player_velocity:
    player_x -= player_velocity
if keys[pygame.K_RIGHT] and player_x < (window_width - 64 -
                                        player_velocity):
    player_x += player_velocity

To fire bullet we will press the space key, so we need to catch the space button press event. We will also play a sound and keep track of our ammo. If we ran out of ammo then we will simple get lost from the game loop by breaking the while loop.

if keys[pygame.K_SPACE]:
    if bullet_state is "ready" and ammo > 0:
        print("FIRE IN THE HOLE!")
        ammo -= 1
        bullet_sound = pygame.mixer.Sound(
            os.path.join("assets", "GUNSHOT.WAV"))
        bullet_sound.play()
        bullet_x = player_x
        trigger_bullet(bullet_x, bullet_y)

        if ammo == 0:
            game_over_text()
            run = False

Now, let’s change and check the bullet state. If the bullet state is “fire” then we will keep increasing the Y-AXIS position of the bullet.

if bullet_y <= 0:
    bullet_y = 750
    bullet_state = "ready"

if bullet_state == "fire":
    trigger_bullet(bullet_x, bullet_y)
    bullet_y -= bullet_velocity

Now,  let’s work on the enemy ship. We need to keep them moving through the Y-Axis. And if enemy ship passes our ship we will lose 10 life out of total 100 life. And a collision between bullet and enemy ship happen, we need to remove the enemy ship from our window. But if any collision between our ship and enemy ship occurs then the game will be over by breaking the loop.

for j in range(0, enemy_number):
    if enemy_y[j] >= 750:
        life -= 10
        if life == 0:
            game_over_text()
            run = False
        enemy_y[j] = 10

    enemy_y[j] += enemy_velocity[j]

    if collision(enemy_x[j], enemy_y[j], bullet_x, bullet_y):
        print("ENEMY DESTROYED")
        score += 10
        if score >= winning_score:
            winner_text()
            run = False
        bullet_y = 750
        bullet_state = "ready"
        enemy_x[j] = random.randint(70, 730)
        enemy_y[j] = 10

    if collision(enemy_x[j], enemy_y[j], player_x, player_y):
        game_over_text()
        run = False

    enemy(enemy_x[j], enemy_y[j], j)

Now at the very end display the player and show the score.

player(player_x, player_y)
show_score()

pygame.display.update()

And outside of the loop, we will just quit the window.

pygame.quit()

Source Code

If you followed all the steps then your code will look like this. And of course, you can improve the code by dividing them in separate files. We will see the OOP version of this game later.

import os
import random
import math
import pygame

pygame.mixer.pre_init(frequency=44100)
pygame.init()
pygame.mixer.init(frequency=44100)

if __name__ == "__main__":

    # window config
    window_height = 800
    window_width = 800

    window = pygame.display.set_mode((window_width, window_height))
    pygame.display.set_caption('War Ship')
    game_icon = pygame.image.load(os.path.join("assets", "icon.png"))
    pygame.display.set_icon(game_icon)

    # assets loading and fonts
    BACKGROUND = pygame.image.load(os.path.join("assets", "background.jpg"))
    PLAYER_IMAGE = pygame.image.load(os.path.join("assets", "spaceship.png"))
    RED_ENEMY_IMAGE = pygame.image.load(os.path.join("assets", "red-enemy.png"))
    BULLET = pygame.image.load(os.path.join("assets", "bullet.png"))

    explosion_sound = pygame.mixer.Sound(os.path.join("assets",
                                                      "explosion.WAV"))
    font = pygame.font.Font("freesansbold.ttf", 20)
    game_over_font = pygame.font.Font("freesansbold.ttf", 70)

    # scoring
    score = 0
    life = 100
    winning_score = 1000
    ammo = winning_score/10 + 20

    # player
    player_x = 350
    player_y = 730
    player_velocity = 5

    # enemy ships
    enemy_image = []
    enemy_x = []
    enemy_y = []
    enemy_velocity = []
    enemy_number = 5
    for i in range(0, enemy_number):
        enemy_image.append(RED_ENEMY_IMAGE)
        enemy_x.append(random.randint(70, 730))
        enemy_y.append(70)
        enemy_velocity.append(random.uniform(0.1, 0.5))

    # bullet
    bullet_x = 0
    bullet_y = 750
    bullet_velocity = 15
    bullet_state = "ready"

    # actions
    def show_score():
        score_text = font.render("Score : " + str(score), True, (255, 255, 255))
        ammo_text = font.render("Ammo : " + str(ammo), True, (255, 255, 255))
        color = (255, 255, 255)
        if 100 > life >= 60:
            color = (14, 214, 0)
        if 60 > life > 40:
            color = (177, 184, 0)
        if 40 > life >= 0:
            color = (184, 0, 0)

        life_text = font.render("Life : " + str(life), True, color)
        window.blit(score_text, (10, 10))
        window.blit(ammo_text, (400, 10))
        window.blit(life_text, (600, 10))

    def player(x, y):
        window.blit(PLAYER_IMAGE, (x, y))


    def enemy(x, y, nth_enemy):
        window.blit(enemy_image[nth_enemy], (x, y))


    def trigger_bullet(x, y):
        global bullet_state
        bullet_state = "fire"
        window.blit(BULLET, (x + 10, y + 10))


    def collision(ex, ey, bx, by):
        distance = math.sqrt(math.pow((ex - bx), 2) + math.pow((ey - by), 2))
        if distance < 30:
            explosion_sound.play()
            return True
        return False

    def game_over_text():
        text = font.render("GAME OVER", True, (255, 255, 255))
        window.blit(text, (400, 400))

    def winner_text():
        text = font.render("YOY WIN", True, (184, 0, 0))
        window.blit(text, (400, 400))

    # Game Loop

    run = True
    while run:
        window.fill((0, 0, 0))
        window.blit(BACKGROUND, (0, 0))

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run = False

        keys = pygame.key.get_pressed()

        if keys[pygame.K_LEFT] and player_x > player_velocity:
            player_x -= player_velocity
        if keys[pygame.K_RIGHT] and player_x < (window_width - 64 -
                                                player_velocity):
            player_x += player_velocity
        if keys[pygame.K_SPACE]:
            if bullet_state is "ready" and ammo > 0:
                print("FIRE IN THE HOLE!")
                ammo -= 1
                bullet_sound = pygame.mixer.Sound(
                    os.path.join("assets", "GUNSHOT.WAV"))
                bullet_sound.play()
                bullet_x = player_x
                trigger_bullet(bullet_x, bullet_y)

                if ammo == 0:
                    game_over_text()
                    run = False

        if bullet_y <= 0:
            bullet_y = 750
            bullet_state = "ready"

        if bullet_state == "fire":
            trigger_bullet(bullet_x, bullet_y)
            bullet_y -= bullet_velocity

        for j in range(0, enemy_number):
            if enemy_y[j] >= 750:
                life -= 10
                if life == 0:
                    game_over_text()
                    run = False
                enemy_y[j] = 10

            enemy_y[j] += enemy_velocity[j]

            if collision(enemy_x[j], enemy_y[j], bullet_x, bullet_y):
                print("ENEMY DESTROYED")
                score += 10
                if score >= winning_score:
                    winner_text()
                    run = False
                bullet_y = 750
                bullet_state = "ready"
                enemy_x[j] = random.randint(70, 730)
                enemy_y[j] = 10

            if collision(enemy_x[j], enemy_y[j], player_x, player_y):
                game_over_text()
                run = False

            enemy(enemy_x[j], enemy_y[j], j)

        player(player_x, player_y)
        show_score()

        pygame.display.update()

pygame.quit()

My Github Repository: https://github.com/zim0101/python_space_invader

Feel free to clone the repo and enjoy the game.