Game Making with Python and Pygame Part 11

in #python5 years ago

Starting a New Game - Space Shooter

gmwithpp-p11.png

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


Apologies for how long it's taken me to post this, next part in the series, but I've been busy doing exactly this sort of thing but for work. For the last week and a bit I've been building games and writing tutorials but using Pygame Zero rather than vanilla Pygame. Pygame Zero is a zero boiler plate wrapper for Pygame, that's designed to introduce many game making concepts whilst hiding away a lot of the complexity of getting a window open and importing the images we want to use. More information can be found here: Pygame Zero have a look, especially if you were struggling with the earlier parts, it might be an easier entry point for getting started.

Anyway first we're going to setup our window class, with a background image. This will be similar to the previous class, most of the changes will come as we add more details at later points. Initially we're going to have a window with a nice starry backdrop.

import pygame
import sys

WIDTH = 600
HEIGHT = 700

class MainWindow:
    def __init__(self):
        pygame.init()
        pygame.key.set_repeat(3, 3)
        self.DISPLAYSURF = pygame.display.set_mode((WIDTH, HEIGHT))
        self.background = pygame.image.load("space1.png")
        self.background_rect = self.background.get_rect()

        self.icon = pygame.image.load("alien-eyes.png")
        pygame.display.set_icon(self.icon)
        pygame.display.set_caption('Shoot the Aliens - Game Making Part 11')

    def main_game_loop(self):
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()

            self.DISPLAYSURF.blit(self.background, self.background_rect)
            pygame.display.update()

game = MainWindow()
game.main_game_loop()

This gives us a window like this (assuming you use the same backdrop as me ;-) ):

Empty Window with a Starry Background

We want to add in a player ship as well, which we can move around the bottom part of the window (but not further up) this will be based on the GameObject and Player classes we created for our last game, the main difference (other than the type of ship) is that we need to restrict where it can move, and there is no down command instead we'll make the ship drift to the bottom if we're not pressing the up arrow.

import pygame
import sys

WIDTH = 600
HEIGHT = 700

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 Player(GameObject):
    def __init__(self, x=0, y=0):
        super().__init__("ship.png", x, y)
        self.rect.midbottom = WIDTH/2, HEIGHT

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

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

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

    def drift_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.DISPLAYSURF = pygame.display.set_mode((WIDTH, HEIGHT))
        self.background = pygame.image.load("space1.png")
        self.background_rect = self.background.get_rect()

        self.icon = pygame.image.load("alien-eyes.png")
        pygame.display.set_icon(self.icon)
        pygame.display.set_caption('Shoot the Aliens - Game Making Part 11')

        self.player = Player(WIDTH/2, HEIGHT-100)

    def main_game_loop(self):
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
            keys = pygame.key.get_pressed()
            if keys[pygame.K_UP] and keys[pygame.K_LEFT]:
                self.player.move_up()
                self.player.move_left()
            elif keys[pygame.K_UP] and keys[pygame.K_RIGHT]:
                self.player.move_up()
                self.player.move_right()
            elif keys[pygame.K_RIGHT]:
                self.player.move_right()
            elif keys[pygame.K_LEFT]:
                self.player.move_left()
            elif keys[pygame.K_UP]:
                self.player.move_up()

            self.player.drift_down()

            self.DISPLAYSURF.blit(self.background, self.background_rect)
            self.player.draw(self.DISPLAYSURF)
            pygame.display.update()

game = MainWindow()
game.main_game_loop()

The main difference from the last game, apart from restricting the movement to the bottom part of the screen is that we're getting the player's key presses in a different way. The get_pressed() method lets us deal with combination of key presses, so that if the player is pressing up and left the ship will travel diagonally whereas using the previous method we could only deal with a single key press. We're going to add one more thing and then leave it for this part.

Currently we have the ship image that looks like this:
Ship No Engines

But what we want is if the up arrow is being pressed, to show this image instead:
Ship With Engines On

and revert back to the other image when we aren't pressing it. If we were just using the images directly this would be straightforward, but we can add some magic beans to the Player class to make this happen.

import pygame
import sys

WIDTH = 600
HEIGHT = 700

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 Player(GameObject):
    def __init__(self, x=0, y=0):
        super().__init__("ship.png", x, y)
        self.rect.midbottom = WIDTH/2, HEIGHT
        self.no_engine = pygame.image.load("ship.png")
        self.engine = pygame.image.load("shipengines.png")

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

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

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

    def drift_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.DISPLAYSURF = pygame.display.set_mode((WIDTH, HEIGHT))
        self.background = pygame.image.load("space1.png")
        self.background_rect = self.background.get_rect()

        self.icon = pygame.image.load("alien-eyes.png")
        pygame.display.set_icon(self.icon)
        pygame.display.set_caption('Shoot the Aliens - Game Making Part 11')

        self.player = Player(WIDTH/2, HEIGHT-100)

    def main_game_loop(self):
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
            keys = pygame.key.get_pressed()
            if keys[pygame.K_UP] and keys[pygame.K_LEFT]:
                self.player.move_up()
                self.player.move_left()
                self.player.image = self.player.engine
            elif keys[pygame.K_UP] and keys[pygame.K_RIGHT]:
                self.player.move_up()
                self.player.move_right()
                self.player.image = self.player.engine
            elif keys[pygame.K_RIGHT]:
                self.player.move_right()
                self.player.image = self.player.no_engine
            elif keys[pygame.K_LEFT]:
                self.player.move_left()
                self.player.image = self.player.no_engine
            elif keys[pygame.K_UP]:
                self.player.move_up()
                self.player.image = self.player.engine
            else:
                self.player.image = self.player.no_engine

            self.player.drift_down()

            self.DISPLAYSURF.blit(self.background, self.background_rect)
            self.player.draw(self.DISPLAYSURF)
            pygame.display.update()

game = MainWindow()
game.main_game_loop()

Initially we load both images into the Player class. To begin with I had the switch of the Player object's image attribute happening in the move_up() and drift_down() methods. But as the drift_down() method runs on every frame we never saw the sprite with the engines on. Instead I've moved the switch to the part that deals with the key presses. What I should actually do, now that I think of it is have two methods engines_on() and engines_off() in the Player class and call those in the bit that deals with the key presses. Making that change now. Part of the reason I've left this in is to model the sorts of thing that you do as you program it, there are lots of dead ends and could have done it betters that mean you end up refactoring your code when a simpler method presents itself.

The reason I'm changing it is that changing the images inside the MainWindow class feels wrong (my OOP Spider Sense is twitching). Changing the contents of the image attribute is something that should be done inside the Player class not elsewhere, calling a method to do so from another class is fine, as we don't need to worry about how the Player class does what it does. Knowing that this is something that needs doing isn't something that can easily be learnt it comes more from experience and realising that things aren't being done in quite the right way.

import pygame
import sys

WIDTH = 600
HEIGHT = 700

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 Player(GameObject):
    def __init__(self, x=0, y=0):
        super().__init__("ship.png", x, y)
        self.rect.midbottom = WIDTH/2, HEIGHT
        self.no_engine = pygame.image.load("ship.png")
        self.engine = pygame.image.load("shipengines.png")

    def engine_on(self):
        self.image = self.engine

    def engine_off(self):
        self.image = self.no_engine

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

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

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

    def drift_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.DISPLAYSURF = pygame.display.set_mode((WIDTH, HEIGHT))
        self.background = pygame.image.load("space1.png")
        self.background_rect = self.background.get_rect()

        self.icon = pygame.image.load("alien-eyes.png")
        pygame.display.set_icon(self.icon)
        pygame.display.set_caption('Shoot the Aliens - Game Making Part 11')

        self.player = Player(WIDTH/2, HEIGHT-100)

    def main_game_loop(self):
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
            keys = pygame.key.get_pressed()
            if keys[pygame.K_UP] and keys[pygame.K_LEFT]:
                self.player.move_up()
                self.player.move_left()
                self.player.engine_on()
            elif keys[pygame.K_UP] and keys[pygame.K_RIGHT]:
                self.player.move_up()
                self.player.move_right()
                self.player.engine_on()
            elif keys[pygame.K_RIGHT]:
                self.player.move_right()
                self.player.engine_off()
            elif keys[pygame.K_LEFT]:
                self.player.move_left()
                self.player.engine_off()
            elif keys[pygame.K_UP]:
                self.player.move_up()
                self.player.engine_on()
            else:
                self.player.engine_off()

            self.player.drift_down()

            self.DISPLAYSURF.blit(self.background, self.background_rect)
            self.player.draw(self.DISPLAYSURF)
            pygame.display.update()

game = MainWindow()
game.main_game_loop()

Right, so that's where we're going to leave things for this part, in the next part we'll add a bullet that the player can fire, some aliens which will travel down the screen and have the beginnings of a space shooter game that's playable. Hopefully it won't take me quite as long to get the next part out the door. Enjoy.


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

Part 10 - Finishing the game and making it so we can restart it: Part 10

Coin Marketplace

STEEM 0.31
TRX 0.11
JST 0.034
BTC 66441.00
ETH 3217.31
USDT 1.00
SBD 4.22