Skip to content

14. Lives

In this lesson we will learn

  • how to compare two algorithms with flowcharts
  • how to change an object's image while the game is running
  • how to store images in a list and choose one with an index
  • how to reset global variables so the game can be played again

Terminology

  • life – one of a limited number of chances a player has before the game ends.
  • data structure – a way of organising and storing data so it is easy to use.
  • list – a data structure that stores items in order, which we can find by their index.
  • tuple – a data structure that groups values that belong together, such as the x and y of a coordinate.
  • dictionary – a data structure that stores values with a key for each one.
  • set – a data structure that stores a group of values with no repeats.
  • global variable – a variable that can be used anywhere in the program and keeps its value for as long as the program runs.

At the moment, hitting just one asteroid ends the game. That's a bit harsh, so the last step in building our game is to give the player lives.

Planning

What mechanism can we use for lives? The Globals variables include LIVES, which works just like SCORE.

We could show the number of lives as a number, but that's a bit boring. Instead we'll show hearts. Look in the Images/Lives_frames folder and you'll see five images, with 1 to 5 hearts. When the player loses a life, we'll change the image. How can we do that?

So far we've only used set_image in __init__, to give an object its image when it's created. But nothing stops us using set_image at other times, like when the player loses a life. Remember, we use load_image to find an image before we pass it to set_image.

Here are flowcharts of two ways we could do this:

  • Method 1 → each time LIVES changes, load the image and then set it
  • Method 2 → load all the images into a list once, then set the image when LIVES changes

Display lives flowcharts

If lives only ever go down, both methods do the same amount of work, because each number of lives is only shown once.

Method 2 is better if lives can go up as well as down. Then any number of lives might be shown several times. Method 1 would load an image every time, but Method 2 still only loads each image once.

When to use data structures

Data structures organise and store data so it's easy to use. The most common Python data structures are lists, tuples, dictionaries and sets.

Use a data structure to group values that belong together. For example, we use a tuple to group coordinates, because x and y belong together: (x, y).

We want to leave the door open for bonus lives later, so we'll use Method 2.

Lives update IPO


Coding

Create the Lives class

The hearts are part of the HUD, so the Lives class goes in Objects/Hud.py. Open it, add the highlighted code below and save it.

Objects/Hud.py
from GameFrame import TextObject, Globals, RoomObject

class Score(TextObject):
    """
    A class for displaying the current score
    """
    def __init__(self, room, x: int, y: int, text=None):
        """
        Initialises the score object
        """         
        # include attributes and methods from TextObject
        TextObject.__init__(self, room, x, y, text)

        # set values         
        self.size = 60
        self.font = 'Arial Black'
        self.colour = (255,255,255)
        self.bold = False
        self.update_text()

    def update_score(self, change):
        """
        Updates the score and redraws the text
        """
        Globals.SCORE += change
        self.text = str(Globals.SCORE)
        self.update_text()


class Lives(RoomObject):
    """
    A class for displaying the number of lives remaining
    """
    def __init__(self, room, x: int, y: int):
        """
        Initialises the lives object
        """   
        RoomObject.__init__(self, room, x, y)

        # load the lives images (1 to 5 hearts) into a list
        self.lives_icon = []
        for index in range(1, 6):
            self.lives_icon.append(self.load_image(f"Lives_frames/Lives_{index}.png"))
        self.update_image()

    def update_image(self):
        """
        Updates the number of lives on the UI
        """
        self.set_image(self.lives_icon[Globals.LIVES - 1], 125, 23)
Code explanation
  • line 1 → imports RoomObject as well, because Lives shows an image, so it's a RoomObject rather than a TextObject.
  • line 30 → defines the Lives class as a subclass of RoomObject.
  • lines 31–33 → a docstring that explains what the class is for.
  • line 34 → defines the __init__ method.
  • lines 35–37 → a docstring that explains what the method does.
  • line 38 → runs RoomObject's __init__ method.
  • line 41 → creates an empty list called lives_icon to store the images.
  • line 42 → loops through the numbers 1 to 5 (range(1, 6) stops before 6)…
  • line 43 → …and adds each image to the list. The f-string builds each file name from the number, for example Lives_frames/Lives_3.png.
  • line 44 → calls update_image to show the starting number of lives.
  • line 46 → defines the update_image method.
  • lines 47–49 → a docstring that explains what the method does.
  • line 50 → sets the image to the one that matches Globals.LIVES. List indexes start at 0, so we subtract 1: with 3 lives it shows lives_icon[2], which is Lives_3.png.

Open Objects/__init__.py, change the highlighted code below and save it.

Objects/__init__.py
1
2
3
4
5
6
7
from Objects.Title import Title
from Objects.Ship import Ship
from Objects.Zork import Zork
from Objects.Asteroid import Asteroid
from Objects.Laser import Laser
from Objects.Astronaut import Astronaut
from Objects.Hud import Score, Lives
Code explanation
  • line 7 → imports the Lives class from Hud.py as well as Score.

Add the lives to the Room

Open Rooms/GamePlay.py, add the highlighted code below and save it.

Rooms/GamePlay.py
from GameFrame import Level, Globals
from Objects.Ship import Ship
from Objects.Zork import Zork
from Objects.Hud import Score, Lives

class GamePlay(Level):
    def __init__(self, screen, joysticks):
        Level.__init__(self, screen, joysticks)

        # set background image
        self.set_background_image("background.png")

        # add objects
        self.add_room_object(Ship(self, 25, 50))
        self.add_room_object(Zork(self,1120, 50))

        # add HUD items
        self.score = Score(self, 
                           Globals.SCREEN_WIDTH/2 - 20, 20, 
                           str(Globals.SCORE))
        self.add_room_object(self.score)
        self.lives = Lives(self, Globals.SCREEN_WIDTH - 150, 20)
        self.add_room_object(self.lives)
Code explanation
  • line 4 → imports the Lives class as well as Score.
  • line 22 → creates a Lives object near the top-right of the screen and stores it in self.lives, so other objects can update it.
  • line 23 → adds the Lives object to the Room.

PRIMM

  1. Predict what you'll see in the top-right corner.
  2. Run MainController.py.
  3. Investigate: change LIVES = 3 in Globals.py to 5. What changes? Change it back when you're done.

Lose a life

Now we connect Globals.LIVES to the collision between an Asteroid and the Ship. That collision is handled in the Asteroid class, so open Objects/Asteroid.py, change the highlighted code below and save it.

Objects/Asteroid.py
    def handle_collision(self, other, other_type):
        """
        Handles the collision events for the Asteroid
        """

        if other_type == "Ship":
            self.room.delete_object(self)
            Globals.LIVES -= 1
            if Globals.LIVES > 0:
                self.room.lives.update_image()
            else:
                self.room.running = False
Code explanation
  • line 59 → deletes this asteroid, so it can't hit the ship again.
  • line 60 → takes one life off Globals.LIVES.
  • line 61 → checks if the player still has lives left…
  • line 62 → …and if so, updates the hearts on the screen…
  • line 63 → …otherwise…
  • line 64 → …ends the GamePlay Room, so the game is over.

PRIMM

  1. Predict what will happen each time an asteroid hits the ship.
  2. Run MainController.py and lose all three lives.
  3. Investigate: now press space on the welcome screen to play again. What happens?

Playing again

When the game ends, we go back to the welcome screen. But if we press space to play again, something strange happens: the screen shows five hearts, and the first asteroid that hits us ends the game. Let's work out why.

  1. Globals keeps its values for as long as the program runs, so when the new game starts, Globals.LIVES is still 0 from the last game.
  2. update_image shows self.lives_icon[Globals.LIVES - 1], which is lives_icon[-1]. In Python, index -1 is the last item in a list, so we see Lives_5.png.
  3. When an asteroid hits us, Globals.LIVES goes down to -1, which isn't greater than 0, so the game ends straight away.

The score has the same problem: it carries over from the last game. We need to reset both when a new game starts.

A new game starts when the player presses space on the welcome screen, so open Objects/Title.py, add the highlighted code below and save it.

Objects/Title.py
from GameFrame import RoomObject, Globals
import pygame

class Title(RoomObject):
    """
    The object for displaying the title
    """
    def __init__(self, room, x, y):
        RoomObject.__init__(self, room, x, y)

        # set image
        image = self.load_image("Title.png")
        self.set_image(image,800,350)

        # register for key events
        self.handle_key_events = True

    def key_pressed(self, key):
        """
        If the key pressed is space the game will start
        """

        if key[pygame.K_SPACE]:
            # reset the score and lives for a new game
            Globals.SCORE = 0
            Globals.LIVES = 3
            self.room.running = False
Code explanation
  • line 1 → imports Globals so the Title can reset the score and lives.
  • line 25 → sets the score back to 0 for the new game.
  • line 26 → sets the lives back to 3 for the new game.

PRIMM

  1. Predict what will happen when you play a second game now.
  2. Run MainController.py, lose all your lives, then press space to play again.
  3. Investigate: what would happen if we reset the score in GamePlay instead? Is there a difference?

Commit and push

  1. In GitHub Desktop, type Added lives in the Summary box.
  2. Click Commit to main.
  3. Click Push origin.

That's the core game finished. Next, in Game Design, we'll learn what makes games fun and use those ideas to improve Space Rescue.