Kollision: Interaktion zwischen Spielfiguren
Bei den meisten Spielen geht es darum, dass Figuren sich gegenseitig berühren oder auch vermeiden müssen. Es muss hier also auf ein Ereignis reagiert werden, das eintritt, wenn zwei Figuren sich überlappen. Dieses Ereignis nennt sich Kollisionsereignis. Und natürlich hat gamegrid auch ein klar definiertes Verfahren, wie damit umgegangen wird. Um Kollisionen in einem Programm verarbeiten zu können, sind im gamegrid-System zwei Dinge erforderlich:
-
Das Objekt (die Spielfigur), mit dem die Hauptfigur kollidieren kann, muss mit der Methode addCollisionActor() der Hauptfigur hinzugefügt werden und ist dann in dessen Liste der Kollisionsobjekte.
-
In der Hauptfigur, die vom Spieler gesteuert wird, muss eine Methode collide() definiert werden, die automatisch ausgelöst wird, wenn sie mit einem Kollisionsobjekt aus der Liste zusammenstößt. Die Methode erhält als Parameter automatisch zwei Werte, nämlich die zwei Objekte, die miteinander kollidiert sind.
Also konkret: Der Krebs braucht eine Funktion mit dem Namen collide(), die alle Aktionen enthält, die bei einer Kollision ausgeführt werden sollen. Diese Funktion wird aber nur aufgerufen, wenn der Krebs mit einem Objekt kollidiert, das in seiner Kollisionsliste steht.
Also muss jede Luftblase, die erstellt wird, gleich nach der Erstellung dem Krebs in die Kollisionsliste geschrieben werden. Das geschieht mit krebs.addCollisionActor(blase)
Die Erstellung des Luftblase-Objekts ändert sich also folgendermaßen:
blase = Luftblase("sprites/bubble1.png")
krebs.addCollisionActor(blase)
feld.addActor(blase,Location(400,-10))
Die Zeile in der Mitte ist neu – damit wird das Luftblasenobjekt in die Liste der Kollisionsobjekte für den Krebs eingetragen. Damit ist der Krebs bereit für die Kollision mit der Luftblase. Nun müssen wir ihm nur noch sagen, was er im Fall eines Zusammenstoßes machen soll.
Also legen wir in der Klassendefinition für Krebs (die bisher ja noch leer ist) die Funktion collide() an, das sieht zum Beispiel so aus:
class Krebs(Actor):
def collide(self,actor1,actor2):
feld.removeActor(actor2)
return 0
Hier fallen mehrere Dinge auf, die erläutert werden müssen. Die Funktion, die ausgeführt wird, wenn zwei Objekte sich berühren, heißt immer collide() (das schreibt gamegrid so vor) – und wie schon erwähnt, bringt diese gleich zwei Parameter mit, nämlich zwei Objekte, die unter dem Namen actor1 und actor2 zur Verfügung gestellt werden. actor1 ist das erste Objekt, das kollidiert ist (in diesem Fall der Krebs), und actor2 ist das Objekt, mit dem der Krebs kollidiert ist (also die Luftblase).
Was tut die Funktion nun? Sie löscht die Luftblase aus dem Spielfeld. Dazu dient der Befehl feld.removeActor(actor2).
removeActor() ist also das Gegenteil von addActor() – anstatt eine Spielfigur hinzuzufügen, wird sie gelöscht. Und somit verschwindet die Luftblase, wenn sie den Krebs berührt, und ist nach dem nächsten Refresh auch nicht mehr zu sehen (Refresh geschieht ja mit doRun() automatisch alle 20 Millisekunden).
Als Letztes steht noch return 0 in der Funktion. Die Funktion collide muss nämlich immer einen ganzzahligen Wert mit return zurückgeben. Der wird zwar hier nicht benutzt – aber wir müssen ihn anlegen, sonst funktioniert es nicht, und das Programm würde abbrechen.
So – damit ist die Kollisionsfunktion geschrieben, und es kann einen Test geben. Probiere das Programm mal mit den Erweiterungen aus:
Abbildung 18.3 Unmittelbar vor dem Aufprall: Danach verschwindet die Luftblase.
Es funktioniert! Jetzt kannst du es ganz einfach erweitern. Eine Luftblase reicht natürlich nicht – sondern jetzt sollen es viele werden. Das ist gar kein Problem. Du erstellst einfach 100 Luftblasenobjekte. Die kann der Krebs dann einsammeln.
Wo sollen die 100 Blasen platziert werden? Sie müssen ja logischerweise alle an verschiedenen Stellen sein, sonst wären sie alle übereinander. Ich würde empfehlen, sie einfach mal zufällig zu platzieren, das heißt: x-Position zwischen 30 und 770 und y-Position zwischen –30 und –570. Dann kommen sie alle mit der Zeit von oben ins Bild herunter.
Probieren wir es mal! Wir müssen für die Zufallszahlen am Anfang wieder den Befehl randint von random importieren, und dann kann es losgehen.
Am Anfang hinzu:
from random import randint
Und statt nur eine Luftblase zu erstellen und hinzuzufügen, kommt eine Schleife mit 100 Blasen:
repeat 100:
blase = Luftblase("sprites/bubble1.png")
krebs.addCollisionActor(blase)
feld.addActor(blase,Location(randint(30,770),randint(-570,-30)))
Das war’s schon. Starte das Programm!
Abbildung 18.4 Cool, der Krebs kann sich durch eine ganze Flut von Luftblasen bewegen und sie auslöschen.
Schon mal sehr schön. Um es lebendiger und plastischer zu machen, könnten die Luftblasen jetzt auch noch mit verschiedenen Geschwindigkeiten herunterfallen.
Wie kann man das lösen?
Nun, die Klasse Luftblase muss etwas erweitert werden. Das Ganze haben wir sehr ähnlich schon einmal vorher mit den Fischen gemacht. Für die Blasen funktioniert es genauso. Wir brauchen wieder eine Eigenschaft speed (also »Geschwindigkeit«, nur das Wort ist mir zu lang), und die act()-Methode, bei der die Blase herunterkommt, muss diese Geschwindigkeit einbeziehen. Und um es wirklich lebendig zu machen, kann man die Geschwindigkeit auch jedes Mal zufällig ändern, sobald die Blase unten verschwunden ist und wieder nach oben gesetzt wird.
Also könnte die neue Klasse »Luftblase« so aussehen:
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,6)
Am Anfang der Klasse wird die Eigenschaft speed eingeführt und erst mal auf 3 gesetzt. Aber sie kann natürlich für jedes Objekt dann noch separat auf einen anderen Wert gesetzt werden.
In der act-Methode wird die ypos nicht mehr +3 gesetzt, sondern +speed. Je nach Wert kann sie also schneller oder langsamer herunterfallen. Wenn die ypos größer als 600 ist, also die Blase von ganz unten wieder nach ganz oben gesetzt wird, ändert sich auch ihr speed, und sie wird möglicherweise schneller oder langsamer – ganz zufällig (Werte zwischen 2 und 6 sehen recht gut aus, aber du kannst natürlich auch andere probieren).
Dementsprechend muss jetzt auch die Erstellung der Luftblasenobjekte angepasst werden, denn jede Luftblase soll zu Beginn einen unterschiedlichen zufälligen Wert für speed erhalten:
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)))
Probiere das Programm mit diesen Änderungen aus: Merkst du? Die Blasen wirken jetzt viel realistischer und organischer, und durch die unterschiedlichen Geschwindigkeiten entsteht sogar ein etwas plastischer Eindruck – als wären einige Blasen weiter vorne, andere weiter hinten. Mit dem Krebs kannst du die Blasen nach wie vor abfangen und verschwinden lassen.
Zusammenfassung
In vielen Spielen muss damit umgegangen werden, was passiert, wenn zwei Spielfiguren miteinander kollidieren (Geschoss mit Raumschiff, Spieler mit Feind, Krebs mit Blase usw. …). Dafür hat gamegrid ein ganz klares Verfahren. Als Erstes musst du einem Actor-Objekt mitteilen, welches Objekt sein Kollisionspartner ist. Das kann eines sein, oder du kannst ihm auch nacheinander viele Kollisionspartner hinzufügen. Du machst das mit dem Befehl objekt1.addCollisionActor(objekt2)
Sobald das Objekt fortan mit einem seiner Kollisionspartner zusammenstößt, wird es automatisch seine eigene Methode collide() aufrufen, in der steht, was bei einer Kollision passieren soll. Diese Methode musst du für das Objekt schreiben – und darin wird dann entweder das Spiel beendet, ein Punkt gezählt, eine Explosion ausgelöst oder was auch immer passieren soll.