Objectives

Working with multiple files

  • Store configuration values in a separate file
  • Explain the benefits of storing configuration values in a separate file

Program structure

  • Create a main() subprogram to control the flow of the whole program
  • Decompose the responsibilities of main() into separate subprograms

Object-oriented design

  • Construct objects using keyword arguments to make code more readable
  • Understand how objects can be composed of Pygame components
  • Apply encapsulation and data hiding: distinguishing between public behaviour and private, internal methods
  • Use getters/setters to safely access and modify private attributes

Game loop and update cycle

  • Read continuous input using pygame.key.get_pressed()
  • Map key presses to object behaviour (e.g., velocity, direction)
  • Describe the structure of a frame update:
    • event handling
    • input processing
    • updating objects
    • drawing

Movement and velocity

  • Update position by modifying the Rect each frame
  • Understand how different selection logic (if/elif) affects movement

Files

Make local copies of the following files before you start.

main.py

import sys
import pygame
import constants
from models import Player
 
def setup():
    pygame.init()
    screen = pygame.display.set_mode((constants.WIDTH, constants.HEIGHT))
    pygame.display.set_caption(constants.WINDOW_TITLE)
    clock = pygame.time.Clock()
    return screen, clock
 
def main():
    screen, clock = setup()
 
    player = Player(
        p_initial_x=constants.WIDTH // 2,
        p_initial_y=constants.HEIGHT // 2,
        p_width=10,
        p_height=50,
        p_speed=1,
    )
 
    running = True
 
    while running:
        # Event handling
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
 
        # Input handling
        keys = pygame.key.get_pressed()
        vx = 0
        vy = 0
 
        if keys[pygame.K_LEFT]:
            vx = -player.get_speed()
        elif keys[pygame.K_RIGHT]:
            vx = player.get_speed()
        elif keys[pygame.K_UP]:
            vy = -player.get_speed()
        elif keys[pygame.K_DOWN]:
            vy = player.get_speed()
 
        player.set_velocity(vx, vy)
 
        # Drawing
        screen.fill(constants.BG_COLOR)
        player.update(screen)
        pygame.display.flip()
 
        clock.tick(constants.FPS)
 
    pygame.quit()
    sys.exit()
 
main()

constants.py

WINDOW_TITLE = "A rectangle that moves"
 
WIDTH = 600
HEIGHT = 400
 
FPS = 60
BG_COLOR = (255, 255, 255)

models.py

import pygame
 
class Player:
    def __init__(
        self,
        p_initial_x,
        p_initial_y,
        p_width,
        p_height,
        p_speed,
        p_color=(0, 255, 0),
        p_vx=0,
        p_vy=0,
    ):
        self.rect = pygame.Rect(p_initial_x, p_initial_y, p_width, p_height)
        self.__speed = p_speed
        self.__max_speed = 10
        self.__color = p_color
        self.__vx = p_vx
        self.__vy = p_vy
 
    def __move(self):
        """Move the Player by vx and vy"""
        self.rect.x = self.rect.x + self.__vx
        self.rect.y = self.rect.y + self.__vy
 
    def __draw(self, screen):
        """Draw the Player on the screen"""
        pygame.draw.rect(screen, self.__color, self.rect)
 
    def get_speed(self):
        return self.__speed
 
    def update(self, screen):
        """Move and draw the Player"""
        self.__move()
        self.__draw(screen)
 
    def set_velocity(self, p_vx, p_vy):
        """Change velocities"""
        self.__vx = p_vx
        self.__vy = p_vy
 
    def reset_position(self):
        """Reset player to its starting position"""
        # TODO: implement this for the modify task
        pass
 
    def increase_speed(self, amount):
        """Increase speed by given amount, capped at max speed"""
        # TODO: implement this for the modify task
        pass

Predict

Predict answers to the following questions without running the program.

  1. What does setup() do in main.py?

  2. What does this line do in main.py?

     screen = pygame.display.set_mode((constants.WIDTH, constants.HEIGHT))
  3. What will the the initial x-coordinate of player be?

  4. What colour will the player be?

  5. What will happen to player when the right arrow key is pressed?

    • Predict how the player’s movement will change
  6. What will happen to player when the right and up arrow keys are pressed simultaneously?

    • Will the player move in a straight line, diagonally, or not at all?

Run

Run the program. Compare your predictions to the actual behaviour.

Investigate

Answer these questions by examining the code.

  1. Why is setup() separate from main()?
    • What responsibilities does setup() have?
    • What effect does separating these responsibilities have on readability and organisation?
    • Identify at least one other part of main() that could be separated into its own subprogram. Justify your answer.
  2. How does the program know what WIDTH and HEIGHT are?
    • Which file defines them?
    • How does main.py gain access to them?
    • Does the Player class know anything about WIDTH and HEIGHT? Why or why not?
  3. Look at how the Player object is created in main().
    • Which parameters are passed to the constructor?
    • What is unusual about the way the parameters are passed?
    • What effect does this have on the code’s readability?
  4. Look at the Player class constructor.
    • Which attributes are public?
    • Which attributes are private?
    • Why might private attributes be useful here?
    • Why does it not store x, y, width, and height? Where is this data stored instead?
  5. What does the set_velocity() method do?
    • How does this relate to key presses in main()?
  6. What does player.update(screen) do each frame?
    • Why does the order of method calls inside update() matter?
    • Why is this method public, but __move() and __draw() are private?

Modify

Make changes to explore how the code works.

  1. Change which keys move the player.
    • Rather than arrow keys, control the player with WASD
  2. Enable diagonal movement.
    • Hint: how does if differ from elif in this context?
  3. Implement reset_position().
    • Pressing the R key should reset the player to their starting position (this should be controlled in main())
    • Hint: does Player need to remember its initial position when created?
  4. Make the player move continuously even after releasing a key.
    • Pressing a key should set the velocity
    • The player should continue moving after the key is released
    • Hint: which line currently overwrites the velocity each frame?
  5. Stop the player leaving the window.
    • Modify __move() so the player cannot move off-screen.
    • Use rect.x, rect.y, rect.width, and rect.height
    • The player should stop at the boundary, they should not bounce.
  6. Implement increase_speed().
    • Pressing the spacebar should increase the player’s speed (this should be controlled in main())
    • The player’s speed should not be able to exceed its max_speed.

Make

Create a new class called Enemy that automatically moves toward the player each frame.

Requirements

Your Enemy class must:

  1. Use a pygame.Rect to represent position and size.
  2. Store private attributes for:
    • colour
    • velocity (x and y)
    • speed
  3. Implement:
    • a public update(screen, player) method
    • a private __move(player) method
    • a private __draw(screen) method
  4. Move toward the player’s position each frame.
    • Hint: compare the enemy’s x/y coordinates to the player’s.
  5. Respect window boundaries and stop at the edges.
  6. Appear on screen alongside the player.

Options

Choose two or more optional enhancements:

  • Fast and slow modes
    • Press a key to toggle between fast and slow chasing speeds
  • Only chase when close
    • The enemy only starts moving if the player is within a certain distance (import math and use distance = math.dist((x1, y1), (x2, y2)))
  • Enemy changes colour depending on distance:
    • Red when close
    • Blue when far
  • Multiple enemies
    • Create a list of enemies and update them all
  • Enemy freezes when the player isn’t moving
  • Implement a superclass that both Player and Enemy inherit from

Hints

You can get the player’s centre using:

px = player.rect.centerx
py = player.rect.centery

To decide which way to move, imagine the enemy asking if the player is to their right or to their left:

if px > self.rect.centerx:
    # player is to the right
elif px < self.rect.centerx:
    # player is to the left

When the enemy knows the player’s direction, they just need to move! The enemy doesn’t teleport, they just take small steps towards the player. Each small step should be the enemy’s speed value.

You will need to extend this to work with the y-axis too.