image image


image

Tutorial: Alien Attack mit TigerJython und GameGrid (Stage 1 + 2)

TigerJython und speziell die Spiele-Bibliothek GameGrid interessieren mich mehr und mehr. Um tiefer einzusteigen, habe ich mir aus dem vorgestern besprochenen Buch »Lets Code Python« das Spiel »Space Attack« (Seiten 239ff.) vorgenommen und eine eigene Implementierung gewagt. Denn der Code des Autors war mir an einigen Stellen nicht »pythonisch« genug (es war eher »javanisch«, zum Beispiel Klassendefinitionen ohne Konstruktor), so daß eine ziemlich eigenständige Version dabei herausgekommen ist. Um das zu unterstreichen, habe ich das Spiel dann auch »Alien Attack« genannt (es ist und bleibt aber eine »Space Invaders«-Kopie 👾 und hat dem Autor Hauke Fehr viel zu verdanken).

Um das Nachprogrammieren auch für Euch da draußen möglichst einfach zu gestalten, habe ich dieses Mal auf eigene Assets verzichtet, sondern ausschließlich Bilder verwendet, die mit der Sprites-Bibliothek der TigerJython-Distribution ausgeliefert werden.

Stage 1: Das Spielfeld und der Spieler

Ich beginne damit, zuerst einmal das Modul gamegrid zu importieren und das Spielefenster zu definieren:

from gamegrid import *

win = makeGameGrid(600, 600, 1, None, "sprites/town.jpg", True)
win.setTitle("Alien Attack")
win.setSimulationPeriod(20)

win.show()
win.doRun()

Das Bild town.jpg gehört zur TigerJython-Distribution und die IDE findet es unter dem angegebenen Verzeichnis. Mit setSimulationPeriod(20) wird der Bildschirm alle 20 Millisekunden neu gezeichnet, das heißt, das Spiel bekommt eine Framerate von etwa 50 Bildern/Sekunde.

Die letzten beiden Zeilen machen aus dem Code schon ein lauffähiges Skript, das heißt, Ihr könnt es starten und wenn Ihr keine (Tipp-) Fehler eingebaut hat, sollte das Fenster hochfahren und die Zeichnung einer wolkenverhangenen Stadt anzeigen.

image

Das letzte True im Aufruf von makeGameGrid() sorgt dafür, daß unten im Fenster eine Steuerungsleiste angezeigt wird. Sie kann manchmal zum Debuggen recht nützlich sein, darum lasse ich sei während der Entwicklung immer anzeigen. Mit False kann man sie im fertigen Spiel dann ausschalten.

Nun kommt der Spieler hinzu. Er steuert ein Raumschiff, das später die Aliens abschießen soll, daher bekommt das Programm erst einmal die Klasse SpaceShip verpaßt:

class SpaceShip(Actor):
    
    def __init__(self):
        Actor.__init__(self, "sprites/spaceship.gif")
        self.timer = 0
        
    def act(self):
        self.timer -= 1

Die Methode act() wird von GameGrid in jeder Simulationsperiode einmal aufgerufen, ohne daß der Programmierer das anstoßen muß. Sie zählt momentan in jedem Schritt den Timer um eins herunter. Momentan ist das noch nutzlos, aber später werde ich es brauchen.

Das Schiff soll sich, wenn der Spieler die Pfeiltasten rechts oder links drückt, nach rechts oder links bewegen. Damit GameGrid dies bemerkt, muß eine entsprechende Callback-Funktion definiert und registriert werden:

def keyCallback(keyCode):
    xpos = player.getX()
    if keyCode == 37: #left
        if xpos > 20:
            player.setX(xpos - 5)
    elif keyCode == 39: #right
        if xpos < 580:
            player.setX(xpos + 5)
    elif keyCode == 32: #space
        fire()

player = SpaceShip()
win.addActor(player, Location(300, 560))
win.addKeyRepeatListener(keyCallback)

Wenn diese Zeilen vor win.show() im Programm eingefügt werden, ist es wieder ein lauffähiges Skript. Am unteren Bildschirmrand erscheint nun eine kleine Rakete, die mit den Pfeiltasten hin- und herbewegt werden kann. keyCallback() sorgt auch dafür, daß die Rakete nicht das Fenster rechts oder links verläßt. Bitte noch nicht die Leertaste drücken, die Funktion fire() wird erst im nächsten Schritt implementiert.

Der Spieler ist nun eine Instanz von SpaceShip, die in der Zeile darunter dem Spielefenster bekannt gemacht wird, genau wie die nächste Zeile den keyCallback() registriert.

Als letztes wollte ich in diesem Stage dem Spieler noch die Möglichkeit geben, Geschosse abzufeuern. Dafür habe ich als erstes die Klasse Missile implementiert:

class Missile(Actor):
    
    def __init__(self):
        Actor.__init__(self, "sprites/bomb.gif")
    
    def act(self):
        ypos = self.getY()
        self.setY(ypos - 5)
        if ypos < 0:
            win.removeActor(self)

Solche Raketen nützen natürlich nichts, wenn man sie nicht abfeuern kann. Daher die Funktion fire(), die Ihr am besten oberhalb des keyCallbacks() im Skript einfügt:

def fire():
    if player.timer < 0:
        missile = Missile()
        win.addActor(missile, Location(player.getX(), 550))
        player.timer = 10

Jetzt seht Ihr auch, wofür der timer benötigt wird. Wird ein Geschoß abgefeuert, wird er auf zehn gesetzt und das nächste Geschoß kann erst wieder abgefeuert werden, wenn der timer < 0 ist. Dadurch wird verhindert, daß der Spieler eine Perlenkette von Geschossen ins All schleudern kann.

Stage 2: Aliens und Explosionen

Ein Space Shooter ohne Gegner, die man abschießen kann, ist zwar möglich, aber in den meisten Fällen sinnlos. Daher werde ich nun eine Klasse Alien einführen:

class Alien(Actor):
    
    def __init__(self):
        Actor.__init__(self, "sprites/alien.gif", 2)
        self.ypos = 0
        self.timer = 10
    
    def act(self):
        self.ypos += 0.1
        self.setY(int(self.ypos))
        self.timer -= 1
        if self.timer == 0:
            self.showNextSprite()
            self.timer = 10

In der TigerJython-Distribution gibt es die Bilder alien_0.gif und alien_1.gif. Eine Besonderheit der Actor-Klasse von GameGrid ist, daß, wenn dem Konstruktor als drittes Argument eine Ziffer mitgegeben wird, die entsprechende Anzahl von Sprites geladen wird. In diesem Falle also zwei Sprites, nämlich genau alien_0.gif und alien_1 gif. Der Unterstrich und die Ziffer werden von GameGrid automatisch ergänzt. Wenn Ihr eigene Sprites erstellt, beachtet bitte, daß auch GameGrid von Null zählt, zwei Sprites enden also mit sprite_0 und sprite_1. Bildformate für Sprites sind sinnvollerweise entweder PNG oder GIF, da sie im Regelfalle ja einen transparenten Hintergrund besitzen sollen. Aber TigerJython kann auch JPG laden, wie Ihr an dem Hintergrundbild seht.

Auch die Methode act() der Klasse Alien besitzt einen Timer. Denn alien_0.gif ist giftig grün und alien_1.gif ist gefährlich rot. Und showNextSprite() zeigt jeweils das nächste Sprite an, oszilliert also hier alle zehn Zeitschritte zwischen rot und grün.

Die Aliens sollen langsam nach unten gleiten, daher erhöht sich die y-Position bei jedem Zeitschritt nur um 0.1. Da der Bildschirm (und somit GameGrid) aber nur Pixel kann, muß mit int() die Position natürlich ganzzahlig gemacht werden.

Als nächstes wollte ich das Spielfeld mit 70 Aliens bevölkern, also fünf Reihen mit je 14 Aliens (die passen gerade so in eine Reihe). Das wird natürlich in einer Schleife erledigt, die ihr sinnvollerweise vor der Zeile player = SpaceShip() im Code einfügt:

for row in range(50, 300, 50):
    for col in range(40, 570, 40):
        alien = Alien()
        alien.ypos = row
        win.addActor(alien, Location(col, row))

Jetzt werdet Ihr Euch sicher fragen: Was, keine Liste? Doch, es gibt natürlich eine Liste, doch die füllt addActor() transparent im Hintergrund. Und jeder Actor in dieser Liste wird mit einer eigenen ID versehen, mit deren Hilfe GameGrid die einzelnen Elemente anspricht.1

Die Aliens bewegen sich langsam nach unten, aber der Player kann sie noch nicht abschießen, die Geschosse gehen einfach durch sie durch. Daher möchte ich als letztes eine Kollisionserkennung implementieren, die bei einer Kollision mit einem Geschoß die Aliens nicht nur einfach verschwinden, sondern sie in einer Explosion aufgehen läßt. Dafür wird erst einmal eine Klasse Explosion benötigt:

class Explosion(Actor):
    
    def __init__(self):
        Actor.__init__(self, "sprites/hit.gif")
        self.timer = 5
    
    def act(self):
        self.timer -= 1
        if self.timer == 0:
            win.removeActor(self)

Das Bild hit.gif ist das Bild einer Explosion, es soll bei einer Kollision mit einem Geschoß für fünf Zeitschritte am Bildschirm zu sehen sein, bevor es wieder verschwindet.

image

Jetzt stehen wir nur noch vor der Schwierigkeit, daß die Geschosse ja potentiell mit jedem der maximal 70 Alien kollidieren können. Daher implementieren wir die Kollisionserkennung sinnvollerweise in der Klasse Missile:

    def collide(self, actor1, actor2):
        xpos = actor2.getX()
        ypos = actor2.getY()
        win.removeActor(self)
        win.removeActor(actor2)
        hit = Explosion()
        win.addActor(hit, Location(xpos, ypos))
        return(0)

GameGrid weiß selber, wann eine Kollision vorliegt und führt dann die in der Methode collide() implementierten Befehle aus. In diesem Falle wird der actor2 (also das Alien) entfernt und dafür die Explosion angezeigt, die sich ja – wie oben programmiert – nach fünf Zeitschritten selber wieder entfernt.

Damit die Kollision auch erkannt wird, muß die Funktion fire() erweitert werden:

def fire():
    if player.timer < 0:
        missile = Missile()
        aliens = win.getActors(Alien)
        for alien in aliens:
            missile.addCollisionActor(alien)
        win.addActor(missile, Location(player.getX(), 550))
        player.timer = 10

Jetzt kommt aber endlich eine (sichtbare) Liste ins Spiel. Denn die Liste aliens wird mit win.getActors(Alien) mit allen noch aktiven Aliens gefüllt. In der Schleife werden die Kollisionen registriert. Danach wird das Geschoß wieder auf die Startposition zurückgesetzt und kann erneut abgefeuert werden.

Das vollständige Skript sieht zu diesem Zeitpunkt so aus (damit Ihr es nachprogrammieren und/oder nachvollziehen könnt):

from gamegrid import *

win = makeGameGrid(600, 600, 1, None, "sprites/town.jpg", True)
win.setTitle("Alien Attack Stage 2")
win.setSimulationPeriod(20)

class SpaceShip(Actor):
    
    def __init__(self):
        Actor.__init__(self, "sprites/spaceship.gif")
        self.timer = 0
        
    def act(self):
        self.timer -= 1
    

class Missile(Actor):
    
    def __init__(self):
        Actor.__init__(self, "sprites/bomb.gif")
    
    def act(self):
        ypos = self.getY()
        self.setY(ypos - 5)
        if ypos < 0:
            win.removeActor(self)
    
    def collide(self, actor1, actor2):
        xpos = actor2.getX()
        ypos = actor2.getY()
        win.removeActor(self)
        win.removeActor(actor2)
        hit = Explosion()
        win.addActor(hit, Location(xpos, ypos))
        return(0)
            
class Alien(Actor):
    
    def __init__(self):
        Actor.__init__(self, "sprites/alien.gif", 2)
        self.ypos = 0
        self.timer = 10
    
    def act(self):
        self.ypos += 0.1
        self.setY(int(self.ypos))
        self.timer -= 1
        if self.timer == 0:
            self.showNextSprite()
            self.timer = 10
            
class Explosion(Actor):
    
    def __init__(self):
        Actor.__init__(self, "sprites/hit.gif")
        self.timer = 5
    
    def act(self):
        self.timer -= 1
        if self.timer == 0:
            win.removeActor(self)

def fire():
    if player.timer < 0:
        missile = Missile()
        aliens = win.getActors(Alien)
        for alien in aliens:
            missile.addCollisionActor(alien)
        win.addActor(missile, Location(player.getX(), 550))
        player.timer = 10
    
def keyCallback(keyCode):
    xpos = player.getX()
    if keyCode == 37: #left
        if xpos > 20:
            player.setX(xpos - 5)
    elif keyCode == 39: #right
        if xpos < 580:
            player.setX(xpos + 5)
    elif keyCode == 32: #space
        fire()

for row in range(50, 300, 50):
    for col in range(40, 570, 40):
        alien = Alien()
        alien.ypos = row
        win.addActor(alien, Location(col, row))

player = SpaceShip()
win.addActor(player, Location(300, 560))
win.addKeyRepeatListener(keyCallback)

win.show()
win.doRun()

Natürlich ist das Spiel so noch nicht vollständig und auch schnell zu Ende gespielt. Denn wenn der Spieler die Aliens mit einem Dauerfeuer belegt, hat er sehr schnell alle abgeschossen. Daher möchte ich es in den nächsten Tagen erweitern und fertigstellen. Zum einen sollen die Aliens sich wehren können, in dem sie Bomben auf den Spieler werfen. Und dann möchte ich eine Gewinn- oder Verlust-Situation programmieren. Wird das Raumschiff des Spielers von einer Bombe getroffen oder erreicht auch nur ein Alien den unteren Rand des Fensters, hat der Spieler verloren. Wenn der Spieler aber alle Aliens vernichtet hat, bevor sie den unteren Rand des Fensters erreichen, hat er gewonnen. Um das zu programmieren, werde ich noch viel Spaß mit TigerJython und GameGrid haben. Still digging!

  1. Es hat mich einiges gekostet, bis ich das herausgefunden habe, bis dahin hatte ich aus reiner Gewohnheit selber noch eine Liste angelegt, die gar nicht benötigt wurde. 


(Kommentieren) 

image image



Über …

Der Schockwellenreiter ist seit dem 24. April 2000 das Weblog digitale Kritzelheft von Jörg Kantel (Neuköllner, EDV-Leiter Rentner, Autor, Netzaktivist und Hundesportler — Reihenfolge rein zufällig). Hier steht, was mir gefällt. Wem es nicht gefällt, der braucht ja nicht mitzulesen. Wer aber mitliest, ist herzlich willkommen und eingeladen, mitzudiskutieren!

Alle eigenen Inhalte des Schockwellenreiters stehen unter einer Creative-Commons-Lizenz, jedoch können fremde Inhalte (speziell Videos, Photos und sonstige Bilder) unter einer anderen Lizenz stehen.

Der Besuch dieser Webseite wird aktuell von der Piwik Webanalyse erfaßt. Hier können Sie der Erfassung widersprechen.

Diese Seite verwendet keine Cookies. Warum auch? Was allerdings die iframes von Amazon, YouTube und Co. machen, entzieht sich meiner Kenntnis.


Werbung

Diese Spalte wurde absichtlich leergelassen!


Werbung


image  image  image
image  image  image


image