Game Making with Python and Pygame Part 7

in #python5 years ago

Adding Alien Sprites and Collision Detection

Game Making with Python and Pygame Part7

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


In this part we're going to add some alien sprites which will move randomly and detect when they're in collision with the player. For now, if they collide we'll make it so that the player sprite resets to a starting point. In later parts we'll add in some lives as well as start and end screens to make it a complete game.

I've gone to the OpenGameArt website and downloaded an Alien Sprite Sheet that looks like this:

Alien Sprite Sheet

At a later point we'll investigate using sprite sheets for our image assets as this is something that we should do, but which I have no experience of. Instead I've cropped the first image from the sprite sheet to make an alien to use, that looks like this: Alien

Here's the code from last time, with just the Player and the MainWindow classes:

import sys
import pygame

WIDTH = 800
HEIGHT = 600

class Player:
    def __init__(self, x=0, y=0):
        self.image = pygame.image.load("player.png")
        self.image_rect = self.image.get_rect()
        self.image_rect.topleft = (x, y)

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

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

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

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

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

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

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

        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.player.draw(self.DISPLAYSURF)
                self.time_counter = 0
            pygame.display.update()

game = MainWindow()
game.main_game_loop()

We're going to add in an Alien class that will be similar to the ball class we had before, but called Alien. In fact we'll probably just copy that one in and modify it slightly, to begin with. Add in an import random line at the top of the code, then add in the following class definition for the alien:

class Alien:
    def __init__(self, x=0, y=0):
        if x == 0 and y ==0:
            x = random.randint(0, WIDTH - 100)
            y = random.randint(0, HEIGHT - 100)
        self.image = pygame.image.load("alien.png")
        self.image_rect = self.image.get_rect()
        self.image_rect.topleft = (x, y)
        self.velocity = [random.randint(-7, 7), random.randint(-7, 7)]

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

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

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

Which is literally the ball class but loading a different image. Modify the MainWindow class to create an instance of the alien class, in the constructor and to call the update and draw methods in the main game loop, as follows:

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

        self.alien = 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 7')

        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.player.draw(self.DISPLAYSURF)
                self.alien.update()
                self.alien.draw(self.DISPLAYSURF)
                self.time_counter = 0
            pygame.display.update()

I tried adding the update call outside of the check on the counter but it updated so quickly the alien was just a blur. I think there is a way of adding the timer check to the game loop directly, but I'll need to research it and add it into a later post. Run the code, you should see one alien bounce around randomly, and still be able to move the player.

We're going to refactor the Alien and Player class to both inherit from a common base class (probably called GameObject), which will have the same constructor and draw methods. Each will then have its' own methods to deal with movement (although these won't be called the same) and the constructor for each will pass the correct image to the superclass when the superclass constructors are called. One difference is that the Alien constructor also creates the velocity attribute whereas the Player doesn't have this.

class GameObject:
    def __init__(self, image, x=0, y=0):
        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.image_rect = self.image.get_rect()
        self.image_rect.topleft = (x, y)

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

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.image_rect = self.image_rect.move(self.velocity)

        if self.image_rect.left < 0 or self.image_rect.right > WIDTH:
            self.velocity[0] = -self.velocity[0]
        if self.image_rect.top < 0 or self.image_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 move_right(self):
        self.image_rect = self.image_rect.move((1, 0))
        if self.image_rect.right > WIDTH:
            self.image_rect.right = WIDTH

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

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

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

We're going to want to test whether our Player sprite and an Alien sprite are colliding (in other words whether their rects are overlapping). Pygame rects have a built-in method called colliderect() which checks if the rect that it belongs to, overlaps with a rect we pass into it. There are also methods to check if the current rect is colliding with 1 or all of the rects in a list, which is what we're probably going to want to use as it should be optimised for checking multiple things efficiently. To begin with we'll use the colliderect method and then change to the list versions afterwards.

For flexibility, we're going to implement the collision detection in the superclass (GameObject) and allow the sub-classes (currently Player and Alien) to use it. We're also going to add a reset() method to the Player class that at the minute will reset the coordinates of the player object to (50, 50) and a later point will allow us to add other functionality too (deducting a life etc).

First off the collision detection in the GameObject class:

class GameObject:
    def __init__(self, image, x=0, y=0):
        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.image_rect = self.image.get_rect()
        self.image_rect.topleft = (x, y)

    def colliding(self, thing):
        if self.image_rect.colliderect(thing.image_rect):
            return True
        return False

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

The method checks if the object is colliding with another object and returns True if it is and False otherwise, we can then use this in the main game loop.

Next, we have the slightly updated Player class which now has a reset() method.

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

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

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

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

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

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

Finally, we have the updated MainWindow class which checks if the player object is colliding and resets it if it is.

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

        self.alien = 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 7')

        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.alien.update()
                self.alien.draw(self.DISPLAYSURF)
                if self.player.colliding(self.alien):
                    self.player.reset()
                self.player.draw(self.DISPLAYSURF)
                self.time_counter = 0
            pygame.display.update()

Running this now should cause the player to reset when it collides with the alien. We're going to modify it slightly so that we have a list of aliens, to begin with, it will still do each of the checks individually using the existing collision method. Which only requires changes to the MainWindow class for now.

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

        self.aliens = []
        for _ in range(20):
            self.aliens.append(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 7')

        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)
                for alien in self.aliens:
                    alien.update()
                    alien.draw(self.DISPLAYSURF)
                    if self.player.colliding(alien):
                        self.player.reset()
                self.player.draw(self.DISPLAYSURF)
                self.time_counter = 0
            pygame.display.update()

Running this gives you quite a tricky game to play. You could make it easier by reducing the range of speed values that the random.randint() function call uses in the constructor for the Aliens, but playability isn't uppermost in our minds at the minute (although if you were making a decent game you would probably playtest it with a variety of speeds to see which ones worked the best).

The alternative method of doing the collision detection is to change the colliding() method in the GameObject class to take a list of objects and return true if one at least of them is colliding with the player. In all probability, this method will have been optimised in some way to make it far more efficient than our loop through the objects in the alien list (it may not have, but it is an obvious place to make optimisations). We first need to change the GameObject class and make the colliding() method take a list rather than a single object.

At least that's what I was thinking until I came to implement it, when I realised that because my list was a list of my own objects rather than a list of Pygame rects it wasn't going to work the way I want it to, and essentially I'd just be moving my loop into the colliding() method rather than in the main game loop, and I would need to build a new list of the rects to pass to it. I then came up with a very hacky way of solving this. my GamObjects have the rect of their image exposed as an attribute called image_rect, I decided to see what would happen if I changed the name of this attribute to just rect and also changed every occurrence of it in the code. I did it, and it works the way I was thinking my code would work, removing the loop (or at least hiding it away somewhere in the Pygame plumbing).

This is my hacky solution, which appears to work, but probably will break at some unspecified point in the future as I haven't implemented things correctly. Here's the full version of the program in all of its ignominy.

import sys
import pygame
import random

WIDTH = 800
HEIGHT = 600

class GameObject:
    def __init__(self, image, x=0, y=0):
        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

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

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

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

        self.aliens = []
        for _ in range(20):
            self.aliens.append(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 7')

        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)
                for alien in self.aliens:
                    alien.update()
                    alien.draw(self.DISPLAYSURF)
                if self.player.colliding(self.aliens):
                    self.player.reset()
                self.player.draw(self.DISPLAYSURF)
                self.time_counter = 0
            pygame.display.update()

game = MainWindow()
game.main_game_loop()

This is where I'm going to leave things for Part 7. What I'm going to do for the next part is look into the Pygame Sprite class and see if what I should be doing is creating my GameObject class as a subclass of that one. As it may also provide us with some useful tools later on. So to summarise Part 8 will be a look at the Sprite class and refactoring our code to take advantage of it. A quick read of the documentation suggests that the Sprite Group may also be the thing to use rather than just a basic list of sprites.


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

Sort:  

Congratulations @amos1969! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :

You published a post every day of the week

You can view your badges on your Steem Board and compare to others on the Steem Ranking
If you no longer want to receive notifications, reply to this comment with the word STOP

To support your work, I also upvoted your post!

Vote for @Steemitboard as a witness to get one more award and increased upvotes!

Coin Marketplace

STEEM 0.28
TRX 0.12
JST 0.032
BTC 61672.72
ETH 2996.85
USDT 1.00
SBD 3.78