Breakout spel in Python
In deze tutorial gaan we een klassiek Breakout spel maken met Pygame Zero. Werk stap voor stap, en test je code na iedere wijziging.
Wat is Breakout?
Breakout is een klassiek computerspelletje waarbij je met een paddle een bal moet raken om bakstenen te breken. Het doel is om alle bakstenen kapot te maken
Stap 0: De basisstructuur en achtergrond
Zorg ervoor dat je de juiste assets hebt, je kan deze downloaden van itch.io: https://moxica.itch.io/block-breaker-arkanoid-breakout.
Plaats ze in een map images, en verander de hoofdletters in de bestandsnamen naar kleine letters. Alleen zo kan Pygame Zero er gebruik van maken. Hernoem Brick1_4.png naar brick1.png, enz.
images/player.pngimages/ball-small.pngimages/brick1.pngt/mimages/brick9.png
Maak een bestandje breakout.py:
import pgzrun
TITLE = "My Breakout Game"
WIDTH = 640
HEIGHT = 480
def draw():
screen.fill((30, 30, 80))
pgzrun.go()
Wat gebeurt er?
TITLE = "My Breakout Game": titel van het vensterWIDTH, HEIGHT: formaat van het speelvelddraw(): speciale functie die Pygame Zero automatisch aanroept, alles wat we zichtbaar op het scherm willen zien moeten we in deze functie “tekenen”
Test: Run
python breakout.py. Je ziet een donkerblauw venster van 640x480 pixels. Het is nog niet veel, maar het is een goede test dat python en Pygame Zero geinstalleerd zijn en functioneren.
Stap 1: De paddle toevoegen
Met deze stap voegen we de beweegbare paddle toe die later de bal zal raken. We gebruiken hiervoor een pygame Actor. Dit is vergelijkbaar met een Sprite in Scratch.
Let op, deze code bouwt verder op de vorige. Met # NIEUW geven we aan welke code je in deze stap moet toevoegen.
breakout1.py:
from pgzero.actor import Actor # NIEUW
import pgzrun
TITLE = "My Breakout Game"
WIDTH = 640
HEIGHT = 480
paddle = None # NIEUW
# NIEUW
def start_game():
global paddle
paddle = Actor("player")
paddle.center = (WIDTH / 2, HEIGHT - 30)
def draw():
screen.fill((30, 30, 80))
paddle.draw() # NIEUW
start_game() # NIEUW
pgzrun.go()
Nieuwe elementen:
paddle = Actor("player"): maak een nieuwe “Actor”,paddle, en gebruik de afbeelding “player.png” uit de images folderstart_game(): start functie die één keer wordt aangeroepen - uiteindelijk gaan we deze opnieuw aanroepen als je Game Over bent- we plaatsen het middelpunt (center) van de paddle horizontaal in het midden, en verticaal 30 pixels verwijdert van de onderkant
- om de paddle zichtbaar te maken voegen we
paddle.draw()toe aan onzedraw()functie
Een lokale variabele bestaat enkel binnen een functie. Elke keer dat de functie wordt opgeroepen krijgt deze opnieuw een waarde, en wanneer de functie ten einde is verdwijnt deze weer. Als je een variabele binnen een functie een waarde geeft, (bv.
x = 1, ofpaddle = Actor("paddle"), dan gaat Python er van uit dat dit een lokale variabele is. De functie is als het ware een krachtschild, de variabele komt daar niet buiten.Een globale variabele is niet gebonden aan één functie, maar kan je van eender waar in je programma gebruiken, maar je moet dan wel in iedere functie waar je ze een waarde geeft (met
=) aangeven dat je de globale variabele bedoelt. Dat doe je metglobal.In ons geval is
paddleeen globale variabele, en in de start functie geven we deze een waarde (paddle = Actor("player"), dus schrijven weglobal paddleaan het begin van de functie. Zo weet Python dat we de globale variable wil wijzigen. Doe je dat niet, dan gaat Python er van uit dat het een lokale variabele is, en blijft de globale variabele ongewijzigd.
Stap 2: De bal toevoegen
Nu voegen we de stuiterende bal toe die voor het breken zorgt. We herhalen niet
de hele code deze keer, maar tonen alleen wat nieuw of gewijzigd is. We voegen
code toe bovenaan, in de start_game functie, en in de draw functie.
ball = None # NIEUW
ball_vx = 0 # NIEUW
ball_vy = 0 # NIEUW
def start_game():
global paddle, ball, ball_vx, ball_vy # GEWIJZIGD
# ...player...
# NIEUW
ball = Actor("ball-small")
ball.center = (WIDTH / 2, HEIGHT / 2)
ball_vx = 3
ball_vy = -3
def draw():
# ...
ball.draw() # NIEUW
Nieuwe variabelen:
ball_vx: horizontale snelheid (3 pixels per frame)ball_vy: verticale snelheid (-3 pixels per frame, naar boven)
Stap 3: De paddle laten bewegen met de muis
De paddles moeten beweeglijk zijn! We voegen muisbesturing toe.
def on_mouse_move(pos):
paddle.x = max(20, min(WIDTH - 20, pos[0]))
Nieuwe functie:
on_mouse_move(pos): Pygame Zero roept deze automatisch aan wanneer je muis beweegtmax(20, min(...)): beperkt de paddle beweging binnen het scherm (met 20 pixels marge)
Deze combinatie van max + min is een veel voorkomend patroon, om te zorgen dat een waarde binnen bepaalde grenzen blijft. Bijvoorbeeld,
Check voor jezelf: Neem deze code:
max(0, min(x, 10)). Wat is het resultaat, bijx=5,x=-2,x=2,x=12?
Test: Als je nu speelt, zie je de paddle bewegen met je muis!
Stap 4: De bal laten bewegen
Nu gaan we de bal echt laten stuiteren met een update functie. Hier is wel wat werk aan. We moeten afzonderlijke tests doen voor de zijkanten, de onderkant (= Game Over), en de paddle.
update() is net als draw() een speciale functie die door Pygame Zero wordt opgeroepen, wel 60 keer per seconde. Dit is de uitgelezen plaats om dingen te laten bewegen.
def update():
# Geef aan dat we de globale variabelen ball_vx en ball_vy gaan wijzigen
global ball_vx, ball_vy
# Ball bewegen op basis van de snelheid
ball.x += ball_vx
ball.y += ball_vy
# Botsen met de zijkanten
if ball.left <= 0 or ball.right >= WIDTH:
ball_vx *= -1
# Botsen met de bovenkant
if ball.top <= 0:
ball_vy *= -1
# Ball raakt de onderkant: GAME OVER
if ball.bottom >= HEIGHT:
start_game()
return
# Botsen met de paddle
if paddle.colliderect(ball):
ball_vy *= -1
Je ziet op verschillende plaatsen ball_vx *= -1, of ball_vy *= -1. Dit is hetzelfde als ball_vy = ball_vy * -1. Met andere woorden, de nieuwe verticale (of horizontale) snelheid van de bal is de oorspronkele snelheid, maal -1. Vermenigvuldigen met -1 verandert het teken. Was de waarde positief, dan is ze nu negatief. Was ze negatief, dan is ze nu positief.
Op deze manier doen we de bal “botsen”. Gaat de bal naar links en raakt de kant, dan gaat deze nu naar rechts. De verticale snelheid (naar boven of beneden) blijft ongewijzigd.
Om te testen of de bal de paddle raakt gebruiken we paddle.colliderect(ball). colliderect is deel van de Actor klasse die we gebruikt hebben voor de paddle. “collide” wil zegen “in aanraking komen met”, “rect” staat hier voor rechthoek. We testen dus of the bal de rechthoek van de paddle raakt.
Stap 5: De bakstenen toevoegen
Tijd voor visuele verrijking met kleurtjes! We gaan een rooster van bakstenen toevoegen.
bricks = [] # NIEUW
def start_game():
# ...
# NIEUW
bricks.clear()
for row in range(8):
for col in range(16):
x = 20 + col * 40
y = 40 + row * 18
color_index = 1 + (row % 8)
brick = Actor(f"brick{color_index}")
brick.center = (x, y)
bricks.append(brick)
def draw():
# NIEUW
for brick in bricks:
brick.draw()
We houden de bakstenen bij in een lijst, bricks. Aan het begin van een nieuw spel maken we de lijst eerst leeg met clear, en dan voegen we al de bakstenen toe. In de draw functie overlopen we dan de lijst om alle stenen te tekenen.
We gebruiken opnieuw Actors voor de bakstenen. We hebben 8 verschillende afbeeldingen, brick1.png, …, brick8.png. We hebben echter meer dan 8 rijen. Met de modulo operator % 8 (rest bij deling door 8) zorgen we dat we een waarde tussen 0 en 7 krijgen. Voegen we er nog 1 aan toe, dan wordt dat tussen 1 en 8. Met een Python “format string” (f"...{...}...") maken we de juiste bestandsnaam.
0 % 8 #=> 0
1 % 8 #=> 1
...
7 % 8 #=> 7
8 % 8 #=> 0
9 % 8 #=> 1
10 % 8 #=> 2
f"abc{1+1}" #=> "abc2"
De bakstenen:
- 8 rijen × 16 kolommen = 128 bakstenen
- 40 pixels breedte tussen bakstenen, 18 pixels hoogte
- Kleurvariatie met
color_index = 1 + (row % 8)enf"brick{color_index}"
Een leuke variant: Je kan zoal rij als kolomnummer gebruiken om de kleur van de baksteen te bepalen, dat ziet er visueel nog iets interessanter uit: `color_index = 1 + ((row+col) % 8)
Stap 6: De bakstenen laten breken
Als laatste stap maken we de bakstenen kapot wanneer de bal ze raakt! We overlopen de lijst met stenen, en testen of de bal de steen raakt. Als dat zo is, dan gebruiken we de remove functie om de baksteen uit de lijst te verwijderen. Dan zal ze niet meer worden getekend.
Een lege lijst is “falsey” in Python, we kunnen dus testen met not bricks of de lijst leeg is.
def update():
# NIEUW
for brick in bricks:
if brick.colliderect(ball):
bricks.remove(brick)
ball_vy *= -1
break
# NIEUW
if not bricks:
start_game()
Nieuwe functionaliteit:
- Brick collision detection: test elke baksteen of deze geraakt wordt door de bal
- Verwijder brick:
bricks.remove(brick)haalt de brick uit de lijst - Bal botst:
ball_vy *= -1laat de bal weer naar beneden stuiteren - Game win:
if not bricks:controleert of alle bakstenen kapot zijn
Het volledige spel:
from pgzero.actor import Actor
import pgzrun
TITLE = "My Breakout Game"
WIDTH = 640
HEIGHT = 480
paddle = None
ball = None
ball_vx = 0
ball_vy = 0
bricks = []
def start_game():
global paddle, ball, ball_vx, ball_vy
paddle = Actor("player")
paddle.center = (WIDTH / 2, HEIGHT - 30)
ball = Actor("ball-small")
ball.center = (WIDTH / 2, HEIGHT / 2)
ball_vx = 3
ball_vy = -3
bricks.clear()
for row in range(8):
for col in range(16):
x = 20 + col * 40
y = 40 + row * 18
color_index = 1 + (row % 8)
brick = Actor(f"brick{color_index}")
brick.center = (x, y)
bricks.append(brick)
def draw():
screen.fill((30, 30, 80))
paddle.draw()
ball.draw()
for brick in bricks:
brick.draw()
def on_mouse_move(pos):
paddle.x = max(20, min(WIDTH - 20, pos[0]))
def update():
global ball_vx, ball_vy
# Move ball
ball.x += ball_vx
ball.y += ball_vy
# Wall collisions
if ball.left <= 0 or ball.right >= WIDTH:
ball_vx *= -1
if ball.top <= 0:
ball_vy *= -1
# Game over if ball hits bottom
if ball.bottom >= HEIGHT:
start_game()
return
# Paddle collision
if paddle.colliderect(ball):
ball_vy *= -1
# Add angle based on where ball hits paddle
offset = (ball.x - paddle.x) / 50
ball_vx += offset
# Brick collisions
for brick in bricks:
if brick.colliderect(ball):
bricks.remove(brick)
ball_vy *= -1
break
# Check win
if not bricks:
start_game()
start_game()
pgzrun.go()
Uitbreidingen voor gevorderden
Ons Breakout spel is nu compleet! Enkele uitbreidingsideeën:
Punten systeem:
score = 0 # toevoegen bovenaan
# in draw functie toevoegen:
screen.draw.text(f"Score: {score}", topleft=(10, 10))
Levens systeem:
lives = 3 # toevoegen bovenaan
Power-ups: zorg voor speciale bakstenen die
- de paddle groter maken
- meerdere ballen geven
- superball die door meerdere bakstenen gaat
Levels: maak verschillende start posities en layouts per level