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.
The first part of this is the use of pygame framework. We'll describe the additions here.
Here is the code we added, starting with 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()
class Screen: WIDTH = 800 HEIGHT = 600 FPS = 60 MARGIN = 96 GAMETOP = MARGIN GAMEBOT = HEIGHT - MARGIN
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()
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
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))
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))
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!")