[refactor] Move objets to separate files.

- ball
- bricks
- paddle
Update documentation and readme.
[fix] Game over: update player score.
This commit is contained in:
2025-04-06 21:37:01 +02:00
parent b8de025881
commit ecbc857d80
7 changed files with 241 additions and 231 deletions
+7 -7
View File
@@ -6,16 +6,16 @@ This program is a simple game implemented in MicroPython for the Raspberry Pi Pi
## Framebuffer
The program uses a framebuffer to draw the game graphics. The framebuffer is a 2D array that represents the pixels on the display. The program uses the micropyhton `framebuf` module to create and manipulate the framebuffer. The framebuffer is then copied to the display using the `blit` method.
## Multithreading
If the program crashes, the framebuffer is not always reset, causing the program to crash due to lack of memeory.
A hard reset is required to fix this issue.
## Multithreading
The program uses multithreading to handle the game logic and the display updates separately. The `threading` module is used to create and manage the threads. The game logic is run in a separate thread from the display updates to ensure smooth gameplay and responsive controls.
## Game Logic
The game logic is implemented in the `game_loop` function. This function runs in a separate thread and handles the following tasks:
- Updating the game state based on user input and game rules
- Generating new obstacles and updating their positions
- Checking for collisions between the character and obstacles
- Updating the game state based on user input andgame flow.
- Updating ball position, paddle position, and bricks on the screen.
- Checking for collisions between the ball, borders, paddle and bricks.
- Updating the score based on the player's performance
- Sending the updated game state to the display thread for rendering
- Sending the updated game state to the display thread for rendering.
+1 -1
View File
@@ -20,7 +20,7 @@ This is a simple Breakout game implemented in MicroPython for the Raspberry Pi P
3. Clone or download this repository to your local machine.
4. Open the Thonny IDE and connect to your Raspberry Pi Pico.
5. Copy the contents of the repository to the Pico's file system. You can do this by dragging and dropping the files from your local machine to the Thonny file explorer.
6. Once the files are copied, reset the Pico to start the game.
6. Once the files are copied, reset the Pico to start the game (main.py will run automatically and start breakout.py).
## .env file
In the .env file the user can control the behaviour of the joystick B-button.
+54
View File
@@ -0,0 +1,54 @@
from random import randint
from screen import Screen, WHITE
from paddle import Paddle
BALL_SPEED = 3
class Ball:
def __init__(self, screen: Screen, paddle: Paddle, radius: int, color: int = WHITE, brick_padding: int = 0, speed: int = BALL_SPEED):
self.screen_width = screen.width
self.screen_height = screen.height
self.radius = radius
self.color = color
self.speed = speed
self.x_speed = self.speed if randint(0, 1) == 0 else -self.speed
self.y_speed = -self.speed
self.brick_padding = brick_padding
self.reset_pos(paddle)
def reset_pos(self, paddle: Paddle):
"""Reset ball position to the center of the paddle."""
self.x = self.screen_width // 2
self.y = self.screen_height // 2 - self.radius - 2
self.x_speed = self.speed if randint(0, 1) == 0 else -self.speed
self.y_speed = -self.speed
def update_pos(self):
"""Update ball position."""
self.x += self.x_speed
self.y += self.y_speed
# Bounce off left or right screen edge
if self.x < 0:
self.x = 0
self.x_speed = -self.x_speed
elif self.x > self.screen_width - self.radius:
self.x = self.screen_width - self.radius
self.x_speed = -self.x_speed
# Bounce off top screen edge
if self.y < self.brick_padding + self.radius:
self.y = self.brick_padding + self.radius
self.y_speed = -self.y_speed
# Drop through bottom screen edge & return True to indicate we lose a life
if self.y > self.screen_height:
self.y = self.screen_height
self.y_speed = -self.y_speed
return True
def draw(self, screen: Screen):
"""Draw ball."""
screen.fbuf.ellipse(self.x, self.y, self.radius, self.radius, self.color, True)
+25 -218
View File
@@ -1,12 +1,15 @@
"""
Threaded breakout game with frame buffer
Threaded breakout game using frame buffer
Author: Seppe De Loore
Author: Seppe De Loore - 2025
"""
from random import randint
from utime import sleep_us
from screen import Screen, RED, YELLOW, GREEN, WHITE
from paddle import Paddle, PADDLE_WIDTH, PADDLE_HEIGHT
from ball import Ball
from bricks import BrickRow, create_bricks, BRICK_PADDING
import _thread
from joystick import Joystick
@@ -19,19 +22,6 @@ SCREEN_HEIGHT = 135
SCREEN_WIDTH = 240
SCREEN_ROTATION = 1 # Landscape mode
PADDLE_WIDTH = 70
PADDLE_HEIGHT = 10
PADDLE_COLOR = WHITE
PADDLE_SPEED = 10
BRICK_WIDTH = 30
BRICK_HEIGHT = 8
BRICK_PADDING = 4
BRICKS_PER_ROW = 7
ROWS = 4
BALL_SPEED = 3
SPLASH_WIDTH = 8
SPLASH_HEIGHT = 5
SPLASH_PADDING = 2
@@ -45,9 +35,7 @@ GAME_NEXT_LEVEL = 4
DEBOUNCE = 300_000
# ============================
# load environment variables
# ============================
try:
DISABLE_B = 0
with open(".env", "r") as file:
@@ -59,177 +47,6 @@ except:
DISABLE_B = 0
# ============================
# CLASSES
# ============================
class Paddle:
def __init__(self, screen: Screen):
"""Initialize the paddle."""
self.screen_width = screen.width
self.screen_height = screen.height
self.x = (self.screen_width - PADDLE_WIDTH) // 2
self.y = self.screen_height - PADDLE_HEIGHT - 5
self.width = PADDLE_WIDTH
self.height = PADDLE_HEIGHT
self.speed = PADDLE_SPEED
def move(self, direction: int):
"""
Move paddle left or right.
Args: direction: -1 for left, 1 for right
"""
self.x += self.speed * direction
if self.x < 0:
self.x = 0
elif self.x > self.screen_width - self.width:
self.x = self.screen_width - self.width
def draw(self, screen: Screen):
"""Draw paddle."""
screen.fbuf.fill_rect(self.x, self.y, self.width, self.height, PADDLE_COLOR)
def update(self, screen: Screen, joystick: Joystick):
"""Update paddle position."""
if joystick.joy_left() == 0:
self.move(-1)
elif joystick.joy_right() == 0:
self.move(1)
self.draw(screen)
def hit(self, ball: Ball) -> bool:
"""Check if the ball hits the paddle and adjust its position."""
if (
self.x <= ball.x <= self.x + self.width
and self.y <= ball.y + ball.radius <= self.y + self.height
):
# Adjust the ball's position to be just above the paddle
ball.y = self.y - ball.radius - 2
return True
return False
class Ball:
def __init__(self, screen: Screen, paddle: Paddle, radius: int, color: int):
"""
Initialize the ball.
Args:
paddle (Paddle): The paddle object to position the ball on.
radius (int): Radius of the ball.
color (int): RGB565 color value of the ball.
"""
self.screen_width = screen.width
self.screen_height = screen.height
self.radius = radius
self.color = color
self.reset_pos(paddle)
self.x_speed = BALL_SPEED if randint(0, 1) == 0 else -BALL_SPEED
self.y_speed = -BALL_SPEED
# Position the ball in the middle of the paddle
self.x = paddle.x + (paddle.width // 2)
self.y = paddle.y - radius - 2 # Place the ball just above the paddle
def reset_pos(self, paddle: Paddle):
"""Reset ball position to the center of the paddle.
Args: Paddle: The paddle object to position the ball on.
"""
self.x = self.screen_width // 2
self.y = self.screen_height // 2 - self.radius - 2
self.x_speed = BALL_SPEED
self.y_speed = -BALL_SPEED
def update_pos(self):
"""Update ball position."""
self.x += self.x_speed
self.y += self.y_speed
# Bounce off left or right screen edge
if self.x < 0:
self.x = 0
self.x_speed = -self.x_speed
elif self.x > self.screen_width - self.radius:
self.x = self.screen_width - self.radius
self.x_speed = -self.x_speed
# Bounce off top screen edge
if self.y < BRICK_PADDING + self.radius:
self.y = BRICK_PADDING + self.radius
self.y_speed = -self.y_speed
# Drop through bottom screen edge & return True to indicate we lose a life
if self.y > self.screen_height:
self.y = self.screen_height
self.y_speed = -self.y_speed
return True
def draw(self, screen: Screen):
"""Draw ball."""
screen.fbuf.ellipse(self.x, self.y, self.radius, self.radius, self.color, True)
class Brick:
def __init__(self, x: int, y: int, width: int, height: int, color: int):
"""
Initialize a brick.
Args: x (int): x-coordinate of the brick.
y (int): y-coordinate of the brick.
width (int): width of the brick.
height (int): height of the brick.
color (int): color of the brick.
"""
self.x = x
self.y = y
self.width = width
self.height = height
self.color = color
def draw(self, screen: Screen):
"""Draw brick."""
screen.fbuf.fill_rect(self.x, self.y, self.width, self.height, self.color)
class BrickRow:
def __init__(self, brick_width: int, brick_height: int, padding:int, offset_top: int, color: int):
"""
Initialize a row of bricks.
Args: brick_width (int): width of each brick.
brick_height (int): height of each brick.
padding (int): padding between bricks.
offset_top (int): y-coordinate of the top of the row.
color (int): color of the bricks.
"""
self.brick_width = brick_width
self.brick_height = brick_height
self.color = color
self.padding = padding
self.offset_top = offset_top
self.bricks = [Brick(padding + i * (brick_width + padding), offset_top, brick_width, brick_height, color) for i in range(BRICKS_PER_ROW)]
self.brick_x = [padding + i * (brick_width + padding) for i in range(BRICKS_PER_ROW)]
self.brick_y = [offset_top] * BRICKS_PER_ROW
def draw(self, screen: Screen):
"""Draw all bricks in the row."""
for brick in self.bricks:
if brick is not None:
brick.draw(screen)
def hit(self, ball: Ball) -> bool:
"""
Check if the ball hits any brick in the row and remove it if hit.
Args: ball (Ball): The ball object to check for collision.
Returns: bool: True if the ball hits a brick, False otherwise.
"""
for i, brick in enumerate(self.bricks):
if brick is not None:
if (brick.x <= ball.x <= brick.x + brick.width) and (brick.y <= ball.y <= brick.y + brick.height):
# Remove the brick by setting it to None
self.bricks[i] = None
return True
return False
class High_score:
def __init__(self):
self.high_score = 0
@@ -298,32 +115,19 @@ def splash_screen(screen: Screen, data_rows: list[int], text: list[str], high_sc
screen.display.blit_buffer(screen.buffer, 0, 0, screen.buffer_width, screen.buffer_height)
def create_bricks() -> list[BrickRow]:
bricks = []
for row in range(ROWS):
if row == 0:
color = RED
elif row == 1:
color = YELLOW
else:
color = GREEN
bricks.append(
BrickRow(BRICK_WIDTH,
BRICK_HEIGHT,
BRICK_PADDING,
10 + row * (BRICK_HEIGHT + BRICK_PADDING),
color))
return bricks
def create_lives(screen: Screen, paddle: Paddle, lives: int) -> list[Ball]:
"""
Create a list of small balls to represent lives
Args: lives (int): Number of lives left
Create a list of small balls to represent lives.
Args:
screen (Screen): The screen object.
paddle (Paddle): The paddle object.
lives (int): Number of lives left.
Returns:
list[Ball]: List of Ball objects representing lives.
"""
lives_balls = []
for i in range(0, lives):
life_ball = Ball(screen, paddle, radius=3, color=WHITE)
life_ball = Ball(screen, paddle, radius=3, color=WHITE, brick_padding=BRICK_PADDING)
life_ball.x = 5 + (i - 1) * 7
life_ball.y = 7
life_ball.x_speed = 0
@@ -350,7 +154,7 @@ def main_loop(screen, joystick):
level = 1
ball_stuck = True # Reset ball to be stuck to the paddle
# Initialize paddle and ball
ball = Ball(screen, paddle, radius=5, color=WHITE)
ball = Ball(screen, paddle, radius=5, color=WHITE, brick_padding=BRICK_PADDING)
paddle.width = PADDLE_WIDTH
paddle.height = PADDLE_HEIGHT
@@ -376,8 +180,8 @@ def main_loop(screen, joystick):
# Launch the ball when "A" is pressed
if joystick.button_a() == 0:
ball_stuck = False
ball.y_speed = -BALL_SPEED
ball.x_speed = BALL_SPEED if randint(0, 1) == 0 else -BALL_SPEED
ball.y_speed = -ball.speed
ball.x_speed = ball.speed if randint(0, 1) == 0 else -ball.speed
else:
if ball.update_pos(): # If ball is out, lose a life and reset ball
lives -= 1
@@ -413,6 +217,7 @@ def main_loop(screen, joystick):
game_state = GAME_NEXT_LEVEL # Transition to next level
if game_state == GAME_OVER: # Game over screen
current_score += score
high_score.update_high_score(current_score)
splash_screen(
screen,
@@ -444,8 +249,8 @@ def main_loop(screen, joystick):
lives += 1
lives_balls = create_lives(screen, paddle, lives)
score = 0
ball_stuck = True # Ball is stuck again
sleep_us(DEBOUNCE) # Debounce delay
ball_stuck = True
sleep_us(DEBOUNCE)
if DISABLE_B == 0 and joystick.button_b() == 0:
break
@@ -455,11 +260,13 @@ def main_loop(screen, joystick):
if __name__ == "__main__":
try:
joystick = Joystick()
screen = Screen(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_ROTATION)
main_loop(screen, joystick)
# Clean up
except KeyboardInterrupt:
pass
finally:
# Clean up resources
screen.clear()
buffer = None
fbuf = None
screen.cleanup()
+88
View File
@@ -0,0 +1,88 @@
from screen import Screen, RED, YELLOW, GREEN
from ball import Ball
BRICK_WIDTH = 30
BRICK_HEIGHT = 8
BRICK_PADDING = 4
BRICKS_PER_ROW = 7
ROWS = 4
class Brick:
def __init__(self, x: int, y: int, width: int, height: int, color: int):
"""
Initialize a brick.
Args: x (int): x-coordinate of the brick.
y (int): y-coordinate of the brick.
width (int): width of the brick.
height (int): height of the brick.
color (int): color of the brick.
"""
self.x = x
self.y = y
self.width = width
self.height = height
self.color = color
def draw(self, screen: Screen):
"""Draw brick."""
screen.fbuf.fill_rect(self.x, self.y, self.width, self.height, self.color)
class BrickRow:
def __init__(self, brick_width: int, brick_height: int, padding:int, offset_top: int, color: int):
"""
Initialize a row of bricks.
Args: brick_width (int): width of each brick.
brick_height (int): height of each brick.
padding (int): padding between bricks.
offset_top (int): y-coordinate of the top of the row.
color (int): color of the bricks.
"""
self.brick_width = brick_width
self.brick_height = brick_height
self.color = color
self.padding = padding
self.offset_top = offset_top
self.bricks = [Brick(padding + i * (brick_width + padding), offset_top, brick_width, brick_height, color) for i in range(BRICKS_PER_ROW)]
self.brick_x = [padding + i * (brick_width + padding) for i in range(BRICKS_PER_ROW)]
self.brick_y = [offset_top] * BRICKS_PER_ROW
def draw(self, screen: Screen):
"""Draw all bricks in the row."""
for brick in self.bricks:
if brick is not None:
brick.draw(screen)
def hit(self, ball: Ball) -> bool:
"""
Check if the ball hits any brick in the row and remove it if hit.
Args: ball (Ball): The ball object to check for collision.
Returns: bool: True if the ball hits a brick, False otherwise.
"""
for i, brick in enumerate(self.bricks):
if brick is not None:
if (brick.x <= ball.x <= brick.x + brick.width) and (brick.y <= ball.y <= brick.y + brick.height):
# Remove the brick by setting it to None
self.bricks[i] = None
return True
return False
def create_bricks() -> list[BrickRow]:
bricks = []
for row in range(ROWS):
if row == 0:
color = RED
elif row == 1:
color = YELLOW
else:
color = GREEN
bricks.append(
BrickRow(BRICK_WIDTH,
BRICK_HEIGHT,
BRICK_PADDING,
10 + row * (BRICK_HEIGHT + BRICK_PADDING),
color))
return bricks
+52
View File
@@ -0,0 +1,52 @@
from screen import Screen, WHITE
PADDLE_WIDTH = 70
PADDLE_HEIGHT = 10
PADDLE_COLOR = WHITE
PADDLE_SPEED = 10
class Paddle:
def __init__(self, screen: Screen):
"""Initialize the paddle."""
self.screen_width = screen.width
self.screen_height = screen.height
self.x = (self.screen_width - PADDLE_WIDTH) // 2
self.y = self.screen_height - PADDLE_HEIGHT - 5
self.width = PADDLE_WIDTH
self.height = PADDLE_HEIGHT
self.speed = PADDLE_SPEED
def move(self, direction: int):
"""
Move paddle left or right.
Args: direction: -1 for left, 1 for right
"""
self.x += self.speed * direction
if self.x < 0:
self.x = 0
elif self.x > self.screen_width - self.width:
self.x = self.screen_width - self.width
def draw(self, screen: Screen):
"""Draw paddle."""
screen.fbuf.fill_rect(self.x, self.y, self.width, self.height, PADDLE_COLOR)
def update(self, screen: Screen, joystick: Joystick):
"""Update paddle position."""
if joystick.joy_left() == 0:
self.move(-1)
elif joystick.joy_right() == 0:
self.move(1)
self.draw(screen)
def hit(self, ball: Ball) -> bool:
"""Check if the ball hits the paddle and adjust its position."""
if (
self.x <= ball.x <= self.x + self.width
and self.y <= ball.y + ball.radius <= self.y + self.height
):
# Adjust the ball's position to be just above the paddle
ball.y = self.y - ball.radius - 2
return True
return False
+10 -1
View File
@@ -2,7 +2,7 @@ from machine import Pin, SPI
import _thread
from st7789 import ST7789
from framebuf import FrameBuffer, RGB565
import gc
def color565(red: int, green: int, blue: int) -> int:
@@ -55,6 +55,8 @@ class Screen:
self.display.blit_buffer(self.buffer, 0, 0, self.buffer_width, self.buffer_height)
def clear(self, refresh: bool = True):
if self.fbuf is None:
return
self.fbuf.fill(BLACK)
if refresh:
self.refresh()
@@ -65,3 +67,10 @@ class Screen:
self.fbuf.fill(BLACK)
self.render_frame = False
# thread will exit and self clean removing need for garbage collection
def cleanup(self):
"""Free resources used by the framebuffer and SPI."""
self.buffer = None
self.fbuf = None
self.spi.deinit()
gc.collect()