Brick Breaker mit CircuitPython
Hinweis zur Zielgruppe und Voraussetzungen
Dieses Projekt richtet sich an Lernende ab etwa 13 Jahren mit grundlegenden Python-Kenntnissen (z. B. Variablen, Schleifen, Funktionen). Es wird empfohlen, bereits erste Erfahrungen mit der senseBox:edu S2 und CircuitPython gesammelt zu haben – beispielsweise durch einfachere Projekte wie „Blinkende LED“ oder „Temperatur messen“.Was ist der MPU6050?
Der MPU6050 ist ein kombinierter Sensor, der sowohl Beschleunigung (Neigung der Box) als auch Drehbewegungen (Gyroskop) misst. In diesem Projekt nutzen wir nur die Neigungsdaten des Beschleunigungssensors, um das Paddle zu steuern – du musst das Gyroskop also nicht verstehen, um mitzumachen!
In diesem Projekt programmierst du das klassische Arcade-Spiel Brick Breaker mit CircuitPython. Du steuerst ein Paddle durch Neigen der senseBox, um einen Ball zu lenken und alle Blöcke zu zerstören. Das Spiel nutzt den integrierten Beschleunigungssensor zur Steuerung und das OLED Display zur Anzeige.
Lernziele
- Grundlagen der Spieleprogrammierung mit CircuitPython
- Verwendung des Beschleunigungssensors zur Bewegungserkennung
- Grafische Darstellung auf dem OLED Display
- Kollisionserkennung und Spiellogik
Aufbau
Schließe das OLED Display mit einem QWIIC-Kabel an einen der I2C-Anschlüsse der senseBox MCU-S2 an. Der MPU6050 Beschleunigungssensor ist bereits auf dem Board integriert und muss nicht zusätzlich angeschlossen werden.
⚠️ Wichtiger Hinweis zur Hardware (senseBox MCU-S2, Version 2.1)
Diese Anleitung setzt spezifische Hardwareeinstellungen voraus:
- Der
IO_POWER-Pin muss aufFalsegesetzt werden (nichtTruewie bei anderen Boards).- Das OLED-Display verwendet die I²C-Adresse
0x3D.- Der integrierte MPU6050 nutzt die GPIO-Pins 42 (SCL) und 45 (SDA) für I²C.
Diese Einstellungen sind im Beispielcode bereits korrekt implementiert.
Abbildung: Aufbau des Brick Breaker Setups
Programmierung
Schritt 0: Vorbereitung
Lade die benötigten Bibliotheken SSD1306, adafruit_display_text und MPU6050 herunter und kopiere die Bibliotheken auf deine senseBox MCU S2. Dazu muss CircuitPython auf deine senseBox MCU S2 installiert werden. Folge dazu diesem Tutorial
Schritt 1: Bibliotheken importieren
Beim Programmieren mit CircuitPython beginnst du damit, die Bibliotheken von deiner senseBox MCU S2 in den Code zu importieren.
import time
import board
import displayio
import digitalio
import busio
import microcontroller
from i2cdisplaybus import I2CDisplayBus
import adafruit_displayio_ssd1306
import adafruit_mpu6050
import math
Schritt 2: Hardware initialisieren
Als Nächstes initialisierst du die angeschlossenen Komponenten: Den Bildschirm und den eingebauten Beschleunigungssensor. Damit legst du fest, wie deine senseBox mit diesen Komponenten kommuniziert.
# Enable IO power
io_enable_pin = digitalio.DigitalInOut(board.IO_POWER)
io_enable_pin.direction = digitalio.Direction.OUTPUT
io_enable_pin.value = False
time.sleep(0.5)
# Initialize display
displayio.release_displays()
i2c_display = board.I2C()
display_bus = I2CDisplayBus(i2c_display, device_address=0x3D)
display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=128, height=64)
# Initialize MPU6050
scl = microcontroller.pin.GPIO42
sda = microcontroller.pin.GPIO45
i2c_mpu = busio.I2C(scl, sda)
mpu = adafruit_mpu6050.MPU6050(i2c_mpu)
Schritt 3: Spielkonstanten definieren
In diesem Schritt wollen wir die Spielkonstanten definieren: Den Ball, den steuerbaren Paddle und die Bricks, die mit dem Ball getroffen werden sollen. Die verschiedenen Komponenten des Spiels werden nur definiert und noch nicht auf dem Bildschirm eingezeichnet.
# Game constants
PADDLE_WIDTH = 20
PADDLE_HEIGHT = 3
PADDLE_Y = 58
PADDLE_SPEED = 2.0
BALL_SIZE = 2
BALL_SPEED = 1.5
BRICK_WIDTH = 12
BRICK_HEIGHT = 4
BRICK_ROWS = 3
BRICK_COLS = 10
BRICK_SPACING = 1
# Game state
paddle_x = 54 # Center position
ball_x = 64.0
ball_y = 50.0
ball_dx = 0.0
ball_dy = 0.0
game_started = False
game_over = False
game_won = False
# Bricks array (True = exists, False = destroyed)
bricks = [[True for _ in range(BRICK_COLS)] for _ in range(BRICK_ROWS)]
Schritt 4: Display Funktionen erstellen
Nachfolgend wollen wir Funktionen erstellen, um das Spiel auf dem Display darzustellen. Wir zeigen an dieser Stelle noch nichts auf dem Display an, sondern erstellen Funktionen, auf die wir später zugreifen. Dafür legen wir die möglichen Positionen der Spielelemente fest. Der Ball soll sich beispielsweise über das gesamte Display bewegen können, er benötigt also variable X- und Y-Koordinaten. Das Paddle dagegen soll sich nur auf der X-Achse am unteren Rand bewegen können und braucht daher nur eine Koordinate. Die Bricks sollen sich nicht bewegen. Sie sollen sich aber schwarz verfärben, wenn sie getroffen werden, sodass es so aussieht, als wären sie nicht mehr vorhanden.
def create_display():
"""Create initial display with bricks and paddle"""
group = displayio.Group()
# Background
bg_bitmap = displayio.Bitmap(128, 64, 1)
bg_palette = displayio.Palette(1)
bg_palette[0] = 0x000000 # Black color
bg_sprite = displayio.TileGrid(bg_bitmap, pixel_shader=bg_palette)
group.append(bg_sprite)
# Draw bricks
brick_palette = displayio.Palette(1)
brick_palette[0] = 0xFFFFFF # White color
for row in range(BRICK_ROWS):
for col in range(BRICK_COLS):
if bricks[row][col]:
x = col * (BRICK_WIDTH + BRICK_SPACING) + 4
y = row * (BRICK_HEIGHT + BRICK_SPACING) + 4
brick_bitmap = displayio.Bitmap(BRICK_WIDTH, BRICK_HEIGHT, 1)
brick_sprite = displayio.TileGrid(
brick_bitmap,
pixel_shader=brick_palette,
x=x,
y=y
)
group.append(brick_sprite)
return group
def draw_paddle(group, x):
"""Draw paddle at position"""
paddle_palette = displayio.Palette(1)
paddle_palette[0] = 0xFFFFFF
paddle_bitmap = displayio.Bitmap(PADDLE_WIDTH, PADDLE_HEIGHT, 1)
paddle_sprite = displayio.TileGrid(
paddle_bitmap,
pixel_shader=paddle_palette,
x=int(x),
y=PADDLE_Y
)
group.append(paddle_sprite)
return paddle_sprite
def draw_ball(group, x, y):
"""Draw ball at position"""
ball_palette = displayio.Palette(1)
ball_palette[0] = 0xFFFFFF # White color
ball_bitmap = displayio.Bitmap(BALL_SIZE, BALL_SIZE, 1)
ball_sprite = displayio.TileGrid(
ball_bitmap,
pixel_shader=ball_palette,
x=int(x),
y=int(y)
)
group.append(ball_sprite)
return ball_sprite
Schritt 5: Kollisionserkennung
Die Kollisionserkennung gehört zu den wichtigsten Bausteinen des Spiels. Wenn der Ball einen Brick berührt, soll dieser aus dem Spiel entfernt werden. Berührt der Ball das Paddle, soll er in die entgegengesetzte Richtung zurückgespielt werden. Das klingt komplizierter, als es ist: Wir müssen lediglich mit einigen if-Abfragen die Position des Balls überprüfen. Außerdem wollen wir in diesem Schritt festlegen, wann der Spieler das Spiel gewonnen hat. Nämlich dann, wenn keine Bricks mehr übrig sind. Was genau passiert, wenn eine Kollision erkannt wird, legen wir im nächsten Schritt fest.
def check_brick_collision(x, y):
"""Check if ball hits a brick and remove it"""
global bricks
for row in range(BRICK_ROWS):
for col in range(BRICK_COLS):
if bricks[row][col]:
brick_x = col * (BRICK_WIDTH + BRICK_SPACING) + 4
brick_y = row * (BRICK_HEIGHT + BRICK_SPACING) + 4
if (x < brick_x + BRICK_WIDTH and
x + BALL_SIZE > brick_x and
y < brick_y + BRICK_HEIGHT and
y + BALL_SIZE > brick_y):
bricks[row][col] = False
return True
return False
def check_paddle_collision(ball_x, ball_y, paddle_x):
"""Check if ball hits paddle"""
if (ball_y + BALL_SIZE >= PADDLE_Y and
ball_y <= PADDLE_Y + PADDLE_HEIGHT and
ball_x + BALL_SIZE >= paddle_x and
ball_x <= paddle_x + PADDLE_WIDTH):
return True
return False
def check_win():
"""Check if all bricks are destroyed"""
for row in bricks:
if any(row):
return False
return True
Schritt 6: Gameplay-Loop
Zum Schluss wollen wir den sogenannten “Gameplay-Loop” definieren. Wir legen also fest, wann das Spiel startet und endet, wie es gesteuert wird und was genau dazwischen passiert. Im Grunde erstellen wir hier das Herz des Spiels. Erst jetzt wird das Programm wirklich zu einem Spiel.
Zunächst nutzen wir die Funktionen aus Schritt 4, um die Spielelemente auf dem Display anzuzeigen und geben in der Python-Konsole am Rechner Informationen über das Spiel aus.
# Initialize display
display_group = create_display()
paddle_sprite = draw_paddle(display_group, paddle_x)
ball_sprite = draw_ball(display_group, ball_x, ball_y)
display.root_group = display_group
print("Brick Breaker Started!")
print("Tilt to move paddle")
print("Shake to start the game")
Danach legen wir den Spielstart mithilfe des Beschleunigungssensors fest. Dazu müssen wir die Werte des Sensors auslesen und bestimmen, ab welchen Werten das Schütteln der senseBox als Spielstart erkannt wird.
while True:
# Read accelerometer
accel_x, accel_y, accel_z = mpu.acceleration
# Check for shake (start/reset)
if abs(accel_x) > 15 or abs(accel_y) > 15 or abs(accel_z) > 15:
if game_over or game_won:
# Reset game
ball_x = 64.0
ball_y = 50.0
ball_dx = 0.0
ball_dy = 0.0
paddle_x = 54
game_started = False
game_over = False
game_won = False
bricks = [[True for _ in range(BRICK_COLS)] for _ in range(BRICK_ROWS)]
# Redraw everything
display_group = create_display()
paddle_sprite = draw_paddle(display_group, paddle_x)
ball_sprite = draw_ball(display_group, ball_x, ball_y)
display.root_group = display_group
print("Game reset!")
elif not game_started:
game_started = True
ball_dx = BALL_SPEED
ball_dy = -BALL_SPEED
print("Game started!")
time.sleep(1)
continue
Während diese while-Schleife läuft, wollen wir außerdem die Steuerung des Paddles über die senseBox definieren, festlegen, was bei einer Kollision passiert, und bestimmen, wann der Spieler das Spiel gewonnen oder verloren hat. Diese Abfragen sollen sehr häufig ausgeführt werden, damit sich das Spiel flüssig anfühlt. Deshalb wird die Schleife alle 0,03 Sekunden wiederholt. Das entspricht etwa 30 Wiederholungen pro Sekunde, auch bekannt als “FPS” (Frames per Second).
# Move paddle based on tilt
if not game_over and not game_won:
paddle_x += accel_y * PADDLE_SPEED
paddle_x = max(0, min(128 - PADDLE_WIDTH, paddle_x))
paddle_sprite.x = int(paddle_x)
# Update ball if game is running
if game_started and not game_over and not game_won:
# Move ball
ball_x += ball_dx
ball_y += ball_dy
# Ball collision with walls
if ball_x <= 0 or ball_x >= 128 - BALL_SIZE:
ball_dx = -ball_dx
ball_x = max(0, min(128 - BALL_SIZE, ball_x))
if ball_y <= 0:
ball_dy = -ball_dy
ball_y = 0
# Ball collision with paddle
if check_paddle_collision(ball_x, ball_y, paddle_x):
ball_dy = -abs(ball_dy)
# Add spin based on where ball hits paddle
hit_pos = (ball_x + BALL_SIZE/2 - paddle_x) / PADDLE_WIDTH
ball_dx = (hit_pos - 0.5) * 3
# Ball collision with bricks
if check_brick_collision(ball_x, ball_y):
ball_dy = -ball_dy
# Redraw display
display_group = create_display()
paddle_sprite = draw_paddle(display_group, paddle_x)
ball_sprite = draw_ball(display_group, ball_x, ball_y)
display.root_group = display_group
# Check win
if check_win():
game_won = True
print("YOU WIN!")
# Check game over
if ball_y > 64:
game_over = True
print("GAME OVER!")
# Update ball position
ball_sprite.x = int(ball_x)
ball_sprite.y = int(ball_y)
time.sleep(0.03)
Spielanleitung
- Spiel starten: Schüttle die senseBox kräftig um das Spiel zu starten
- Paddle steuern: Neige die senseBox nach links oder rechts um das Paddle zu bewegen
- Ziel: Zerstöre alle Blöcke mit dem Ball
- Game Over: Der Ball darf nicht am Paddle vorbei nach unten fallen
- Neustart: Nach Game Over oder Gewinn kannst du durch Schütteln neu starten
Abbildung: Steuerung des Brick Breaker Spiels mit dem eingebauten Beschleunigungssensor
Erweiterungen
Du kannst das Spiel beliebig erweitern, falls du etwas hinzufügen möchtest. Du könntest zusätzliche Komponenten an deine MCU anschließen oder folgendes verändern:
- Schwierigkeitsgrade: Ändere BALL_SPEED für verschiedene Schwierigkeiten
- Mehr Blöcke: Erhöhe BRICK_ROWS für mehr Herausforderung
- Power-Ups: Füge spezielle Blöcke hinzu, die das Paddle vergrößern
- Punktezähler: Implementiere einen Score-Counter
Sonstiges
Den gesamten Code zum Kopieren findest du hier. Weitere Tutorials und Informationen zu CircuiPython findest du bei den senseBox Docs.