Objectives

Working with multiple files

  • Explain what from x import y does
  • Import a class definition from another file
  • Explain the benefits of splitting code across multiple files

Subprograms

Object-oriented fundamentals in Pygame

  • Apply principles of object-oriented design to organise data and behaviour of game entities
  • Explain why game entities benefit from being stored as objects rather than in separate variables
  • Understand why attributes like position, velocity, radius, and colour must be stored in the object

Updating and drawing via an object

  • Represent movement using velocity attributes
  • Use iteration to update many objects each frame
  • Understand the separation of concerns between
    • update() (public, frame-level behaviour)
    • __move() (private, internal position/physics logic)
    • __draw() (private, internal rendering logic)

Coordinates, edges, and boundaries

  • Understand the difference between an object’s position and its edge coordinates
  • Implement methods to get an object’s position and edge coordinates
    • Detect boundary interactions
    • Implement basic bouncing by reversing velocity when reaching an edge

Code quality

  • Apply encapsulation and good method design to keep responsibilities clear
  • Use data hiding and gettters to expose only information needed outside the class definition

Files

Make local copies of the following files before you start.

main.py

import pygame
from models import Ball
 
WIDTH = 600
HEIGHT = 400
CLOCK = 60
BG_COLOR = (255, 255, 255)
 
pygame.init()
 
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Bouncing balls")
 
clock = pygame.time.Clock()
 
balls = [
    Ball(100, 100, 30, (255, 0, 0), 4, 2),
    Ball(300, 200, 20, (0, 255, 0), -3, 5),
]
 
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
 
    screen.fill(BG_COLOR)
 
    for ball in balls:
        ball.update(screen)
 
    pygame.display.flip()
    clock.tick(CLOCK)
 
pygame.quit()

models.py

import pygame
 
class Ball:
    def __init__(self, p_x, p_y, p_radius, p_color=(255, 0, 0), p_vx=0, p_vy=0):
        """Constructor"""
        # TODO: Implement constructor, storing all parameters in attributes
 
    def __draw(self, screen):
        """Draws the Ball on the screen"""
        # TODO: Implement!
 
    def __move(self):
        """Moves the Ball by vx and vy"""
        # TODO: Implement!
 
    def update(self, screen):
        """Move and draw the ball"""
        self.__move()
        self.__draw(screen)

Investigate

Answer these questions by examining the code.

  1. What does from models import Ball do in main.py?
  2. Why does main.py create a list of balls instead of storing them in separate variables?
  3. What do the two for loops do in main.py?
  4. What is unusual about the constructor signature?
    • What does p_color=(255, 0, 0), p_vx=0, p_vy=0 mean in the constructor signature in models.py?
    • Why do the other parameters not have = signs?
    • What do the = signs mean?
  5. Why does the Ball object need all of the parameters as attributes?
  6. Why are the __draw() and __move() methods private?
  7. Why does update() call both __move() and __draw()?
  8. Why does the __draw() method need screen as a parameter?

Modify

Make changes to explore how the code works.

  1. Implement the constructor using the information below:
    VisibilityNameTypeDescription
    privatexintHorizontal position of the ball’s centre
    privateyintVertical position of the ball’s centre
    privatevxintHorizontal velocity (change in x each frame)
    privatevyintVertical velocity (change in y each frame)
    privatecolortupleColour in (R, G, B) format
    privateradiusintSize of the ball
  2. Implement the __draw() method
  3. Implement the __move() method
    • At this stage, do not implement bouncing

Make

Use the information below to implement the missing get methods (get_coords(), __get_left(), __get_right(), __get_top(), __get_bottom()).

The __get_<direction>() methods should return the position of the ball’s edge in that direction (hint: how can you use the ball’s radius to work this out?).

VisibilityMethod SignatureReturnsDescription
publicget_coords()tuple[int, int]Returns (x, y), the ball’s current position
private__get_left()intReturns the x-coordinate of the left edge (x - radius)
private__get_right()intReturns the x-coordinate of the right edge (x + radius)
private__get_top()intReturns the y-coordinate of the top edge (y - radius)
private__get_bottom()intReturns the y-coordinate of the bottom edge (y + radius)
private__draw(screen)NoneDraws the ball on the given screen
private__move(x_bounds, y_bounds)NoneUpdates the ball’s position (later: add bouncing)
publicupdate(screen, x_bounds, y_bounds)NoneCalls __move() then __draw() each frame

Once all the getters work, update __move() so the ball cannot leave the window.

  • When the ball reaches and edge, it should bounce by reversing the appropriate velocity.

You will need to change the update() method:

def update(self, screen, x_bounds, y_bounds):
    """
    Move and draw the ball.
 
    Parameters:
        screen: a pygame screen
        x_bounds: a tuple containing the lower and upper x bounds, e.g. (0, WIDTH)
        y_bounds: a tuple containing the lower and upper y bounds, e.g. (0, HEIGHT)
    """
    self.__move(x_bounds, y_bounds)
    self.__draw(screen)

Extensions

Complete these tasks in any order.

Colour changing

  1. Implement a __change_color(new_color) method to change the ball’s colour
  2. Modify __move() so the ball changes colour each time it bounces.

Inside-bounds?

Implement a inside_bounds(x_bounds, y_bounds) method:

  • Returns True if every part of the ball is within given bounds
  • Returns False otherwise

Randomised bounce velocities

Change __move() so that, when the ball bounces, the velocity is slightly randomised. Make sure the ball cannot become frozen!

Decaying velocity

Create a DecayingBall class that inherits from Ball.

  • Add a new attribute, decay_factor (e.g., 0.9)
  • Override __move() so that velocity is scaled after each bounce