Game Making with Python and Pygame Part 10

in #python5 years ago

Adding a Score Timer and Finishing off the Game

Game Making with Python and Pygame Part 10

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


We're going to start by creating a score attribute in the Player class that we'll then access and display from the MainWindow in exactly the same way as we did with the lives variable. At the moment this won't update at all, as we haven't added the code to do that yet. The full code along with the parts we've just added that make the score appear looks 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
        self.score = 0

    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 10')

        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_lives()
            self.draw_score()
            self.aliens.draw(self.DISPLAYSURF)
            self.player.draw(self.DISPLAYSURF)

            pygame.display.update()

    def draw_lives(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)

    def draw_score(self):
        the_score, the_score_rect = self.font.render("Score: " + str(self.player.score), (255, 255, 255))
        the_score_rect.topleft = 20, 20
        self.DISPLAYSURF.blit(the_score, the_score_rect)

game = MainWindow()
game.main_game_loop()

We need to add in a way to update the score. As discussed previously we're going to add a timer and update the score with the value of it. First off we'll add a method to the Player class to add a value (some time) onto the value of the score, we could do this directly in the MainWindow class just by adding a value to the score attribute, but by making a suitable named method we can communicate our intentions much more clearly. Here's the updated version of the Player class:

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

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

    def add_to_score(self, some_time):
        self.score = self.score + some_time

    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 then need to add some code to the main_game_loop() so that it tracks how much time we've been playing and updates the score on each iteration. Since we already have the self.clock.tick(60) call in the main_game_loop() we can just call get_time() on our clock and it will give us the number of milliseconds that have elapsed between the last two calls to tick(). Here's the updated version of the main_game_loop() method:

    def main_game_loop(self):
        while True:
            self.clock.tick(60)
            self.player.add_to_score(self.clock.get_time())
            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_lives()
            self.draw_score()
            self.aliens.draw(self.DISPLAYSURF)
            self.player.draw(self.DISPLAYSURF)

            pygame.display.update()

Running the code should show the score being updated in the top corner up to the point where we lose the game. We're going to make one final set of changes to the game, so that when we lose, we show a Game Over screen which also has our score on it. Clicking in that window will then let us start a new game.

To do that we need to move some of our code out of the main loop and into its own method, we'll do this so that we can create one method to hold the code when we're playing the game and another for when we're in the Game Over screen. First we add a game_over attribute to the MainWindow constructor and set it to False. We also add the method to draw the Game Over screen and move some of the other parts around to make things work. Here's the updated MainWindow class:

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 10')

        self.clock = pygame.time.Clock()
        self.game_over = False

    def main_game_loop(self):
        while True:
            if not self.game_over:
                self.clock.tick(60)
                self.player.add_to_score(self.clock.get_time())
                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.display_game()
            else:
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        pygame.quit()
                        sys.exit()
                self.display_game_over()

            pygame.display.update()

    def display_game_over(self):
        self.DISPLAYSURF.fill((0, 0, 0))
        the_games_over, the_games_over_rect = self.font.render("Game Over!", (255, 255, 255))
        the_games_over_rect.center = WIDTH/2, HEIGHT/2 - 40
        the_score, the_score_rect = self.font.render("You scored: " + str(self.player.score), (255, 255, 255))
        the_score_rect.center = WIDTH/2, HEIGHT/2
        reset_me, reset_me_rect = self.font.render("Click to restart the game.", (255, 255, 255))
        reset_me_rect.center = WIDTH/2, HEIGHT/2 + 40
        self.DISPLAYSURF.blit(the_games_over, the_games_over_rect)
        self.DISPLAYSURF.blit(the_score, the_score_rect)
        self.DISPLAYSURF.blit(reset_me, reset_me_rect)

    def display_game(self):
        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():
                self.game_over = True
        self.draw_lives()
        self.draw_score()
        self.aliens.draw(self.DISPLAYSURF)
        self.player.draw(self.DISPLAYSURF)

    def draw_lives(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)

    def draw_score(self):
        the_score, the_score_rect = self.font.render("Score: " + str(self.player.score), (255, 255, 255))
        the_score_rect.topleft = 20, 20
        self.DISPLAYSURF.blit(the_score, the_score_rect)

We had to include the event loop again in the second half of the if statement in the main_game_loop() as otherwise there is no way to exit the window. Running that you should see everything working apart from making the game restart. If we add a check to the event queue when the game is over we can make it restart by also adding a reset() method to create new copies of the Player and Alien objects. Here's the updated MainWindow class:

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 10')

        self.clock = pygame.time.Clock()
        self.game_over = False

    def reset(self):
        self.player = Player(50, 50)
        self.aliens = pygame.sprite.Group()
        for _ in range(20):
            self.aliens.add(Alien())

    def main_game_loop(self):
        while True:
            if not self.game_over:
                self.clock.tick(60)
                self.player.add_to_score(self.clock.get_time())
                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.display_game()
            else:
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        pygame.quit()
                        sys.exit()
                    if event.type == pygame.MOUSEBUTTONDOWN:
                        self.reset()
                        self.game_over = False
                self.display_game_over()

            pygame.display.update()

    def display_game_over(self):
        self.DISPLAYSURF.fill((0, 0, 0))
        the_games_over, the_games_over_rect = self.font.render("Game Over!", (255, 255, 255))
        the_games_over_rect.center = WIDTH/2, HEIGHT/2 - 40
        the_score, the_score_rect = self.font.render("You scored: " + str(self.player.score), (255, 255, 255))
        the_score_rect.center = WIDTH/2, HEIGHT/2
        reset_me, reset_me_rect = self.font.render("Click to restart the game.", (255, 255, 255))
        reset_me_rect.center = WIDTH/2, HEIGHT/2 + 40
        self.DISPLAYSURF.blit(the_games_over, the_games_over_rect)
        self.DISPLAYSURF.blit(the_score, the_score_rect)
        self.DISPLAYSURF.blit(reset_me, reset_me_rect)

    def display_game(self):
        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():
                self.game_over = True
        self.draw_lives()
        self.draw_score()
        self.aliens.draw(self.DISPLAYSURF)
        self.player.draw(self.DISPLAYSURF)

    def draw_lives(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)

    def draw_score(self):
        the_score, the_score_rect = self.font.render("Score: " + str(self.player.score), (255, 255, 255))
        the_score_rect.topleft = 20, 20
        self.DISPLAYSURF.blit(the_score, the_score_rect)

Here's the whole game program:

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
        self.score = 0

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

    def add_to_score(self, some_time):
        self.score = self.score + some_time

    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 10')

        self.clock = pygame.time.Clock()
        self.game_over = False

    def reset(self):
        self.player = Player(50, 50)
        self.aliens = pygame.sprite.Group()
        for _ in range(20):
            self.aliens.add(Alien())

    def main_game_loop(self):
        while True:
            if not self.game_over:
                self.clock.tick(60)
                self.player.add_to_score(self.clock.get_time())
                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.display_game()
            else:
                for event in pygame.event.get():
                    if event.type == pygame.QUIT:
                        pygame.quit()
                        sys.exit()
                    if event.type == pygame.MOUSEBUTTONDOWN:
                        self.reset()
                        self.game_over = False
                self.display_game_over()

            pygame.display.update()

    def display_game_over(self):
        self.DISPLAYSURF.fill((0, 0, 0))
        the_games_over, the_games_over_rect = self.font.render("Game Over!", (255, 255, 255))
        the_games_over_rect.center = WIDTH/2, HEIGHT/2 - 40
        the_score, the_score_rect = self.font.render("You scored: " + str(self.player.score), (255, 255, 255))
        the_score_rect.center = WIDTH/2, HEIGHT/2
        reset_me, reset_me_rect = self.font.render("Click to restart the game.", (255, 255, 255))
        reset_me_rect.center = WIDTH/2, HEIGHT/2 + 40
        self.DISPLAYSURF.blit(the_games_over, the_games_over_rect)
        self.DISPLAYSURF.blit(the_score, the_score_rect)
        self.DISPLAYSURF.blit(reset_me, reset_me_rect)

    def display_game(self):
        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():
                self.game_over = True
        self.draw_lives()
        self.draw_score()
        self.aliens.draw(self.DISPLAYSURF)
        self.player.draw(self.DISPLAYSURF)

    def draw_lives(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)

    def draw_score(self):
        the_score, the_score_rect = self.font.render("Score: " + str(self.player.score), (255, 255, 255))
        the_score_rect.topleft = 20, 20
        self.DISPLAYSURF.blit(the_score, the_score_rect)


game = MainWindow()
game.main_game_loop()

Here's a picture of it in action:

Finished Game
Image by the Author - Amos1969 - Dave A

Whilst it's not the greatest game in the world it does fit into less that 180 lines of code, and is playable (just about). I intend to carry on with this series but will be starting to make a Top Down Shooter game (something in the vein of Space Invaders or Galaxians) in the next part.


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

Part 9 - Adding lives and a game over screen: Part 9

Coin Marketplace

STEEM 0.30
TRX 0.11
JST 0.033
BTC 64243.42
ETH 3152.93
USDT 1.00
SBD 4.28