Objectives
Working with multiple files
- Explain what
from x import ydoes - Import a class definition from another file
- Explain the benefits of splitting code across multiple files
Subprograms
- Use default parameters
- Explain the benefit of using default parameters
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.
- What does
from models import Balldo inmain.py? - Why does
main.pycreate a list of balls instead of storing them in separate variables? - What do the two
forloops do inmain.py? - What is unusual about the constructor signature?
- What does
p_color=(255, 0, 0), p_vx=0, p_vy=0mean in the constructor signature inmodels.py? - Why do the other parameters not have
=signs? - What do the
=signs mean?
- What does
- Why does the
Ballobject need all of the parameters as attributes? - Why are the
__draw()and__move()methods private? - Why does
update()call both__move()and__draw()? - Why does the
__draw()method needscreenas a parameter?
Modify
Make changes to explore how the code works.
- Implement the constructor using the information below:
Visibility Name Type Description private xint Horizontal position of the ball’s centre private yint Vertical position of the ball’s centre private vxint Horizontal velocity (change in x each frame) private vyint Vertical velocity (change in y each frame) private colortuple Colour in (R, G, B)formatprivate radiusint Size of the ball - Implement the
__draw()method - 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?).
| Visibility | Method Signature | Returns | Description |
|---|---|---|---|
| public | get_coords() | tuple[int, int] | Returns (x, y), the ball’s current position |
| private | __get_left() | int | Returns the x-coordinate of the left edge (x - radius) |
| private | __get_right() | int | Returns the x-coordinate of the right edge (x + radius) |
| private | __get_top() | int | Returns the y-coordinate of the top edge (y - radius) |
| private | __get_bottom() | int | Returns the y-coordinate of the bottom edge (y + radius) |
| private | __draw(screen) | None | Draws the ball on the given screen |
| private | __move(x_bounds, y_bounds) | None | Updates the ball’s position (later: add bouncing) |
| public | update(screen, x_bounds, y_bounds) | None | Calls __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
- Implement a
__change_color(new_color)method to change the ball’s colour - Modify
__move()so the ball changes colour each time it bounces.
Inside-bounds?
Implement a inside_bounds(x_bounds, y_bounds) method:
- Returns
Trueif every part of the ball is within given bounds - Returns
Falseotherwise
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