Game Making with Python and Pygame Part 8

in #python6 years ago

Refactoring the Sprites to Inherit from the Pygame Sprite Class

Game Making with Python and Pygame Part 8

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


So following on from my realisation in the previous part that there might be built-in classes in Pygame that would make sprite handling slightly easier if I made my classes inherit from them, I went and investigated the Sprite class in Pygame. Basically all I needed to do was make my GameObject class inherit from pygame.sprite.Sprite and call pygame.sprite.Sprite__init__(self) in the GameObject constructor. For some reason calling super().__init__(self) fails but I'm not sure why, I guess I could try and find out at some point, but not today. My updated GameObject code now looks like this:

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):
        if self.rect.collidelist(things) != -1:
            return True
        return False

I've moved the draw() method to the Player class as the Aliens will all be added to a Sprite group which will do all the drawing stuff for them, and we don't need it in here anymore. I will be updating the colliding() method shortly but will come back and do that.

The Alien class definition looks like this, and hasn't changed:

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]

The slightly modified Player class looks like this:

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)

Essentially the only change to it is the draw() method which is literally the same one we had before transplanted back into here.

Finally, we come to the MainWindow class where we now create the aliens as a pygame.sprite.group and add each new one to it, in the loop. The main benefit we get from doing this is being able to simplify the part of the code in the main game loop that updates and draws them, as we don't need a loop to do either thing calling the update() method on the group automatically triggers the update() method for each sprite contained within it. If we also call the draw() method on the group, supplying a surface as an argument, then each sprite in the group is automatically blitted onto that surface (hence the reason we don't need the Aliens to have their own draw() method any more). We'll talk about the collision parts shortly.

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()
        self.time_counter = 0

    def main_game_loop(self):
        while True:
            self.time_counter += self.clock.tick()
            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()

            if self.time_counter > 15:
                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)

                self.time_counter = 0
            pygame.display.update()

If we look at the main game loop above basically call the Player's colliding() method passing it the sprite group made up of aliens to check. The updated code that the Player class inherits from an updated GameObject class looks like this:

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

The pygame.sprite.spritecollideany call takes a sprite as its first argument and a sprite group as its second if any of the sprites in the group collide then a single sprite from the group (I assume the colliding sprite but it doesn't specify that that is the case in the documentation) is returned. If nothing is colliding then None is returned. Using the truthiness of objects in Python, this allows us to just use a very simple if statement to return True if any sprite is returned and False otherwise.

The whole program 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()
        self.time_counter = 0

    def main_game_loop(self):
        while True:
            self.time_counter += self.clock.tick()
            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()

            if self.time_counter > 15:
                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)

                self.time_counter = 0
            pygame.display.update()

game = MainWindow()
game.main_game_loop()

Another change we can make is to use the clock.tick() method in a slightly different way. Rather than keeping track of when the last update was and only updating after a given amount of time. Instead we can pass the clock.tick() method a value representing the number of Frames Per Second in the main game loop, which will limit the speed of the updates to no more than the FPS value per second. Essentially we remove a few lines of code, and add in the number of FPS to the call to clock.tick(). I've gone with 60 but you may need to fiddle with the value to get something that works. All of the changes are in the MainWindow class:

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

Otherwise, nothing else is changed.

We're going to leave Part 8 there. Refactoring things has simplified some of the code we had previously created and made things easier to read and think logically about. In the next Part, we'll add some lives and a timer to the game, to turn it into something that's playable (although we may have to tinker with some things to improve playability).


Like and follow if you're enjoying these. If you have any comments or suggestions for improvements or things you'd like it to 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