Zum Inhalt

Der Mausinator

Lernziele

  • Gruppenkollisionen zwischen verschiedenen Sprite-Gruppen
  • Soundeffekte laden und abspielen
  • Event: Tastendruck (py.KEYDOWN) vs. Taste loslassen (py.KEYUP)
  • Dynamisches Erzeugen von Sprites (Explosionen)

Übersicht: Das Spiel

Du wirst ein komplettes Spiel programmieren: Die Maus schiesst auf Münzen und versucht, alle in Zeit zu eliminieren.

  • Maus: Kann mit Pfeiltasten links/rechts bewegt werden
  • Münzen: Animierte Münzen an zufälligen Positionen
  • Schüsse: Werden mit der Leertaste abgefeuert
  • Explosionen: Animiert, wenn ein Schuss eine Münze trifft
  • Soundeffekte: Schussgeräusch + Explosionsgeräusch
  • Endbildschirm: Zeigt die benötigte Zeit

Vorlage: Lade hier einf_pygame_06_mausinator.zip herunter

Schritt 1: Soundeffekte laden und initialisieren

Soundeffekte müssen vor der Hauptschleife geladen werden.

  1. Füge nach der Schriftinitialisierung folgende Zeilen hinzu:

    # Sounds laden
    schusse = py.mixer.Sound("sound/gun-shot.wav")
    schusse.set_volume(0.3)
    explosion = py.mixer.Sound("sound/explosion-012.mp3")
    explosion.set_volume(0.4)
    
    • py.mixer.Sound() lädt die Audiodatei
    • set_volume() regelt die Lautstärke (0.0 = stumm, 1.0 = maximal)
    • Die Dateien befinden sich im sound/ Ordner der Vorlage
  2. Die Sounds werden später in der Hauptschleife mit py.mixer.Sound.play() abgespielt.

Schritt 2: Die neuen Sprite-Klassen

Die Bullet-Klasse

Schüsse sind bewegliche Objekte, die sich selbst nach oben bewegen.

Erstelle eine neue Klasse Bullet:

class Bullet(py.sprite.Sprite):
    def __init__(self, x, y):
        super().__init__()
        self.image = py.image.load("res/bullet.png").convert()
        self.rect = self.image.get_rect()
        self.rect.centerx = x
        self.rect.y = y
        self.step_size = 20

    def update(self):
        # Schuss nach oben bewegen
        self.rect.y = self.rect.y - self.step_size
        # Wenn der Schuss oben angekommen, löschen
        if self.rect.y + self.rect.height < 0:
            self.kill()

Detaillierte Erklärung der update()-Funktion

Die update()-Funktion wird von Pygame bei jedem Schleifendurchgang automatisch aufgerufen (wenn du all_sprites.update() aufrufst). Sie hat zwei Aufgaben:

1. Schuss nach oben bewegen:

self.rect.y = self.rect.y - self.step_size

Dies geschieht bei jedem Durchgang. Wenn der Schuss z.B. bei y=500 ist und step_size=20:

  1. Durchgang: y = 500 - 20 = 480
  2. Durchgang: y = 480 - 20 = 460
  3. Durchgang: y = 460 - 20 = 440
  4. ...und so weiter

Warum subtrahieren? Weil in Pygame die y-Achse von oben nach unten läuft (y=0 ist oben, y=800 ist unten).

2. Speicher freigeben, wenn der Schuss das Fenster verlässt:

if self.rect.y + self.rect.height < 0:
    self.kill()

Dies ist sehr wichtig! Schüsse dürfen nicht endlos weiterfliegen, sonst verbrauchen sie Speicher.

Wie funktioniert die Bedingung?

Bullet-Logik

  • self.rect.y = Position des oberen Randes des Schusses
  • self.rect.height = Höhe des Schusses (z.B. 10 Pixel)
  • self.rect.y + self.rect.height = Position des unteren Randes des Schusses

Wenn der untere Rand kleiner als 0 ist, bedeutet das: Der Schuss ist komplett oben aus dem Fenster raus!

Beispiel: Schuss hat Höhe 10 und oberer Rand ist bei y=-5 - Unterer Rand: -5 + 10 = 5 (noch sichtbar, nicht gelöscht)

Schuss hat Höhe 10 und oberer Rand ist bei y=-12 - Unterer Rand: -12 + 10 = -2 (komplett weg, gelöscht!)

Wichtig: self.kill() entfernt das Objekt aus allen Gruppen (all_bullets und all_sprites), sodass es nicht mehr gezeichnet oder aktualisiert wird.

Die Explosion-Klasse

Explosionen sind animierte visuellen Effekte, die automatisch verschwinden, wenn die Animation zu Ende ist.

Erstelle die Explosion Klasse:

class Explosion(py.sprite.Sprite):
    def __init__(self, center):
        super().__init__()
        # 40 Bilder für die Explosionsanimation
        self.animation = load_images("res/explosion/", "e", ".gif", 40, 128, 128)
        self.costume = 0
        self.image = self.animation[self.costume]
        self.rect = self.image.get_rect()
        self.rect.center = center

    def update(self):
        self.costume = (self.costume + 1) % 40
        self.image = self.animation[self.costume]
        # Nach der Animation (Bild 38) löschen
        if self.costume > 38:
            self.kill()

Detaillierte Erklärung der Explosion-Klasse

Unterschied zu anderen Sprites: Eine Explosion wird nur einmal erzeugt, lädt alle ihre Bilder auf einmal, spielt die Animation ab und löscht sich dann selbst.

In der __init__()-Funktion:

self.animation = load_images("res/explosion/", "e", ".gif", 40, 128, 128)
Alle 40 Explosionsbilder (e0.gif, e1.gif, ..., e39.gif) werden einmal beim Erzeugen geladen und in self.animation gespeichert.

self.rect.center = center
Dies ist neu! Der Parameter center ist eine Koordinate (x, y) – die Stelle, wo die Münze getroffen wurde. Mit self.rect.center = center wird die Explosion genau dort positioniert.

Die update()-Funktion erklärt:

Diese Funktion wird bei jedem Schleifendurchgang aufgerufen und hat zwei Aufgaben:

1. Animation abspielen:

self.costume = (self.costume + 1) % 40
self.image = self.animation[self.costume]

Das Prozentzeichen % ist der Modulo-Operator. Hier ist, was passiert:

Durchgang Rechnung Ergebnis Bedeutung
1. (0 + 1) % 40 = 1 costume = 1 Zeige Bild 1
2. (1 + 1) % 40 = 2 costume = 2 Zeige Bild 2
... ... ... ...
39. (38 + 1) % 40 = 39 costume = 39 Zeige Bild 39
40. (39 + 1) % 40 = 0 costume = 0 Zeige Bild 0 (Schleife)
41. (0 + 1) % 40 = 1 costume = 1 Zeige Bild 1 (wieder vorne)

Die Modulo-Operation sorgt dafür, dass costume immer zwischen 0 und 39 bleibt – die Animation wird endlos wiederholt!

2. Animation stoppen und löschen:

if self.costume > 38:
    self.kill()

Aber wir wollen die Explosion ja nicht endlos laufen lassen! Daher wird geprüft:

  • Wenn costume grösser als 38 ist (also 39), dann ist die Animation fast vorbei
  • Mit self.kill() wird die Explosion gelöscht

Warum > 38 statt > 39?

Weil die Bilder von 0-39 gehen (40 Bilder insgesamt). Wenn costume = 39, haben wir das letzte Bild gerade angezeigt. Beim nächsten Durchgang würde costume = 0 und die Animation würde von vorne beginnen. Mit der Bedingung > 38 stoppen wir dies und löschen die Explosion nach dem letzten Bild.

Zusammengefasst: - Die Explosion spielt 40 Bilder ab (0-39) - Bei jedem Durchgang wird das nächste Bild gezeigt - Nach Bild 39 wird die Explosion aus dem Speicher entfernt - Alles geschieht automatisch durch all_sprites.update() in der Hauptschleife

Schritt 3: Gruppenkollisionen - Teil 2

Im Gegensatz zu vorher überprüfen wir jetzt Kollisionen zwischen zwei Gruppen (Schüsse vs. Münzen).

  1. Verwende py.sprite.groupcollide() statt spritecollide():

    def is_a_collision():
        # groupcollide(group1, group2, dokill_group1, dokill_group2, collided=None)
        kollisionsliste = py.sprite.groupcollide(all_bullets, all_coins, True, True, 
                                                 collided=py.sprite.collide_mask)
        if len(kollisionsliste) != 0:
            # Für jede Kollision eine Explosion erstellen
            for i in kollisionsliste.values():
                for j in i:
                    all_sprites.add(Explosion(j.rect.center))
            return True
        else:
            return False
    

    Erklärung der Parameter: - all_bullets: Gruppe der Schüsse - all_coins: Gruppe der Münzen - True, True: Beide Gruppen-Objekte sollen gelöscht werden bei Kollision - collided=py.sprite.collide_mask: Pixel-perfekte Kollisionserkennung - Rückgabewert: Dictionary mit Kollisionen: {Bullet: [Coin]}

  2. Wenn Kollisionen gefunden wurden, erstelle für jede Münze eine Explosion an ihrer Position.

Schritt 4: Tasten-Events verarbeiten

Unterscheide zwischen verschiedenen Tastendrücken:

  1. Ergänze die Event-Verarbeitung in der Hauptschleife:

    for event in py.event.get():
        if event.type == py.QUIT:
            game_is_running = False
        # Taste LOSGELASSEN (nicht gedrückt!)
        elif event.type == py.KEYDOWN and event.key == py.K_ESCAPE:
            game_is_running = False
        # Leertaste GEDRÜCKT
        elif event.type == py.KEYDOWN and event.key == py.K_SPACE:
            bullet = Bullet(maus.rect.centerx - 7, maus.rect.y)
            bullet.rect.y -= bullet.rect.height
            all_bullets.add(bullet)
            all_sprites.add(bullet)
            py.mixer.Sound.play(schusse)
    

    Wichtig: - py.KEYDOWN: Taste wurde soeben gedrückt - py.KEYUP: Taste wurde soeben losgelassen (wird hier nicht verwendet) - Schüsse werden nur erzeugt, wenn die Leertaste gedrückt wird

  2. Pfeiltasten werden weiterhin mit py.key.get_pressed() in der key_handler() Funktion verarbeitet.

Schritt 5: Hauptschleife zusammenbinden

Die Hauptschleife folgt diesem Ablauf:

  1. Events verarbeiten (Fenster schliessen, Schuss abfeuern)
  2. Tastatur abfragen (Pfeiltasten für Bewegung)
  3. Kollisionen prüfen (Schuss trifft Münze?)
  4. Sprites aktualisieren (all_sprites.update())
  5. Alles zeichnen (Hintergrund, Sprites, Text)
while game_is_running:
    # Events
    for event in py.event.get():
        # ...

    if which_window == 0:
        # Zeit messen
        counter = (py.time.get_ticks() - start) // 1000

        # Tasten verarbeiten
        key_handler()

        # Kollisionen prüfen
        if is_a_collision():
            anzahl_coins = len(all_coins)
            py.mixer.Sound.play(explosion)

        # Update
        all_sprites.update()

        # Zeichnen
        screen.fill([255, 255, 255])
        all_sprites.draw(screen)
        draw_score(anzahl_coins, counter)

        # Spiel beendet?
        if anzahl_coins == 0:
            which_window = 1
            time_needed = counter
    else:
        # Endbildschirm
        screen.fill([255, 255, 255])
        draw_ende(time_needed)

    py.display.update()
    clock.tick(fps)

Teste dein Spiel.

Erweiterte Aufgaben

  1. Verstehen: Starte das Programm und spiele es. Versuche alle Münzen zu treffen.

  2. Soundlautstärke ändern: Erhöhe oder senke die Lautstärke der Soundeffekte durch Anpassung von set_volume().

  3. Schwierigkeit erhöhen:

    • Erhöhe anzahl_coins auf 10
    • Verringere step_size der Schüsse auf 15 (langsamer)
    • Münzen schneller machen: Erhöhe die Anzahl der Animationsbilder
  4. (s) Neue Sprite-Klasse: Erstelle einen Enemy, der sich von oben nach unten bewegt. Schüsse sollen auch den Enemy treffen können (Explosion).

  5. (s) Score-System: Zähle die Anzahl der getroffenen Münzen und zeige sie im Endbildschirm an.

  6. (s) Weitere Soundeffekte: Füge einen Sound hinzu, der abgespielt wird, wenn das Spiel endet.