In sechs Schritten baust du «Sternenfänger»: von einem leeren Fenster bis zu einem Spiel, das online im Browser läuft. Sterne fangen, Steinen ausweichen. Unterwegs lernst du, wie Spiele aufgebaut sind: die Spielschleife, Delta-Time, Klassen, Vererbung und Szenen. Genau so ist auch die Spielvorlage aufgebaut, mit der du in der Richtung Spieleprogrammierung startest.
Du kannst das Tutorial vor dem Projekt durcharbeiten oder einfach zum Nachlesen benutzen. Zu den Schritten 1 bis 4 gibt es je ein lauffähiges Programm. Lies den Text, starte das Programm, lies den Code und probiere die Aufgaben aus.
| Schritt | Thema | Programm |
|---|---|---|
| 1 | Die Spielschleife und Delta-Time | schritt1_fenster.py |
| 2 | Die erste Klasse: der Spieler | schritt2_spieler.py |
| 3 | Viele Objekte: Gruppen, Zeitgeber, Kollision | schritt3_sterne.py |
| 4 | Vererbung und Polymorphie | schritt4_vererbung.py |
| 5 | Szenen und eigene Dateien | die Spielvorlage |
| 6 | Online stellen | die Spielvorlage |
Du brauchst Python (ab 3.10) und VS Code. Installiere einmal im Terminal von VS Code pygame-ce:
python -m pip install pygame-ce
Die Programme liegen an zwei Orten, du kannst den nehmen, der dir lieber ist:
tutorial/.
Starten im Hauptordner des Repos mit python tutorial/schritt1_fenster.py.beispiele/sternenfaenger/schritte/
(oder auf GitHub).Im Arbeitsordner öffnest du im Terminal den Ordner und startest ein Programm:
cd beispiele/sternenfaenger/schritte
python schritt1_fenster.py
pygame oder pygame-ce? Beide lädt man mit
import pygame, sie vertragen sich aber nicht im selben Python. Beim Start muss im Terminalpygame-cestehen. Steht dort nurpygame 2.6…, zuerstpython -m pip uninstall -y pygame pygame-ceund dann nochmalspython -m pip install pygame-ce.
Programm: schritt1_fenster.py. Ein gelber Kreis fliegt von links nach rechts.
Ein Spiel ist eine Schleife, die etwa 60-mal pro Sekunde dasselbe tut:
Eingaben lesen → aktualisieren → zeichnen → (von vorne)
while laeuft:
dt = min(uhr.tick(60) / 1000, 0.05)
for ereignis in pygame.event.get(): # 1. Eingaben
if ereignis.type == pygame.QUIT:
laeuft = False
x += tempo * dt # 2. Aktualisieren
flaeche.fill((18, 22, 40)) # 3. Zeichnen
pygame.draw.circle(flaeche, (255, 210, 60), (round(x), HOEHE // 2), 20)
pygame.display.flip()
await asyncio.sleep(0)
Delta-Time (dt) ist die Zeit in Sekunden seit dem letzten Bild. Das Tempo gibst du in Pixel pro Sekunde an,
und in jedem Bild bewegst du um tempo * dt:
| Rechner | Bilder pro Sekunde | dt | Weg pro Bild bei 200 Pixel/s | Weg pro Sekunde |
|---|---|---|---|---|
| schnell | 60 | 0.017 s | 3.3 Pixel | 200 Pixel |
| langsam | 30 | 0.033 s | 6.7 Pixel | 200 Pixel |
Auf jedem Gerät ist der Kreis also gleich schnell. Würdest du einfach x += 3 rechnen, liefe das Spiel auf einem langsamen
Laptop in Zeitlupe. Mit min(..., 0.05) gibt es nach einer Pause (zum Beispiel einem Tab-Wechsel im Browser) keinen riesigen
Sprung. await asyncio.sleep(0) braucht es später für den Browser: Dort gibt die Zeile die Kontrolle kurz an die Webseite zurück.
Probier aus
tempo. Wie schnell ist 50, wie schnell 1000?flaeche.fill(...). Was passiert, und warum?uhr.tick(60) auf uhr.tick(30). Der Kreis bleibt gleich schnell. Warum?uhr.tick(10)? Jetzt wird er langsamer. Welche Zeile ist daran schuld, und ist das gut oder schlecht?Funktioniert es?
tick(30) gleich schnell bleibt.Programm: schritt2_spieler.py. Du bewegst eine blaue Schale mit den Pfeiltasten, mit A/D oder der Maus.
Eine Klasse ist ein Bauplan, ein Objekt ist ein Ding, das nach diesem Bauplan gebaut wurde.
Die Klasse Spieler legt fest, welche Daten ein Spieler hat (Bild, Position) und was er kann (update):
class Spieler(pygame.sprite.Sprite):
GESCHWINDIGKEIT = 420 # Pixel pro Sekunde
def __init__(self):
super().__init__()
self.image = pygame.Surface((80, 18), pygame.SRCALPHA)
self.rect = self.image.get_rect(midbottom=(BREITE / 2, HOEHE - 20))
self.x = float(self.rect.centerx)
def update(self, dt):
... # Tasten lesen, self.x ändern, am Rand stoppen
self.rect.centerx = round(self.x)
In der Schleife steht danach nur noch spieler.update(dt) und flaeche.blit(spieler.image, spieler.rect).
Die Schleife muss nicht wissen, wie sich der Spieler bewegt. Das weiss der Spieler selbst.
pygame.sprite.Sprite ist eine fertige Klasse von pygame. Ein Sprite hat ein Bild (image) und ein Rechteck (rect),
das sagt, wo das Bild liegt.self.x ist eine Kommazahl, rect kennt nur ganze Pixel. Ohne self.x gingen kleine Schritte verloren.Probier aus
GESCHWINDIGKEIT).Funktioniert es?
Programm: schritt3_sterne.py. Sterne fallen herunter, und jeder gefangene Stern gibt einen Punkt.
Neu sind drei Werkzeuge, die du in fast jedem Spiel brauchst:
Gruppen. Statt einer Liste von Sternen gibt es eine pygame.sprite.Group. Ein einziger Befehl bewegt oder zeichnet alle:
sterne = pygame.sprite.Group()
sterne.update(dt) # ruft update(dt) bei jedem Stern auf
sterne.draw(flaeche) # zeichnet jeden Stern
Ein Stern, der unten aus dem Bild fällt, entfernt sich mit self.kill() selbst aus der Gruppe.
Zeitgeber. Etwas soll alle 0.8 Sekunden passieren. Dafür zählst du mit dt herunter:
bis_zum_naechsten -= dt
if bis_zum_naechsten <= 0:
sterne.add(Stern(random.randint(20, BREITE - 20), 180))
bis_zum_naechsten = 0.8
Kollision. spritecollide liefert alle Sterne, deren Rechteck den Spieler berührt. Mit dokill=True verschwinden sie gleich:
gefangen = pygame.sprite.spritecollide(spieler, sterne, dokill=True)
punkte += len(gefangen)
Probier aus
self.kill() und gib len(sterne) im Fenster aus. Was passiert nach einer Minute?Funktioniert es?
dt besser ist als «alle 50 Bilder».Programm: schritt4_vererbung.py. Jetzt fallen auch graue Steine. Wer einen fängt, hat verloren.
Stern und Stein fallen gleich, sehen aber anders aus und wirken anders. Statt den Code zu kopieren, schreibst du eine gemeinsame Oberklasse. Die Unterklassen erben das Fallen und legen nur fest, was anders ist:
FallendesObjekt
update(dt): fallen, unten verschwinden
bild_erstellen() → muss die Unterklasse festlegen
beim_fangen() → muss die Unterklasse festlegen
/ \
Stern Stein
gelber Stern, +1 Punkt graues Quadrat, verloren
class Stern(FallendesObjekt):
def bild_erstellen(self):
... # gelben Stern zeichnen
def beim_fangen(self, szene):
szene.punkte += 1
class Stein(FallendesObjekt):
def bild_erstellen(self):
... # graues Quadrat zeichnen
def beim_fangen(self, szene):
szene.verloren()
Beim Fangen fragt das Spiel nicht, was gefangen wurde. Es ruft bei jedem Objekt dieselbe Methode auf, und jedes Objekt reagiert auf seine Art. Das heisst Polymorphie:
for objekt in pygame.sprite.spritecollide(self.spieler, self.fallende, dokill=True):
objekt.beim_fangen(self)
Willst du später ein Herz, das ein Leben schenkt, schreibst du eine neue Unterklasse. Am Code zum Fangen änderst du nichts.
Ausserdem ist die Runde jetzt eine Klasse SpielSzene. Sie hält alles zusammen, was zum Spiel gehört (Spieler, Gruppe, Punkte,
Zeit), und hat drei Methoden: ereignis, aktualisieren und zeichnen. Eine neue Runde ist einfach ein neues Objekt:
szene = SpielSzene().
Probier aus
random.random() < 0.3).Goldstern, die 5 Punkte gibt. Ergänze sie dort, wo art = ... ausgewählt wird.Funktioniert es?
Stern keine eigene Methode update braucht.Code: die Spielvorlage, Ordner spiel/.
In Schritt 4 muss die Hauptschleife wissen, dass es ein Game Over gibt und dass die Leertaste neu startet. Kommen ein Menü, eine Pause und Levels dazu, wird die Schleife schnell unübersichtlich. Die Lösung sind Szenen: Jeder Zustand des Spiels ist eine eigene Klasse, und genau eine Szene ist aktiv. Das nennt man einen Zustandsautomaten:
Menü ──Leertaste──▶ Spiel ──Stein──▶ Game Over
▲ │ │
└──────Escape────────┘ │
▲───Leertaste──────┘
Alle Szenen erben von der Oberklasse Szene mit den drei Methoden aus Schritt 4. Die Klasse Spiel hält die aktuelle Szene und
gibt jedes Bild an sie weiter. Wann gewechselt wird, entscheidet die Szene selbst:
class MenueSzene(Szene):
def ereignis(self, ereignis):
if ist_weiter(ereignis):
self.spiel.wechsle_zu(SpielSzene(self.spiel))
Weil das Programm grösser wird, ist es auf mehrere Dateien verteilt:
| Datei | Inhalt |
|---|---|
spiel/main.py |
Klasse Spiel mit der Spielschleife, Startpunkt |
spiel/szenen.py |
Szene, MenueSzene, SpielSzene, GameOverSzene |
spiel/figuren.py |
Spieler, FallendesObjekt, Stern, Stein (wie in Schritt 4) |
spiel/einstellungen.py |
Grösse, Farben, Tempo an einem Ort |
Lies die Dateien in dieser Reihenfolge: einstellungen.py, figuren.py, szenen.py, main.py. Fast alles kennst du schon.
Probier aus
einstellungen.py Titel und Farben.PauseSzene: Die Taste P hält das Spiel an und setzt es fort.
Tipp: Die Pause-Szene merkt sich die laufende SpielSzene und wechselt am Ende zu ihr zurück.Funktioniert es?
szenen.py die Stelle, an der vom Spiel zu Game Over gewechselt wird.Das Werkzeug pygbag übersetzt dein Spiel so, dass es im Browser läuft, auch auf dem Handy.
https://<dein-name>.github.io/<repo>/.Vorher lokal im Browser testen, im Ordner der Vorlage (dann http://localhost:8000 öffnen):
python -m pip install pygbag
python -m pygbag spiel
Damit es im Browser läuft: Startdatei main.py, in der Schleife await asyncio.sleep(0), kein input() und kein
time.sleep(), Töne nur als .ogg.
Ideen: Jump’n’Run mit Levels · Space Shooter mit Gegnerwellen · Top-down-Abenteuer mit Karte · Puzzlespiel · Tower Defense
Zum Weiterlesen und Weiterbauen