Pygame Zero: Space Invaders II

By Lucy Hattersley. Posted

Space Invaders must be the first computer game that springs to mind for a lot of people. Our advanced Space Invaders code has all the extras

If you're looking to code Space Invaders in Python using Pygame then you've come to the right place.

In this previous Space Invaders tutorial; We set up the basics for our Space Invaders game with our player ship controlled by the keyboard, defence bases, the aliens moving backwards and forwards across the screen, and lasers flying everywhere. In this part we will add lives and levels to the game, introduce a bonus alien, code a leader board for high scores, and add some groovy sound effects. We may even get round to adding an introduction screen if we get time. We are going to start from where we left off in part one.

This tutorial was written by Mark Vanstone and first appeared in The MagPi magazine issue 75. Click here to download your free digital copy of The MagPi magazine.

If you don’t have the part one code and files, you can download them from GitHub.

Space Invaders II: You'll need

Space Invaders II: You only live thrice

It was a tradition with Space Invaders to be given three lives at the start of the game. We can easily set up a place to keep track of our player lives by writing player.lives = 3 in our init() function. While we are in the init() function, let’s add a player name variable with player.name = "" so that we can show names on our leader board, but we’ll come to that in a bit. To display the number of lives our player has, we can add drawLives() to our draw() function and then define our drawLives() function containing a loop which ‘blits’ life.png once for each life in the top left of the screen.

Life after death

Now we have a counter for how many lives the player has, we will need to write some code to deal with what happens when a life is lost. In part one we ended the game when the player.status reached 30. In our update() function we already have a condition to check the player.status and if there are any aliens still alive. Where we have written if player.status == 30: we can write player.lives -=1. We can also check to see if the player has run out of lives when we check to see if the RETURN (aka ENTER) key is pressed.

Keep calm and carry on

Once we have reduced player.lives by one and the player has pressed the RETURN key, all we need to do to set things back in motion is to set player.status = 0. We may want to reset the laser list too, because if the player was hit by a flurry of lasers we may find that several lives are lost without giving the player a chance to get out of the way of subsequent lasers. We can do this by writing lasers = []. If the player has run out of lives at this point, we will send them off to the leader‑board page. See figure1.py to examine the code for dealing with lives.

def draw()
    # additional drawing code
    drawLives()
    if player.status >= 30:
        if player.lives > 0:
            drawCentreText(
"YOU WERE HIT!\nPress Enter to re-spawn")
        else:
            drawCentreText(
"GAME OVER!\nPress Enter to continue")

def init():
    # additional player variables
    player.lives = 3
    player.name = ""

def drawLives():
    for l in range(player.lives):
        screen.blit("life", (10+(l*32),10))

def update():
    # additional code for life handling
    global player, lasers
    if player.status < 30 and len(aliens) > 0:
        if player.status > 0:
            player.status += 1
            if player.status == 30:
                player.lives -= 1
    else:
        if keyboard.RETURN:
            if player.lives > 0:
                player.status = 0
                lasers = []
            else:
                # go to the leader-board
                pass;

def drawCentreText(t):
    screen.draw.text(t , center=(400, 300), owidth=0.5, ocolor=(255,255,255), color=(255,64,0) , fontsize=60)

On the level

The idea of having levels is to start the game in an easy mode; then, when the player has shot all the aliens, we make a new level which is a bit harder than the last. In this case we are going to tweak a few variables to make each level more difficult. To start, we can set up a global variable level = 1 in our init() function. Now we can use our level variable to alter things as we increase the value. Let’s start by speeding up how quickly the aliens move down the screen as the level goes up. When we calculate the movey value in updateAliens(), we can write movey = 40 + (5*level) on the condition that moveSequence is 10 or 30.

Space Invaders II: On the up

To go from one level to the next, the player will need to shoot all the aliens. We can tell if there are any aliens left if len(aliens) = 0. So, with that in mind, we can put a condition in our draw() function with if len(aliens) == 0: and then draw text on the screen to say that the level has been cleared. We can put the same condition in the section of the update() function where we are waiting for RETURN to be pressed. When RETURN is pressed and the length of the aliens list is 0, we can add 1 to level and call initAliens() and initBases() to set things ready to start the new level.

Front and centre

You may have noticed in figure1.py that we made a couple of calls to a function called drawCentreText() which we have not yet discussed. All that this function does is to shorten the process of writing text to the centre of the screen. We assume that the text will be positioned at coordinates (400, 300) and will have a set of standard style settings and colours, and the function definition just contains one line: screen.draw.text(t , center=(400, 300), owidth=0.5, ocolor=(255,255,255), color=(255,64,0), fontsize=60) – where t is passed into the function as a parameter.

Flying like a boss

To liven up our game a little bit, we are going to add in a bonus or boss alien. This could be triggered in various ways, but in this case we will start the boss activity with a random number. First we will need to create the boss actor. Because there will only ever be one boss alien on screen at any time, we can just use one actor created near the start of our code. In this case we don’t need to give it coordinates as we will start the game with the boss actor not being drawn. We write boss = Actor("boss").

 Lasers can be very bad for your health. Best to avoid them

Keeping the boss in the loop

We want to start the game with the boss not being displayed, so we can add to our init() function boss.active = False and then in our draw() function if boss.active: boss.draw(), which will mean the boss will not be drawn until we make it active. In our update() function, along with our other functions to update elements, we can call updateBoss(). This function will update the coordinates of the boss actor if it is active or, if it is not, check to see if we need to start a new boss flying. See figure2.py for the updateBoss() function.

def updateBoss():
    global boss, level, player, lasers
    if boss.active:
        boss.y += (0.3*level)
        if boss.direction == 0: boss.x -= (1* level)
        else: boss.x += (1* level)
        if boss.x < 100: boss.direction = 1
        if boss.x > 700: boss.direction = 0
        if boss.y > 500:
            sounds.explosion.play()
            player.status = 1
            boss.active = False
        if randint(0, 30) == 0:
            lasers.append(Actor("laser1", (boss.x,boss.y)))
            lasers[len(lasers)-1].status = 0
            lasers[len(lasers)-1].type = 0
    else:
        if randint(0, 800) == 0:
            boss.active = True
            boss.x = 800
            boss.y = 100
            boss.direction = 0

Did you hear that?

You may have noticed that in figure2.py we have an element of Pygame Zero that we have not discussed yet, and that is sound. If we write sounds.explosion.play(), then the sound file located at sounds/explosion.wav will be played. There are many free sound effects for games on the internet. If you use a downloaded WAV file, make sure that it is fairly small. You can edit WAV sound files with programs like Audacity. We can add sound code to other events in the program in the same way, like when a laser is fired.

More about the boss

Staying with figure2.py, note how we can use random numbers to decide when the boss becomes active and also when the boss fires a laser. You can change the parameters of the randint() function to alter the occurrence of these events. You can also see that we have a simple path calculating system for the boss to make it move diagonally down the screen. We use the level variable to alter aspects of the movement. We treat the boss lasers in the same way as the normal alien lasers, but we need to have a check to see if the boss is hit by a player laser. We do this by adding a check to our checkPlayerLaserHit() function.

Three strikes and you’re out

In the previous episode, the game ended if you were hit by a laser. In this version we have three chances before the game ends, and when it does, we want to display a high score table or leader board to be updated from one player to the next. There are a few considerations to think about here. We need a separate screen for our leader board; we need to get players to enter their name to put against each score and we will have to save the score information. In other programs in this series we have used the variable gameStatus to control different screens, so let’s bring that back for this program.

Screen switching with gameStatus

We will need three states for the gameStatus variable. If it is set to 0 then we should display an intro screen where we can get the player to type in their name. If it is set to 1 then we want to run code for playing the game. And if it is set to 2 then we display the leader-board page. Let’s first deal with the intro screen. Having set our variable to 0 at the top of the code, we need to add a condition to our draw() function: if gameStatus == 0:. Then, under that, use drawCentreText() to show some intro text and display the player.name string. To start with, player.name will be blank.

A name is just a name

Now to respond to the player typing their name into the intro screen. We will write a very simple input routine and put it in the built-in Pygame Zero function on_key_down(). figure3.py shows how we do this. With this code, if the player presses a key, the name of the key is added to the player.name string unless the key is the BACKSPACE key, in which case we remove the last character. Notice the rather cunning way of doing that with player.name = player.name[:-1]. We also ignore the RETURN key, as we can deal with that in our update() function.

Game on

When the player has entered their name on the intro screen, all we need to do is detect a press of the RETURN key in our update() function and we can switch to the game part. We can easily do this by just writing if gameStatus == 0: and then under that, if keyboard.RETURN and player.name != "": gameStatus = 1. We will also now need to put our main game update code under a condition, if gameStatus == 1:. We will also need to have the same condition in the draw() function. Once this is done, we have a system for switching from intro screen to game screen.

Leader of the pack

So now we come to our leader-board screen. It will be triggered when the player loses the third life. When that happens, we set gameStatus to 2 and put a condition in our draw() and update() functions to react to that. When we switch to our leader board, we need to display the high score list – so, we can write in our draw() function: if gameStatus == 2: drawHighScore(). Going back to figure1.py, you’ll see that we left a section at the end commented out, ready for the leader board. We can now fill this in with some code.

If only I learned to read and write

We are going to save all our scores in a file so that we can get them back each time the game is played. We can use a simple text file for this. When a new score is available, we will have to read the old score list in, add our new score to the list, sort the scores into the correct order, and then save the scores back out to create an updated file. So, the code we need to write in our update() function will be to call a readHighScore() function, set our gameStatus to 2, and call a writeHighScore() function.

Functions need to function

We have named three functions that need writing in the last couple of steps: drawHighScore(), readHighScore(), and writeHighScore().Have a look at figure4.py to see the code that we need in these functions. The file reading and writing are standard Python functions. When reading, we create a list of entries and add each line to a list. We then sort the list into highest-score-first order. When we write the file, we just write each list item to the file. To draw the leader board, we just run through the high-score list that we have sorted and draw the lines of text to the screen.  All the aliens have been destroyed. It’s time to move up a level

Sort it out

It’s worth mentioning the way we are sorting the high scores. In figure4.py we are adding a key sorting method to the list sorting function. We do this because the list is a string but we want to sort by the high score, which is numerical, so we break up the string and convert it to an integer and sort based on that value rather than the string. If we didn’t do this and sorted as a string then all the scores starting with 9 would come first, then all the 8s, then all the 7s and so on, with 9000 being shown before 80 000, which would be wrong.
def readHighScore():
    global highScore, score, player
    highScore = []
    try:
        hsFile = open("highscores.txt", "r")
        for line in hsFile:
            highScore.append(line.rstrip())
    except:
        pass
    highScore.append(str(score)+ " " + player.name)
    highScore.sort(key=natural_key, reverse=True)

def natural_key(string_):
    return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_)]

def writeHighScore():
    global highScore
    hsFile = open("highscores.txt", "w")
    for line in highScore:
        hsFile.write(line + "\n")

def drawHighScore():
    global highScore
    y = 0
    screen.draw.text("TOP SCORES", midtop=(400, 30), owidth=0.5, ocolor=(255,255,255), color=(0,64,255) , fontsize=60)
    for line in highScore:
        if y < 400:
            screen.draw.text(line, midtop=(400, 100+y), owidth=0.5, ocolor=(0,0,255), color=(255,255,0) , fontsize=50)
            y += 50
    screen.draw.text("Press Escape to play again" , center=(400, 550), owidth=0.5, ocolor=(255,255,255), color=(255,64,0) , fontsize=60)

Well, that’s all folks

That’s about all we need for our Pygame Zero Invaders game other than all the additions that you could make to it. For example, you could have different graphics for each row of aliens. We’re sure you can improve on the sounds that we have supplied, and there are many ways that the level variable can be worked into the code to make the different levels more difficult or more varied.

import pgzrun, math, re, time
from random import randint
player = Actor("player", (400, 550))
boss = Actor("boss")
gameStatus = 0
highScore = []

def draw(): # Pygame Zero draw function
    screen.blit('background', (0, 0))
    if gameStatus == 0: # display the title page
        drawCentreText("PYGAME ZERO INVADERS\n\n\nType your name then\npress Enter to start\n(arrow keys move, space to fire)")
        screen.draw.text(player.name , center=(400, 500), owidth=0.5, ocolor=(255,0,0), color=(0,64,255) , fontsize=60)
    if gameStatus == 1: # playing the game
        player.image = 
player.images[math.floor(player.status/6)]
        player.draw()
        if boss.active: boss.draw()
        drawLasers()
        drawAliens()
        drawBases()
        screen.draw.text(str(score) , 
topright=(780, 10), owidth=0.5, ocolor=(255,255,255), color=(0,64,255) , fontsize=60)
        screen.draw.text("LEVEL " + str(level) , midtop=(400, 10), owidth=0.5, ocolor=(255,255,255), color=(0,64,255) , fontsize=60)
        drawLives()
        if player.status >= 30:
            if player.lives > 0:
                drawCentreText(
"YOU WERE HIT!\nPress Enter to re-spawn")
            else:
                drawCentreText(
"GAME OVER!\nPress Enter to continue")
        if len(aliens) == 0 :
            drawCentreText("LEVEL CLEARED!\nPress Enter to go to the next level")
    if gameStatus == 2: # game over show the leaderboard
        drawHighScore()

def drawCentreText(t):
    screen.draw.text(t , center=(400, 300), owidth=0.5, ocolor=(255,255,255), color=(255,64,0) , fontsize=60)
    
def update(): # Pygame Zero update function
    global moveCounter, player, gameStatus, lasers, level, boss
    if gameStatus == 0:
        if keyboard.RETURN and player.name != "":
            gameStatus = 1
    if gameStatus == 1:
        if player.status < 30 and len(aliens) > 0:
            checkKeys()
            updateLasers()
            updateBoss()
            if moveCounter == 0: updateAliens()
            moveCounter += 1
            if moveCounter == moveDelay:
                moveCounter = 0
            if player.status > 0:
                player.status += 1
                if player.status == 30:
                    player.lives -= 1
        else:
            if keyboard.RETURN:
                if player.lives > 0:
                    player.status = 0
                    lasers = []
                    if len(aliens) == 0:
                        level += 1
                        boss.active = False
                        initAliens()
                        initBases()
                else:
                    readHighScore()
                    gameStatus = 2
                    writeHighScore()
    if gameStatus == 2:
        if keyboard.ESCAPE:
            init()
            gameStatus = 0
            
def on_key_down(key):
    global player
    if gameStatus == 0 and key.name != "RETURN":
        if len(key.name) == 1:
            player.name += key.name
        else:
            if key.name == "BACKSPACE":
                player.name = player.name[:-1]
    
def readHighScore():
    global highScore, score, player
    highScore = []
    try:
        hsFile = open("highscores.txt", "r")
        for line in hsFile:
            highScore.append(line.rstrip())
    except:
        pass
    highScore.append(str(score)+ " " + 
player.name)
    highScore.sort(key=natural_key, reverse=True)

def natural_key(string_):
    return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_)]

def writeHighScore():
    global highScore
    hsFile = open("highscores.txt", "w")
    for line in highScore:
        hsFile.write(line + "\n")

def drawHighScore():
    global highScore
    y = 0
    screen.draw.text("TOP SCORES", midtop=
(400, 30), owidth=0.5, ocolor=(255,255,255), color=(0,64,255) , fontsize=60)
    for line in highScore:
        if y < 400:
            screen.draw.text(line, midtop=
(400, 100+y), owidth=0.5, ocolor=(0,0,255), color=(255,255,0) , fontsize=50)
            y += 50
    screen.draw.text(
"Press Escape to play again" , center=
(400, 550), owidth=0.5, ocolor=(255,255,255), color=(255,64,0) , fontsize=60)

def drawLives():
    for l in range(player.lives):
        screen.blit("life", (10+(l*32),10))

def drawAliens():
    for a in range(len(aliens)): aliens[a].draw()

def drawBases():
    for b in range(len(bases)):
        bases[b].drawClipped()

def drawLasers():
    for l in range(len(lasers)): lasers[l].draw()

def checkKeys():
    global player, score
    if keyboard.left:
        if player.x > 40: player.x -= 5
    if keyboard.right:
        if player.x < 760: player.x += 5
    if keyboard.space:
        if player.laserActive == 1:
            sounds.gun.play()
            player.laserActive = 0
            clock.schedule(makeLaserActive, 1.0)
            lasers.append(Actor("laser2", (player.x,player.y-32)))
            lasers[len(lasers)-1].status = 0
            lasers[len(lasers)-1].type = 1
            score -= 100

def makeLaserActive():
    global player
    player.laserActive = 1
    
def checkBases():
    for b in range(len(bases)):
        if l < len(bases):
            if bases[b].height < 5:
                del bases[b]

def updateLasers():
    global lasers, aliens
    for l in range(len(lasers)):
        if lasers[l].type == 0:
            lasers[l].y += 2
            checkLaserHit(l)
            if lasers[l].y > 600:
                lasers[l].status = 1
        if lasers[l].type == 1:
            lasers[l].y -= 5
            checkPlayerLaserHit(l)
            if lasers[l].y < 10:
                lasers[l].status = 1
    lasers = listCleanup(lasers)
    aliens = listCleanup(aliens)

def listCleanup(l):
    newList = []
    for i in range(len(l)):
        if l[i].status == 0:
            newList.append(l[i])
    return newList
    
def checkLaserHit(l):
    global player
    if player.collidepoint((lasers[l].x, lasers[l].y)):
        sounds.explosion.play()
        player.status = 1
        lasers[l].status = 1
    for b in range(len(bases)):
        if bases[b].collideLaser(lasers[l]):
            bases[b].height -= 10
            lasers[l].status = 1

def checkPlayerLaserHit(l):
    global score, boss
    for b in range(len(bases)):
        if bases[b].collideLaser(lasers[l]):
            lasers[l].status = 1
    for a in range(len(aliens)):
        if aliens[a].collidepoint((lasers[l].x, lasers[l].y)):
            lasers[l].status = 1
            aliens[a].status = 1
            score += 1000
    if boss.active:
        if boss.collidepoint((lasers[l].x, lasers[l].y)):
            lasers[l].status = 1
            boss.active = 0
            score += 5000
        
def updateAliens():
    global moveSequence, lasers, moveDelay
    movex = movey = 0
    if moveSequence < 10 or moveSequence > 30:
        movex = -15
    if moveSequence == 10 or moveSequence == 30:
        movey = 40 + (5*level)
        moveDelay -= 1
    if moveSequence >10 and moveSequence < 30:
        movex = 15
    for a in range(len(aliens)):
        animate(aliens[a], pos=(aliens[a].x + movex, aliens[a].y + movey), duration=0.5, tween='linear')
        if randint(0, 1) == 0:
            aliens[a].image = "alien1"
        else:
            aliens[a].image = "alien1b"
            if randint(0, 5) == 0:
                lasers.append(Actor("laser1", (aliens[a].x,aliens[a].y)))
                lasers[len(lasers)-1].status = 0
                lasers[len(lasers)-1].type = 0
                sounds.laser.play()
        if aliens[a].y > 500 and player.status == 0:
            sounds.explosion.play()
            player.status = 1
            player.lives = 1
    moveSequence +=1
    if moveSequence == 40: moveSequence = 0

def updateBoss():
    global boss, level, player, lasers
    if boss.active:
        boss.y += (0.3*level)
        if boss.direction == 0:
            boss.x -= (1* level)
        else: boss.x += (1* level)
        if boss.x < 100: boss.direction = 1
        if boss.x > 700: boss.direction = 0
        if boss.y > 500:
            sounds.explosion.play()
            player.status = 1
            boss.active = False
        if randint(0, 30) == 0:
            lasers.append(Actor("laser1", (boss.x,boss.y)))
            lasers[len(lasers)-1].status = 0
            lasers[len(lasers)-1].type = 0
    else:
        if randint(0, 800) == 0:
            boss.active = True
            boss.x = 800
            boss.y = 100
            boss.direction = 0

def init():
    global lasers, score, player, moveSequence, moveCounter, moveDelay, level, boss
    initAliens()
    initBases()
    moveCounter = moveSequence = player.status = score = player.laserCountdown = 0
    lasers = []
    moveDelay = 30
    boss.active = False
    player.images = ["player","explosion1","explosion2","explosion3", "explosion4","explosion5"]
    player.laserActive = 1
    player.lives = 3
    player.name = ""
    level = 1

def initAliens():
    global aliens, moveCounter, moveSequence
    aliens = []
    moveCounter = moveSequence = 0
    for a in range(18):
        aliens.append(Actor("alien1", (210+
(a % 6)*80,100+(int(a/6)*64))))
        aliens[a].status = 0

def drawClipped(self):
    screen.surface.blit(self._surf, (self.x-32, self.y-self.height+30),(0,0,64,self.height))

def collideLaser(self, other):
    return (
        self.x-20 < other.x+5 and
        self.y-self.height+30 < other.y and
        self.x+32 > other.x+5 and
        self.y-self.height+30 + self.height > other.y
    )

def initBases():
    global bases
    bases = []
    bc = 0
    for b in range(3):
        for p in range(3):
            bases.append(Actor("base1", midbottom=(150+(b*200)+(p*40),520)))
            bases[bc].drawClipped = 
drawClipped.__get__(bases[bc])
            bases[bc].collideLaser = collideLaser.__get__(bases[bc])
            bases[bc].height = 60
            bc +=1
    
init()
pgzrun.go()

 

From The MagPi store

Subscribe

Subscribe to the newsletter

Get every issue delivered directly to your inbox and keep up to date with the latest news, offers, events, and more.