Die Aliens
Nun geht es also an die Gegner – die Aliens aus dem All, die angreifen. Sie sollen in mehreren Reihen vorhanden sein, sich langsam immer weiter nach unten bewegen und außerdem auch regelmäßig kleine Bomben abwerfen, um dem Raumschiff das Leben schwer zu machen. Wird das Raumschiff getroffen, oder erreicht ein Alien die Stadt, ist das Spiel verloren. Wenn alle Aliens weg sind, ist das Spiel gewonnen.
Wir erstellen also erst einmal eine Klasse abgeleitet von Actor, mit dem Namen Alien und geben ihr gleich die act()-Methode mit.
class Alien(Actor):
ypos = 0
def act(self):
self.ypos += 0.1
self.setY(int(self.ypos))
Was passiert da? Jeder Alien hat eine Eigenschaft ypos, die sich in der act-Methode jedes Mal um 0.1 vergrößert. Dann wird der Alien auf die gerundete y-Position ypos gesetzt (mit int). Auf die Weise bewegt er sich sehr langsam nach unten, jeden 10. Takt um ein Pixel.
[+] Pixel sind Ganzzahlen
Warum muss ypos mit der Funktion int() auf eine Ganzzahl gerundet werden? Ganz einfach: Weil es keine Zehntelpixel gibt. setY() erwartet eine Ganzzahl, sonst würde es eine Fehlermeldung geben. Daher wird ypos vor dem Setzen gerundet.
Wir probieren das Ganze jetzt gleich aus, indem wir einen Alien erzeugen und das Spiel starten: Der folgende Code sollte nach der Erzeugung des Raumschiffs kommen.
alien = Alien("sprites/alien.png")
alien.ypos = 30
feld.addActor(alien,Location(300,30))
Abbildung 20.5 Langsam, aber sicher kommt der Alien herunter.
Da das so schön funktioniert, werden wir jetzt gleich eine ganze Menge von ihnen erstellen – sagen wir 5 Reihen à 14 Aliens.
for reihe in range (50,300,50):
for spalte in range (40,570,40):
alien = Alien("sprites/alien.png")
alien.ypos = reihe
feld.addActor(alien,Location(spalte,reihe))
Siehst du, wie es funktioniert? Hier wird die range-Funktion mit Startwert, Endwert und Schrittweite verwendet, um eine Liste zu bilden, mit der dann die Aliens platziert werden. Die range-Funktion für die Reihe erzeugt also eine Liste [50, 100, 150, 200, 250], die für die Spalte erzeugt hier folgende Liste: [40, 80, 120, 160, 200, 240, 280, 320, 360, 400, 440, 480, 520, 560].
Daraus ergeben sich x- und y-Position der Aliens. Und so sieht das Ergebnis aus:
Abbildung 20.6 Na, wenn das keine Bedrohung ist!
Was fehlt noch, um das Spiel erst einmal grob funktionsfähig zu machen?
Ganz klar: Die Kollisionserkennung, damit man die Aliens auch abschießen kann. Sonst fliegen die Geschosse einfach durch sie hindurch.
Aber da gibt es ein kleines Problem. Bei Breakball hatten wir nur einen Ball, und wir konnten jedem der Blocks, die im oberen Teil des Spiels erzeugt wurden, einfach bei ihrer Erstellung den Ball als Kollisionsobjekt zuordnen. Hier ist es anders: Es gibt 70 Aliens, aber wir können diesen Aliens keinen Kollisionspartner zuordnen, während wir sie erstellen, denn die Geschosse, mit denen sie kollidieren sollen, existieren an dieser Stelle noch gar nicht. Sie werden jeweils erst in der Funktion schuss() erzeugt, wenn sie abgeschossen werden.
Stattdessen bleibt uns also nichts anderes, als jedem Geschoss, das beim Abfeuern erzeugt wird, bevor wir es aufs Spielfeld schicken, nacheinander alle zu der Zeit vorhandenen Aliens als Kollisionsobjekte zuzuweisen.
Wie macht man das?
Wir müssen bei jeder Erzeugung eines Geschoss-Objekts auf alle einzelnen Alien-Objekte des Spielfeldes zugreifen. Dafür gibt es zum Glück in gamegrid auch schon eine fertige Funktion. Die lautet getActors(Klasse) und liefert eine Liste aller passenden Objekte zurück.
Um eine Liste aller noch existierenden Alien-Objekte zu erhalten, verwenden wir folgenden Befehl:
alien_liste = feld.getActors(Alien)
Die Liste können wir jetzt mit der for-Schleife durchgehen und jedes Element dem Geschoss als Kollisionsobjekt eintragen. Das sieht dann so aus:
for a in alien_liste:
geschoss.addCollisionActor(a)
Und die gesamte Funktion schuss() sieht am Ende so aus:
def schuss():
if raumschiff.timer < 0:
geschoss = Geschoss("sprites/bomb.gif")
alien_liste = feld.getActors(Alien)
for a in alien_liste:
geschoss.addCollisionActor(a)
feld.addActor(geschoss,Location(raumschiff.getX(),590))
raumschiff.timer = 15
Obwohl jetzt viel mehr passiert, weil bis zu 70 Alien-Objekte nacheinander bei jedem abgefeuerten Schuss als Kollisionsobjekte dem Geschoss zugeordnet werden, wird das Programm nicht spürbar langsamer. Python ist dafür definitiv mehr als schnell genug.
Jetzt fehlt natürlich nur noch die collide()-Methode in den Geschossen, denn irgendetwas muss ja passieren, wenn das Geschoss mit einem Alien kollidiert.
Und was passiert? Es werden ganz einfach beide Kollisionsobjekte gelöscht, das Geschoss und der Alien. So sieht die Methode aus, die du der Klasse Geschoss hinzufügst:
def collide(self,actor1,actor2):
feld.removeActor(self)
feld.removeActor(actor2)
return 0
Und nun kannst du das Programm wieder testen!
Abbildung 20.7 Cool. Jetzt kann man die Aliens problemlos abschießen!
Es fühlt sich jetzt schon richtig gut an. Aber natürlich ist es noch viel zu einfach, denn es fehlen die eigentlichen Gegner. Die Aliens sind zwar da, aber sie werfen noch keine Bomben ab. Das wollen wir sofort ändern.
Wir erstellen also eine Klasse Bombe samt einer act()-Methode. Was tut die Bombe? Natürlich herunterfallen, bis sie aus dem Bild ist. Dann kann sie entfernt werden.
class Bombe(Actor):
def act(self):
ypos = self.getY()
self.setY(ypos+5)
if ypos>600:
feld.removeActor(self)
Und wo werden die Bomben erstellt?
Die Idee ist, dass alle Aliens Bomben werfen können, aber dies nur ab und zu tun. Machen wir es einfach: Jeder Alien ermittelt in jedem Takt eine Zufallszahl zwischen 1 und 1.000. Falls diese Zahl zufällig exakt 500 ist, dann wirft er eine Bombe.
[+] Ist das nicht zu selten?
Könnte man denken, aber man muss sich klarmachen, dass es 50 Takte pro Sekunde gibt und am Anfang 70 Aliens da sind – also 3.500 Ziehungen pro Sekunde. Da wird schon noch oft genug die richtige Nummer gezogen, durchschnittlich 3,5 Mal pro Sekunde. Allerdings: Je weniger Aliens es werden, desto seltener kommen die Bomben. Das liegt in der Natur der Sache.
So erweitern wir dann die act()-Methode der Aliens.
if randint(1,1000) == 500:
bombe = Bombe("sprites/creature_1.gif")
feld.addActor(bombe, Location(self.getX(),self.getY()+10))
Vergiss dabei nicht, ganz an den Anfang des Programms ein from random import * zu setzen.
Und schon ist alles da, um den munteren Bombenregen zu betrachten.
Abbildung 20.8 Obwohl jeder Alien nur etwa in jedem 1.000. Takt eine Bombe wirft, kommen da beim Spielen so einige zusammen.
Okay. Jetzt sind alle wichtigen Spielelemente beisammen und bewegen sich korrekt. Was fehlt, ist die Möglichkeit für den Spieler zu sterben.
Und das wäre ja höchst langweilig, wenn es nicht ginge.
Als Erstes sorgen wir dafür, dass die Bomben der Aliens bei Kollision mit dem Raumschiff das Spiel beenden. Dazu erhält jede Bombe bei Erstellung das Raumschiff als Kollisionsobjekt zugewiesen. Außerdem brauchen wir eine collide()-Methode in der Bombenklasse.
Die Klasse Alien ändert sich folgendermaßen:
class Alien(Actor):
ypos = 0
def act(self):
self.ypos += 0.1
self.setY(int(self.ypos))
if randint(1,1000) == 500:
bombe = Bombe("sprites/creature_1.gif")
bombe.addCollisionActor(raumschiff)
feld.addActor(bombe, Location(self.getX(),self.getY()+10))
# Und die Klasse Bombe erhält eine collide-Methode:
def collide(self,actor1,actor2):
gameover()
return 0
Bei Kollision wird also die Funktion gameover() aufgerufen. Die schreiben wir jetzt erst mal ganz simpel und verschönern sie später.
def gameover():
feld.doPause()
msgDlg("GAME OVER")
Und jetzt noch die zweite Möglichkeit, mit der man das Spiel verlieren kann: Wenn einer der Aliens die Stadt erreicht, ist das Spiel ebenfalls zu Ende. Wir fügen die folgende Abfrage noch in die act()-Methode der Alien-Klasse ein:
if self.ypos >520:
gameover()
So, damit wären alle negativen Abfragen gemacht.
Es wäre natürlich schön, wenn man das Spiel auch gewinnen könnte. Und das ist der Fall, wenn alle Aliens verschwunden sind.
Also wenn folgende Bedingung zutrifft:
feld.getNumberOfActors(Alien) == 0
Wo wird das abgefragt? Sinnvollerweise dort, wo ein Alien gelöscht wird, wenn er getroffen wird, also in der collide()-Methode der Klasse Geschoss.
Direkt vor der letzten Zeile (also vor return 0) tragen wir noch dies hier ein:
if feld.getNumberOfActors(Alien) == 0:
gewonnen()
# Jetzt fehlt nur noch die hier aufgerufene Funktion gewonnen(), und dann ist alles beisammen.
def gewonnen():
feld.refresh()
feld.doPause()
msgDlg("GEWONNEN!")
Fertig! Hier ist der gesamte Code für ein grundlegend funktionierendes Space-Attack-Spiel:
from gamegrid import *
from random import *
class Raumschiff(Actor):
timer = 0
def act(self):
self.timer -= 1
class Geschoss(Actor):
def act(self):
ypos = self.getY()
self.setY(ypos-5)
if ypos < 0:
feld.removeActor(self)
def collide(self,actor1,actor2):
feld.removeActor(self)
feld.removeActor(actor2)
if feld.getNumberOfActors(Alien) == 0:
gewonnen()
return 0
class Alien(Actor):
ypos = 0
def act(self):
self.ypos += 0.1
self.setY(int(self.ypos))
if randint(1,1000) == 500:
bombe = Bombe("sprites/creature_1.gif")
bombe.addCollisionActor(raumschiff)
feld.addActor(bombe, Location(self.getX(),self.getY()+10))
class Bombe(Actor):
def act(self):
ypos = self.getY()
self.setY(ypos+5)
if ypos>600:
feld.removeActor(self)
def collide(self,actor1,actor2):
gameover()
return 0
def tasteGedrueckt(tastencode):
xpos = raumschiff.getX()
if tastencode == 37:
if xpos > 20:
raumschiff.setX(xpos - 5)
elif tastencode == 39:
if xpos < 580:
raumschiff.setX(xpos + 5)
elif tastencode == 32:
schuss()
def schuss():
if raumschiff.timer < 0:
geschoss = Geschoss("sprites/bomb.gif")
alien_liste = feld.getActors(Alien)
for a in alien_liste:
geschoss.addCollisionActor(a)
feld.addActor(geschoss,Location(raumschiff.getX(),590))
raumschiff.timer = 10
def gameover():
feld.doPause()
msgDlg("GAME OVER")
def gewonnen():
feld.refresh()
feld.doPause()
msgDlg("GEWONNEN!")
feld = GameGrid(600,600,1,None,"sprites/town.jpg",False)
feld.setTitle("Space Attack")
raumschiff = Raumschiff("sprites/spaceship.gif")
feld.addActor(raumschiff,Location(300,586))
for reihe in range (50,300,50):
for spalte in range (40,570,40):
alien = Alien("sprites/alien.png")
alien.ypos = reihe
feld.addActor(alien,Location(spalte,reihe))
feld.setSimulationPeriod(20)
feld.addKeyRepeatListener(tasteGedrueckt)
feld.show()
feld.doRun()
Abbildung 20.9 Fertig! Alle Funktionen sind da. Space Attack ist komplett spielbar!