Table of Contents

Bullet Game

Bullet Game is our near-end stage pygame 2d game framework example. We've already done at least 6 or 7 other games using pygame. So this will document the patterns we have learned.

We began by “just writing” a game, then the second part was refactoring the game. I wanted to make refactoring it's own project. What are the goals of this refactoring?

The primary goal is encapsulation. So, we're going to break up the old main into several new classes.

PyGame Framework

The first part of this is the use of pygame framework. We'll describe the additions here.

Also See

Bullet Game

Here is the code we added, starting with main.py:

main.py

from Window import *
from Game import *

def main():
    window = Window()
    window.setCaption("Bullet Game")
    window.setSize(Screen.WIDTH, Screen.HEIGHT)
    window.setFont("assets/ps2p.ttf", 32)

    game = Game(window)
    game.start()

if __name__ == "__main__":
    main()

Screen,py


class Screen:
    WIDTH = 800
    HEIGHT = 600
    FPS = 60

    MARGIN = 96
    GAMETOP = MARGIN
    GAMEBOT = HEIGHT - MARGIN

Game.py

import pygame
import random
import pickle

from Screen import *
from Window import *
from Bullet import *
from Color import *
from Screen import *
from Zombie import *
from SpaceShip import *
from Sound import *
from Player import *
from Constants import *
from Level import *
from Star import *


class Game:
    def __init__(self, window):
        self.window = window
        self.sound = Sound()

        # Initialize Game World Data
        # The Player & Level
        self.player = Player()
        self.player.hiscore = self.load_hiscore("score.txt")

        self.level = Level(1, self.player)

        # Bullet list
        self.bullets = pygame.sprite.Group()

        # Zombies list
        self.zombies = pygame.sprite.Group()
        self.spaceships = pygame.sprite.Group()

        # 30 random stars.
        self.stars = []
        for n in range(30):
            x = random.randint(0, Screen.WIDTH)
            y = random.randint(0, Screen.HEIGHT)
            self.stars.append((x, y))

        # All member variables are now globals:
        # Try commenting it out to see what happens!
        globals().update(self.__dict__)

    #################################
    ##          Main Loop          ##
    #################################

    def start(self):

        # Clock for controlling the frame rate
        clock = pygame.time.Clock()

        # Main game loop
        running = True
        frame_counter = 0

        ##################
        # GAME LOOP ORDER OF EVENTS:
        ##################
        sound.play("start")
        # pygame.mixer.init()
        # pygame.mixer.music.load('assets/sound/waveafterwave.mp3')
        # pygame.mixer.music.play()

        # Start of Game Loop
        time_start = time.time() * 1000
        LEVEL_DELAY = 3000
        while running:
            time_clock = (time.time() * 1000) - time_start
            frame_counter += 1
            if frame_counter > 60:
                frame_counter = 1

            ########################
            # 1. EVENT HANDLING
            ########################
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
                    quit_game()
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_z:
                        self.create_spaceship()
                    if event.key == pygame.K_ESCAPE:
                        self.quit_game()
                    if event.key == pygame.K_q:
                        self.quit_game()

                    if event.key == pygame.K_w:
                        x = self.player.rect.x + self.player.SIZE / 2
                        y = self.player.rect.y + self.player.SIZE / 2
                        self.create_bullet(x, y, 0, -1)

            # Continuous (frame-by-frame) Key press handling
            keys = pygame.key.get_pressed()
            if keys[pygame.K_LEFT]:
                self.player.rect.x -= self.player.SPEED
            if keys[pygame.K_RIGHT]:
                self.player.rect.x += self.player.SPEED

            #######################
            # 2. RULES SECTION
            #######################
            ### Level-specifc stuff:
            if time_clock > LEVEL_DELAY:
                commands = self.level.tick_hook()

                for cmd in commands:
                    if cmd == "create zombie":
                        self.create_zombie()

                    if cmd == "create spaceship":
                        self.create_spaceship()

                    if cmd == "go to level2":
                        self.kill_enemies()
                        time_start = time.time() * 1000
                        self.level = Level(2, player)

                    if cmd == "go to level3":
                        self.kill_enemies()
                        time_start = time.time() * 1000
                        self.level = Level(3, self.player)
                    if cmd == "go to level4":
                        self.kill_enemies()
                        time_start = time.time() * 1000
                        self.level = Level(4, self.player)

            # 2b. rules for bullets
            # 2b.1. Update the bullets position.
            self.bullets.update()

            # Update all stars.
            for i in range(len(self.stars)):
                (x, y) = self.stars[i]
                y = y + 1

                if y > Screen.HEIGHT:
                    x = random.randint(0, Screen.WIDTH)
                    y = 0

                self.stars[i] = (x, y)

            if time_clock > LEVEL_DELAY:
                # 2a rules for the square
                # Keep the square within bounds
                self.player.update()

                # 2c. rules for zombies
                # 2c.1. Update the zombies position.
                self.zombies.update()
                self.spaceships.update()

            # Clear the screen
            self.window.screen.fill(Color.BLACK)

            # Draw stars first (because its a background)
            for s in self.stars:
                pygame.draw.rect(self.window.screen, "white", pygame.Rect(s[0], s[1], 2, 2))

            # Draw the square
            # pygame.draw.rect(screen, square_color, (square_x, square_y, square_size, square_size))
            self.window.screen.blit(self.player.image, self.player.rect)

            if time_clock > LEVEL_DELAY:
                # Draw the zombies.
                self.zombies.draw(self.window.screen)
                self.spaceships.draw(self.window.screen)

                # ships make a sound, if...
                if self.spaceships:
                    if frame_counter == 30:
                        self.sound.play("pellet")

            # Draw the bullets.
            self.bullets.draw(self.window.screen)

            # write score.
            if frame_counter < 30:
                s = "1UP"
                text_surface = self.window.font.render(s, True, "white")
                self.window.screen.blit(text_surface, (96, 10))

            s = f"{self.player.score:02}"
            text_surface = self.window.font.render(s, True, "red")
            self.window.screen.blit(text_surface, (160, 42))

            # do high score.
            s = "HIGH SCORE"
            text_surface = self.window.font.render(s, True, "white")
            self.window.screen.blit(text_surface, (320, 10))

            s = f"{self.player.hiscore:02}"
            text_surface = self.window.font.render(s, True, "red")
            self.window.screen.blit(text_surface, (512, 42))

            s = f"{self.level.level_number}"
            text_surface = self.window.font.render(s, True, "red")
            self.window.screen.blit(text_surface, (700, 42))

            if time_clock < LEVEL_DELAY:
                s = f"Level {self.level.level_number}"
                text_surface = self.window.font.render(s, True, "white")
                tr = text_surface.get_rect()
                tr.center = (Screen.WIDTH // 2, Screen.HEIGHT // 2 - 100)
                self.window.screen.blit(text_surface, tr)

            # Update the display
            pygame.display.flip()

            if time_clock > LEVEL_DELAY:
                # 3. INTERSPRITE COLLISION
                # 3a. bullets kill zombies
                collisions = pygame.sprite.groupcollide(self.bullets, self.zombies, True, True)
                collisions2 = pygame.sprite.groupcollide(self.bullets, self.spaceships, True, True)

                if collisions:
                    self.player.score = self.player.score + len(collisions)
                    if self.player.score > self.player.hiscore:
                        self.player.hiscore = self.player.score

                if collisions or collisions2:
                    n = random.randint(1, 2)
                    if n == 1:
                        self.sound.play("boom")
                    else:
                        self.sound.play("boom7")

                if collisions2:
                    self.player.score = self.player.score + (len(collisions2) * 10)
                    if self.player.score > self.player.hiscore:
                        self.player.hiscore = self.player.score

                # Players may not touch zombies:
                collided_enemies = pygame.sprite.spritecollide(self.player, self.zombies, False)

                if collided_enemies:
                    print("You are DEAD!")
                    print(" High score: " + str(self.player.hiscore))
                    print(" Your score: " + str(self.player.score))
                    self.quit_game()

            # Control the frame rate
            clock.tick(60)

    #################################
    ##      Support Functions      ##
    #################################

    def create_bullet(self, x, y, dx, dy):
        self.bullet = Bullet(x, y, dx, dy)
        self.bullets.add(self.bullet)
        self.sound.play("laser")
        if Constants.VERBOSE:
            print("bullet created")

    def create_zombie(self):
        sx = random.randint(50, Screen.WIDTH - 50)
        sy = 0 + Screen.MARGIN
        dx = 0
        dy = 1
        z = Zombie(sx, sy, dx, dy)
        self.zombies.add(z)
        if Constants.VERBOSE:
            print("zombie created")

    def create_spaceship(self):
        sx = Screen.WIDTH
        sy = 0 + Screen.MARGIN
        dx = -1
        dy = 0

        if random.randint(1, 2) == 1:
            dx = 1
            sx = 0

        s = SpaceShip(sx, sy, dx, dy)
        self.spaceships.add(s)
        if Constants.VERBOSE:
            print("spaceship created")

    def kill_enemies(self):
        for z in self.zombies:
            z.kill()
        for s in self.spaceships:
            s.kill()

    # Save the score to a text file.
    def save_hiscore(self, filename, score):
        with open(filename, 'w') as file:
            file.write(str(score))

    # Load the score from a text file.
    def load_hiscore(self, filename):
        try:
            with open(filename, 'r') as file:
                score = int(file.read())
                return score
        except:
            print(f"Error. Resetting high score to 0.")
            return 0

    def quit_game():
        self.save_hiscore("score.txt", str(self.player.hiscore))
        pygame.quit()
        quit()

Level.py

import random

class Level:

    def __init__(self, n, p):
        self.level_number = n
        self.player = p

        match self.level_number:
            case 1:
                self.tick_hook = self.level1

            case 2:
                self.tick_hook = self.level2

            case 3:
                self.tick_hook = self.level3

            case 4:
                self.tick_hook = self.level4

            case default:
                print("Unknown Level Number: " + str(n))
                print("Defaulting to Level 1!")
                self.tick_hook = self.level1

        return


##### Tick Hooks
    def level1(self):
        commands = []

        if random.randint(1,45) == 1:
            commands.append("create zombie")

        if self.player.score >= 20:
            commands.append("go to level2")

        return commands


    def level2(self):
        commands = []

        if random.randint(1, 35) == 1:
            commands.append("create zombie")

        if self.player.score >= 40:
            commands.append("go to level3")

        return commands

    def level3(self):
        commands = []

        if random.randint(1, 30) == 1:
            commands.append("create zombie")

        if random.randint(1, 500) == 1:
            commands.append("create spaceship")

        if self.player.score >= 80:
            commands.append("go to level4")

        return commands

    def level4(self):
        commands = []

        if random.randint(1, 20) == 1:
            commands.append("create zombie")

        if random.randint(1, 350) == 1:
            commands.append("create spaceship")

        #if self.player.score > 320:
        #    commands.append("go to level5")

        return commands

Object.py

import pygame

class Object(pygame.sprite.Sprite):
    def __init__(self, x, y, width, height, name=None):
        super().__init__()
        self.rect = pygame.Rect(x, y, width, height)
        self.image = pygame.Surface((width, height), pygame.SRCALPHA)
        self.width = width
        self.height = height
        self.name = name

    def draw(self, win, offset_x, offset_y):
        win.blit(self.image, (self.rect.x - offset_x, self.rect.y + offset_y))

Player.py

import pygame
import time
from Color import *
from Screen import *

# Define the Bullet Sprite class
class Player(pygame.sprite.Sprite):
    SPEED = 5
    SIZE = 20
    COLOR = Color.WHITE

    def __init__(self):
        super().__init__()
        self.ticks = 0
        self.loops = 0

        # Create a surface for the bullet
        self.image = pygame.Surface((self.SIZE, self.SIZE))

        # Fill the surface with a color
        self.image.fill(self.COLOR)

        # Get the rectangle for positioning
        self.rect = self.image.get_rect()

        # Set the initial position
        self.rect.center = (Screen.WIDTH//2, Screen.HEIGHT-50)

        self.score = 0
        self.hiscore = 0

    def update(self):
        # Restrict to screen.
        if self.rect.x < 0:
            self.rect.x = 0
        if self.rect.x > Screen.WIDTH - self.SIZE:
            self.rect.x = Screen.WIDTH - self.SIZE
        if self.rect.y < 0:
            self.rect.y = 0
        if self.rect.y > Screen.HEIGHT - self.SIZE:
            self.rect.y = Screen.HEIGHT - self.SIZE

        # Easier to read, but, slower.
        #self.rect.x = max(0, min(self.rect.x, Screen.WIDTH - self.SIZE))
        #self.rect.y = max(0, min(self.rect.y, Screen.HEIGHT - self.SIZE))

Sound.py

import pygame as a

class Sound:
    def __init__(self):
        a.mixer.init()
        dir = 'assets/sound/'
        self.sounds = {}

        self.sounds["laser"] = a.mixer.Sound(dir+'laser1.wav')
        self.sounds["laser"].set_volume(0.15)
        self.sounds["boom"] = a.mixer.Sound(dir+'boom6.wav')
        self.sounds["boom7"] = a.mixer.Sound(dir+'boom7.wav')
        self.sounds["start"] = a.mixer.Sound(dir+'start4.wav')
        self.sounds["pellet"] = a.mixer.Sound(dir+'pellet.wav')

    def play(self, sound):
        if sound in self.sounds:
            self.sounds[sound].play()
        else:
            print(f"There is no {sound} sound!")