Ein Spiel braucht Gegner

So hübsch das Spiel jetzt schon ist: Es bietet keine Schwierigkeit. Nach ein paar Mal Hin-und-Her-Laufen hat der Krebs irgendwann alle Blasen erwischt. Dann passiert gar nichts mehr. Das ist eine Demo, aber noch kein Spiel. Um ein Spiel interessant zu machen, muss es eine Schwierigkeit geben, an der man auch scheitern kann. Meistens sind dies Gegner – in Form von anderen Spielfiguren oder Gegenständen, denen man zum Beispiel aus dem Weg gehen muss –, manchmal ist der Gegner auch die Zeit, die man einhalten muss.

Mein Vorschlag für dieses Spiel: Wir führen noch zusätzliche »böse Blasen« ein, die quer durch das Spiel treiben und die der Krebs nicht berühren darf, sonst verliert er. Damit gewinnt das Spiel an Reiz und kann gewonnen oder verloren werden.

Also – wie beginnen wir? Wir müssen eine Klasse für die Gegnerblasen definieren. Nennen wir sie »Giftblase« – diese Blasen darf unser Krebs nicht berühren. Sie bewegen sich nicht gerade von oben nach unten durch das Spiel, sondern sie schweben schräg von links oben nach rechts unten – zum Beispiel. Und sie sind etwas schneller als die normalen Blasen.

Definieren wir also die Klasse »Giftblase«, und schreiben wir sie hinter die Definitionen der anderen Klassen ins Programm:

class Giftblase(Actor):
speed = 5
def act(self):
ypos = self.getY()+self.speed
xpos = self.getX()+self.speed
self.setY(ypos)
self.setX(xpos)
if ypos>600:
self.setY(-10)
self.setX(randint(-400,500))

Was hier vor allem auffällt, ist, dass beim Bewegen nicht nur die y-Position verändert wird, sondern auch die x-Position. Damit bewegen sich die Blasen also immer gleichzeitig nach unten und nach rechts – diagonal.

Wenn sie unterhalb des Bildes sind, gehen sie wieder nach oben, x-Position zufällig, y-Position –10.

Nun müssen die Giftblasen aber erst mal noch erzeugt werden. Ich schlage vor, wir nehmen die mitgelieferte Grafik »peg_2.png« – das ist eine rote Kugel. Und wir erstellen erst einmal 5 Stück.

repeat 5:
gblase = Giftblase("sprites/peg_2.png")
gblase.speed = randint(4,8)
feld.addActor(gblase,Location(randint(-500,400),randint(-200,-20)))

Die Anfangspositionen der Kugeln befinden sich oberhalb des Spielfelds, tendenziell links, da sie dann ja schräg nach rechts herunterfallen.

Natürlich kannst du diese Werte anpassen, wenn du möchtest. Experimentiere gerne herum.

Auf jeden Fall haben wir jetzt 5 Giftblasen erzeugt, die zusätzlich zu den Luftblasen schräg durch das Bild fliegen. Starte das Programm!

Die roten Blasen fliegen zwischen den Luftblasen hindurch, eine ist außerhalb des Bildes.

Abbildung 18.5    Die roten Blasen fliegen zwischen den Luftblasen hindurch, eine ist außerhalb des Bildes.

Sieht doch schon mal gut aus. Nun der nächste Schritt: Wenn die roten Blasen den Krebs treffen, soll etwas passieren – sagen wir einfach mal, das Spiel ist dann beendet.

Wie programmieren wir das?

Es gibt hier verschiedene Wege, das Ziel zu erreichen. Wir könnten für die Klasse »Giftblase« ein Kollisionsereignis schreiben und den Krebs als Kollisionsobjekt hinzufügen. Das würde funktionieren.

Ich schlage hier aber noch einen anderen Weg vor. Wir fügen die Giftblasen so wie die Luftblasen auch dem Krebs als Kollisionsobjekt hinzu. Wenn wir das erst einmal einfach so ohne weitere Maßnahmen machen, dann brauchen wir nur eine Zeile:

krebs.addCollisionActor(gblase)

Diese Zeile kommt vor die Zeile, in der die Giftblase (als Objekt: gblase) dem Feld hinzugefügt wird.

Startest du das Programm jetzt, passiert das, was zu erwarten war: Die Giftblasen verschwinden bei Berührung mit dem Krebs genauso mit einem Klick wie die Luftblasen. Schließlich lösen sie jetzt ebenfalls die Kollisionsfunktion aus und werden nicht anders behandelt als die Luftblasen. Das müssen wir ändern!

Die Kollisionsfunktion muss unterscheiden, ob der Krebs eine Luftblase oder eine Giftblase getroffen hat.

Wie kann man das feststellen?

Auch hier gibt es verschiedene Wege. Der einfachste besteht darin, den Namen der zugehörigen Klasse aus dem Objekt zu lesen und ihn dann abzugleichen. Der Name der Klasse ist eine interne Eigenschaft des Objekts, die man jedoch trotzdem von außen auslesen kann. Man tut dies mit:

objekt.__class__.__name__

Vor und nach dem Wort class und name müssen dabei jeweils zwei Unterstriche stehen – denn dadurch sind ja interne Variablen in einem Objekt gekennzeichnet.

Entsprechend muss jetzt die collide-Funktion des Krebses geändert werden:

Erst mal wird geprüft, ob es sich um eine Luftblase handelt, mit der der Krebs kollidiert ist:

def collide(self,actor1,actor2):
if actor2.__class__.__name__ == "Luftblase":
feld.removeActor(actor2)
openSoundPlayer("wav/click.wav")
play()
return 0

Alles wie gehabt – aber jetzt wird der Funktion hinzugefügt, was passiert, wenn eine Giftblase berührt wird (bitte vor dem return 0 einfügen):

    elif actor2.__class__.__name__  == "Giftblase":
self.hide()
feld.refresh()
openSoundPlayer("wav/explode.wav")
play()
feld.doPause()

Als Erstes verschwindet der Krebs – das geht mit der hide()-Funktion. self ist hier natürlich der Krebs selbst, hier ist er identisch mit actor1. actor2 ist dagegen immer das Objekt, mit dem er kollidiert. Damit sofort zu sehen ist, dass der Krebs verschwunden ist, wird das Feld mit refresh() aktualisiert, dann wird der Sound explode.wav gespielt und außerdem der Spieltakt mit feld.doPause() auf Pause gesetzt.

doPause() ist sozusagen das Gegenteil von doRun(). Es beendet das automatische regelmäßige Ausführen der act()-Methode, und damit steht alles auf dem Spielfeld still, was sich sonst automatisch bewegt.

Peng! Und weg ist der Krebs.

Abbildung 18.6    Peng! Und weg ist der Krebs.

Damit ist das Spiel erst einmal so gut wie abgeschlossen. Man kann jetzt auf jeden Fall problemlos verlieren – aber wie kann man gewinnen?

Klar: Der Gewinnfall wäre der, dass man alle Luftblasen erwischt hat und nur noch die roten Giftblasen übrigbleiben. Woher weiß das Spiel nun, dass alle Luftblasen weg sind?

Nun, auch da gibt es natürlich mal wieder mehrere Möglichkeiten. Wir nehmen an dieser Stelle mal eine etwas umständlichere, aber leicht zu verstehende Variante. Wir führen einen Zähler ein – am besten als Variable von Krebs. Der Krebs zählt also die verbleibenden Luftblasen mit der Variable zaehler. Am Anfang muss der Zähler natürlich auf 100 stehen – bei jeder Kollision mit einer Luftblase geht er um eins nach unten. Steht der Zähler auf 0, ist das Spiel gewonnen.

Erst mal ändert sich einmal wieder der Anfang der Klassendefinition des Krebses:

class Krebs(Actor):
zaehler = 100

Und in der collide-Methode kommt auch erst einmal eine neue Zeile hinzu.

def collide(self,actor1,actor2):
if actor2.__class__.__name__ == "Luftblase":
self.zaehler -= 1

Wann immer eine Luftblase getroffen wird, verringert sich der Zähler um 1.

Schön und gut. Hier muss jetzt allerdings am Ende (nachdem die Luftblase gelöscht wurde) auch noch geprüft werden, ob der Zähler vielleicht auf 0 steht – dann sind nämlich alle Luftblasen weg, und der Spieler hat gewonnen:

if self.zaehler == 0:
msgDlg("Hurra! Gewonnen! Alle Luftblasen erwischt!")
feld.doPause()

Übrigens: Es gibt immer mehrere Wege, um zum Ziel zu kommen. Anstatt einen Zähler zu verwenden, wie wir es hier gemacht haben, weil das in vielen Situationen praktisch ist, hättest du auch mit den eingebauten Methoden von gamegrid ermitteln können, wie viele Actors von der Klasse »Luftblase« dem Spielfeld noch zugeordnet sind, ganz ohne Zähler. Die Funktion hätte so aussehen müssen:

anzahl_luftblasen = feld.getNumberOfActors(Luftblase)

Wenn man will, kann man natürlich auch noch einen Sieges-Sound abspielen. Das ist dir überlassen.

Dementsprechend soll es auch noch eine Nachricht geben, wenn man verloren hat – und damit wäre unser erstes Spiel FERTIG!

Hier ist der gesamte Code für das Spiel »Bubble Fight«:

from gamegrid import *
from random import randint
from soundsystem import *

class Krebs(Actor):
zaehler = 100
def collide(self,actor1,actor2):
if actor2.__class__.__name__ == "Luftblase":
self.zaehler -= 1
feld.removeActor(actor2)
openSoundPlayer("wav/click.wav")
play()
if self.zaehler == 0:
msgDlg("Hurra! Gewonnen! Alle Luftblasen erwischt!")
feld.doPause()
elif actor2.__class__.__name__ == "Giftblase":
actor1.hide()
feld.refresh()
openSoundPlayer("wav/explode.wav")
play()
feld.doPause()
msgDlg("Verloren. Blasen übrig: "+str(self.zaehler))
return 0

class Luftblase(Actor):
speed = 3
def act(self):
ypos = self.getY()+self.speed
self.setY(ypos)
if ypos>600:
self.setY(-10)
self.speed = randint(2,8)

class Giftblase(Actor):
speed = 5
def act(self):
ypos = self.getY()+self.speed
xpos = self.getX()+self.speed
self.setY(ypos)
self.setX(xpos)
if ypos>600:
self.setY(-10)
self.setX(randint(-400,500))

def tasteGedrueckt(tastencode):
xpos = krebs.getX()
if tastencode == 37: # links
if xpos > 30:
krebs.setX(xpos - 5)
elif tastencode == 39: # rechts
if xpos < 770:
krebs.setX(xpos + 5)

feld = GameGrid(800,600)
feld.setTitle("Krebsspiel")
feld.setBgColor(255,255,255)
krebs = Krebs("sprites/crab.png")
feld.addActor(krebs,Location(400,550))
repeat 100:
blase = Luftblase("sprites/bubble1.png")
blase.speed = randint(2,6)
krebs.addCollisionActor(blase)
feld.addActor(blase,Location(randint(30,770),randint(-570,-30)))
repeat 5:
gblase = Giftblase("sprites/peg_2.png")
gblase.speed = randint(4,8)
krebs.addCollisionActor(gblase)
feld.addActor(gblase,Location(randint(-500,400),randint(-200,-20)))

feld.setSimulationPeriod(20)
feld.addKeyRepeatListener(tasteGedrueckt)
feld.show()
feld.doRun()
Aufgaben

Aufgabe 1: Teste das Spiel, und ändere die Werte für die Anzahl der Luftblasen, Anzahl der Giftblasen und Geschwindigkeiten der Blasen so ab, dass du es nicht zu leicht und nicht zu schwer findest. Du kannst auch noch mehr ändern, wie die Positionen und Bewegungsrichtungen der Blasen. Experimentiere herum!

Aufgabe 2: Spielerweiterung: Lass den Krebs nicht nur nach links und rechts gehen, sondern erlaube auch noch, ihn nach oben und unten zu bewegen. Versuche, die Funktion für die Steuerung selbst zu erweitern, bevor du den Code weiter unten anschaust. Der Tastencode für »Pfeil hoch« ist 38 und für »Pfeil runter« 40. Statt der x-Position musst du natürlich bei hoch und runter die y-Position verändern.

Geschafft?

Hier eine Möglichkeit, wie du Aufgabe 2 lösen kannst (nur falls du es nicht schon selbst hinbekommen hast):

def tasteGedrueckt(tastencode):
xpos = krebs.getX()
ypos = krebs.getY()
if tastencode == 37: # links
if xpos > 30:
krebs.setX(xpos - 5)
elif tastencode == 39: # rechts
if xpos < 770:
krebs.setX(xpos + 5)
elif tastencode == 38: # hoch
if ypos > 30:
krebs.setY(ypos - 5)
elif tastencode == 40: # runter
if ypos < 570:
krebs.setY(ypos + 5)

Ob das Spiel dadurch leichter wird? Ich denke nicht – aber es wird auf jeden Fall noch ein bisschen interessanter. Wenn es dir jetzt viel zu schwer ist, gehe zurück zu Aufgabe 1, und passe die Parameter an, bis alles für dich angemessen funktioniert. Denke daran, dass du auch die Spielsteuerungsleiste bei der Erstellung des Spielfelds einblenden kannst, um Schritt für Schritt zu testen.