Game Making with Python and Pygame Part 9

in #python5 years ago

Adding Lives and a Score Timer to the Game

Game Making with Python and Pygame Part 9

See the previous parts linked at the bottom of this post if you haven't already.


In the previous part, we simplified our code somewhat by making the Player and Alien classes ultimately inherit from the Pygame Sprite class. In this part, we're going to add in some text on the screen to display the number of lives. In this kind of video game, these details are sometimes referred to as the HUD (short for Heads Up Display).

The code from the previous instalment looks like this:

import sys
import pygame
import random

WIDTH = 800
HEIGHT = 600

class GameObject(pygame.sprite.Sprite):
    def __init__(self, image, x=0, y=0):
        pygame.sprite.Sprite.__init__(self)
        if x == 0 and y ==0:
            x = random.randint(0, WIDTH - 100)
            y = random.randint(0, HEIGHT - 100)
        self.image = pygame.image.load(image)
        self.rect = self.image.get_rect()
        self.rect.topleft = (x, y)

    def colliding(self, things):
        # This may be bad form and non-pythonic, not sure if I should have self
        # as an argument to the function call, or if there's a different way
        # of getting it to do this which is better practice.
        if pygame.sprite.spritecollideany(self, things):
            return True
        return False

class Alien(GameObject):
    def __init__(self, x=0, y=0):
        super().__init__("alien.png", x, y)
        self.velocity = [random.randint(-7, 7), random.randint(-7, 7)]

    def update(self):
        self.rect = self.rect.move(self.velocity)

        if self.rect.left < 0 or self.rect.right > WIDTH:
            self.velocity[0] = -self.velocity[0]
        if self.rect.top < 0 or self.rect.bottom > HEIGHT:
            self.velocity[1] = -self.velocity[1]

class Player(GameObject):
    def __init__(self, x=0, y=0):
        super().__init__("player.png", x, y)

    def reset(self):
        self.rect.topleft = (50, 50)

    def move_right(self):
        self.rect = self.rect.move((1, 0))
        if self.rect.right > WIDTH:
            self.rect.right = WIDTH

    def move_left(self):
        self.rect = self.rect.move((-1, 0))
        if self.rect.left < 0:
            self.rect.left = 0

    def move_up(self):
        self.rect = self.rect.move((0, -1))
        if self.rect.top < 0:
            self.rect.top = 0

    def move_down(self):
        self.rect = self.rect.move((0, 1))
        if self.rect.bottom > HEIGHT:
            self.rect.bottom = HEIGHT

    def draw(self, a_surface):
        a_surface.blit(self.image, self.rect)

class MainWindow:
    def __init__(self):
        pygame.init()
        pygame.key.set_repeat(3, 3)
        self.player = Player(50, 50)

        self.aliens = pygame.sprite.Group()
        for _ in range(20):
            self.aliens.add(Alien())

        self.DISPLAYSURF = pygame.display.set_mode((WIDTH, HEIGHT))
        self.purple = (150, 0, 220)
        self.DISPLAYSURF.fill(self.purple)
        pygame.display.set_caption('Dodge the Aliens - Game Making Part 8')

        self.clock = pygame.time.Clock()

    def main_game_loop(self):
        while True:
            self.clock.tick(60)
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_RIGHT:
                        self.player.move_right()
                    if event.key == pygame.K_LEFT:
                        self.player.move_left()
                    if event.key == pygame.K_UP:
                        self.player.move_up()
                    if event.key == pygame.K_DOWN:
                        self.player.move_down()

            self.DISPLAYSURF.fill(self.purple)

            self.aliens.update()
            if self.player.colliding(self.aliens):
                self.player.reset()

            self.aliens.draw(self.DISPLAYSURF)
            self.player.draw(self.DISPLAYSURF)

            pygame.display.update()

game = MainWindow()
game.main_game_loop()

Essentially we're at a point where our game needs to start and end at given times, for a reason. In other words our game will start when we run the code, and then end when we run out of lives. We're going to give the player 5 lives to begin with, each time they collide with an Alien they lose a life, when the lives run out the game is over. Once we have this part working we'll move on to add a timer, which will be how scores are obtained.

To begin with then we're going to add a lives attribute to the game and every time we collide with an Alien we'll deduct a life. At the end of each time through the loop we'll check if the score is less than 1, and if it is, we'll call the break command which will exit our main loop and finish the program for us.

class MainWindow:
    def __init__(self):
        pygame.init()
        pygame.key.set_repeat(3, 3)
        self.player = Player(50, 50)

        self.aliens = pygame.sprite.Group()
        for _ in range(20):
            self.aliens.add(Alien())

        self.DISPLAYSURF = pygame.display.set_mode((WIDTH, HEIGHT))
        self.purple = (150, 0, 220)
        self.DISPLAYSURF.fill(self.purple)
        pygame.display.set_caption('Dodge the Aliens - Game Making Part 9')

        self.lives = 5
        self.clock = pygame.time.Clock()

    def main_game_loop(self):
        while True:
            self.clock.tick(60)
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_RIGHT:
                        self.player.move_right()
                    if event.key == pygame.K_LEFT:
                        self.player.move_left()
                    if event.key == pygame.K_UP:
                        self.player.move_up()
                    if event.key == pygame.K_DOWN:
                        self.player.move_down()

            self.DISPLAYSURF.fill(self.purple)

            self.aliens.update()
            if self.player.colliding(self.aliens):
                self.player.reset()
                self.lives = self.lives - 1
                if self.lives < 1:
                    break
            self.aliens.draw(self.DISPLAYSURF)
            self.player.draw(self.DISPLAYSURF)

            pygame.display.update()

We add a lives attribute in the constructor and then modify it in the main_game_loop this works, but it would probably be better from a programming point of view if the Player object handles the lives and we call a method to modify it and check if there are still any left. So we'll change the Player class accordingly and then modify the MainWindow class to make use of this.

The Player class looks like this:

class Player(GameObject):
    def __init__(self, x=0, y=0):
        super().__init__("player.png", x, y)
        self.lives = 5

    def lose_a_life(self):
        self.lives -= 1

    def is_dead(self):
        if self.lives < 1:
            return True
        return False

    def reset(self):
        self.rect.topleft = (50, 50)

    def move_right(self):
        self.rect = self.rect.move((1, 0))
        if self.rect.right > WIDTH:
            self.rect.right = WIDTH

    def move_left(self):
        self.rect = self.rect.move((-1, 0))
        if self.rect.left < 0:
            self.rect.left = 0

    def move_up(self):
        self.rect = self.rect.move((0, -1))
        if self.rect.top < 0:
            self.rect.top = 0

    def move_down(self):
        self.rect = self.rect.move((0, 1))
        if self.rect.bottom > HEIGHT:
            self.rect.bottom = HEIGHT

    def draw(self, a_surface):
        a_surface.blit(self.image, self.rect)

We've added the lives attribute to the constructor and then two new methods, lose_a_life and is_dead, which hopefully are fairly self explanatory.

Then we modify the MainWindow class removing the lives attribute and changing the main_game_loop to use the methods to update and check the Player's status.

class MainWindow:
    def __init__(self):
        pygame.init()
        pygame.key.set_repeat(3, 3)
        self.player = Player(50, 50)

        self.aliens = pygame.sprite.Group()
        for _ in range(20):
            self.aliens.add(Alien())

        self.DISPLAYSURF = pygame.display.set_mode((WIDTH, HEIGHT))
        self.purple = (150, 0, 220)
        self.DISPLAYSURF.fill(self.purple)
        pygame.display.set_caption('Dodge the Aliens - Game Making Part 9')

        self.clock = pygame.time.Clock()

    def main_game_loop(self):
        while True:
            self.clock.tick(60)
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_RIGHT:
                        self.player.move_right()
                    if event.key == pygame.K_LEFT:
                        self.player.move_left()
                    if event.key == pygame.K_UP:
                        self.player.move_up()
                    if event.key == pygame.K_DOWN:
                        self.player.move_down()

            self.DISPLAYSURF.fill(self.purple)

            self.aliens.update()
            if self.player.colliding(self.aliens):
                self.player.reset()
                self.player.lose_a_life()
                if self.player.is_dead():
                    break
            self.aliens.draw(self.DISPLAYSURF)
            self.player.draw(self.DISPLAYSURF)

            pygame.display.update()

If we now run the code, it works in exactly the same way as it did previously, but now when we read the code in the main_game_loop it is clearer what our intention is as we have named the methods appropriately. This is something that it is a good idea to do as it makes it easier for others (and ourselves) to understand what is going on (or what is supposed to be going on).

We're now going to look at how we can display the lives on the screen. To use fonts we're going to make use of the freetype module that's part of Pygame, but we'll need to import it separately, so we need to add import pygame.freetype to our list of imports. We also need to initialise the module, but our call to pygame.init() will initialise it for us so we don't need to make a separate call to it.

Basically we tell it which font and what size we want that font to be in the constructor. We use self.font = pygame.freetype.SysFont("FreeSans", 30) to create a font attribute for the MainWindow class that uses the FreeSans font type which is available on my Linux box, you may need to change this to one that is available. The 30 is to tell it to make the font 30 pixels in size.

We then add a method call to our main_game_loop that's called draw_score which deals with putting the text on the screen. This makes the MainWindow class look like this:

class MainWindow:
    def __init__(self):
        pygame.init()
        pygame.key.set_repeat(3, 3)
        self.player = Player(50, 50)

        self.font = pygame.freetype.SysFont("FreeSans", 30)

        self.aliens = pygame.sprite.Group()
        for _ in range(20):
            self.aliens.add(Alien())

        self.DISPLAYSURF = pygame.display.set_mode((WIDTH, HEIGHT))
        self.purple = (150, 0, 220)
        self.DISPLAYSURF.fill(self.purple)
        pygame.display.set_caption('Dodge the Aliens - Game Making Part 9')

        self.clock = pygame.time.Clock()

    def main_game_loop(self):
        while True:
            self.clock.tick(60)
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_RIGHT:
                        self.player.move_right()
                    if event.key == pygame.K_LEFT:
                        self.player.move_left()
                    if event.key == pygame.K_UP:
                        self.player.move_up()
                    if event.key == pygame.K_DOWN:
                        self.player.move_down()

            self.DISPLAYSURF.fill(self.purple)

            self.aliens.update()
            if self.player.colliding(self.aliens):
                self.player.reset()
                self.player.lose_a_life()
                if self.player.is_dead():
                    break
            self.draw_score()
            self.aliens.draw(self.DISPLAYSURF)
            self.player.draw(self.DISPLAYSURF)

            pygame.display.update()

    def draw_score(self):
        the_lives, the_lives_rect = self.font.render("Lives: " + str(self.player.lives), (255, 255, 255))
        the_lives_rect.topright = WIDTH-20, 20
        self.DISPLAYSURF.blit(the_lives, the_lives_rect)

This makes our whole program look like this:

import sys
import pygame
import pygame.freetype
import random

WIDTH = 800
HEIGHT = 600

class GameObject(pygame.sprite.Sprite):
    def __init__(self, image, x=0, y=0):
        pygame.sprite.Sprite.__init__(self)
        if x == 0 and y ==0:
            x = random.randint(0, WIDTH - 100)
            y = random.randint(0, HEIGHT - 100)
        self.image = pygame.image.load(image)
        self.rect = self.image.get_rect()
        self.rect.topleft = (x, y)

    def colliding(self, things):
        # This may be bad form and non-pythonic, not sure if I should have self 
        # as an argument to the function call, or if there's a different way
        # of getting it to do this which is better practice.
        if pygame.sprite.spritecollideany(self, things):
            return True
        return False

class Alien(GameObject):
    def __init__(self, x=0, y=0):
        super().__init__("alien.png", x, y)
        self.velocity = [random.randint(-7, 7), random.randint(-7, 7)]

    def update(self):
        self.rect = self.rect.move(self.velocity)

        if self.rect.left < 0 or self.rect.right > WIDTH:
            self.velocity[0] = -self.velocity[0]
        if self.rect.top < 0 or self.rect.bottom > HEIGHT:
            self.velocity[1] = -self.velocity[1]

class Player(GameObject):
    def __init__(self, x=0, y=0):
        super().__init__("player.png", x, y)
        self.lives = 5

    def lose_a_life(self):
        self.lives -= 1

    def is_dead(self):
        if self.lives < 1:
            return True
        return False

    def reset(self):
        self.rect.topleft = (50, 50)

    def move_right(self):
        self.rect = self.rect.move((1, 0))
        if self.rect.right > WIDTH:
            self.rect.right = WIDTH

    def move_left(self):
        self.rect = self.rect.move((-1, 0))
        if self.rect.left < 0:
            self.rect.left = 0

    def move_up(self):
        self.rect = self.rect.move((0, -1))
        if self.rect.top < 0:
            self.rect.top = 0

    def move_down(self):
        self.rect = self.rect.move((0, 1))
        if self.rect.bottom > HEIGHT:
            self.rect.bottom = HEIGHT

    def draw(self, a_surface):
        a_surface.blit(self.image, self.rect)

class MainWindow:
    def __init__(self):
        pygame.init()
        pygame.key.set_repeat(3, 3)
        self.player = Player(50, 50)

        self.font = pygame.freetype.SysFont("FreeSans", 30)

        self.aliens = pygame.sprite.Group()
        for _ in range(20):
            self.aliens.add(Alien())

        self.DISPLAYSURF = pygame.display.set_mode((WIDTH, HEIGHT))
        self.purple = (150, 0, 220)
        self.DISPLAYSURF.fill(self.purple)
        pygame.display.set_caption('Dodge the Aliens - Game Making Part 9')

        self.clock = pygame.time.Clock()

    def main_game_loop(self):
        while True:
            self.clock.tick(60)
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_RIGHT:
                        self.player.move_right()
                    if event.key == pygame.K_LEFT:
                        self.player.move_left()
                    if event.key == pygame.K_UP:
                        self.player.move_up()
                    if event.key == pygame.K_DOWN:
                        self.player.move_down()

            self.DISPLAYSURF.fill(self.purple)

            self.aliens.update()
            if self.player.colliding(self.aliens):
                self.player.reset()
                self.player.lose_a_life()
                if self.player.is_dead():
                    break
            self.draw_score()
            self.aliens.draw(self.DISPLAYSURF)
            self.player.draw(self.DISPLAYSURF)

            pygame.display.update()

    def draw_score(self):
        the_lives, the_lives_rect = self.font.render("Lives: " + str(self.player.lives), (255, 255, 255))
        the_lives_rect.topright = WIDTH-20, 20
        self.DISPLAYSURF.blit(the_lives, the_lives_rect)

game = MainWindow()
game.main_game_loop()

We're going to leave it there, as getting to this point has taken longer than expected. We'll be back at some point soon with Part 10 which will contain details of displaying the score, which we'll calculate by keeping track of how long the player survives for.


Like and follow if you're enjoying these. If you have any comments or suggestions for improvements or things you'd like it do, let me know in the comments.


Previous parts:

Part 1 - How to get set up with an up-to-date version of Pygame: Part 1

Part 2 - How to make a window appear using basic code: Part 2

Part 3 - Refactoring the code into a class and adding a background image: Part 3

Part 4 - Added a moving sprite to the window: Part 4

Part 5 - Refactoring the ball sprite into a class of its own: Part 5

Part 6 - Adding player controls to our Sprite: Part 6

Part 7 - Adding alien sprites and collision detection: Part 7

Part 8 - Refactoring the Sprites to Inherit from the Pygame Sprite Class: Part 8

Coin Marketplace

STEEM 0.16
TRX 0.16
JST 0.030
BTC 58171.46
ETH 2472.55
USDT 1.00
SBD 2.42