= 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? * Refactoring ** Encapsulation ** Light Commentary (code commentary) ex. IN and OUT for functions. 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 * [[Box Sprites]] -- the kind of sprites first used in this game. == 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!")