Objekte und Klassen mit Kitty

Hallo Hörnchen – Hallo Kitty revisited

Ich hatte vor einiger Zeit mal ein PyGame-Tutorial geschrieben. Doch da PyGame aus irgendwelchen Gründen nicht mit Python 3 spielt und auch sonst ziemlich buggy geworden ist, habe ich beschlossen, mein vierteiliges PyGame-Tutorial in Processing.py zu implementieren. Erfreulich war, daß ich mir den ersten Teil gleich schenken konnte, denn

def setup():
    size(640, 480)

def draw():
    background(0, 80, 125)

erzeugt bereits ein leeres, blaues Fenster. Also habe ich gleich den zweiten Teil in Angriff genommen und das »Horn Girl« aus dem von Daniel Cook (Danc) in seinem Blog Lost Garden unter einer freien Lizenz (CC BY 3.0 US) zu Verfügung gestellten Tileset Planet Cute in das Fenster gezaubert:

Hallo Hörnchen!

Zur Vorbereitung habe ich erst einmal das Bild der jungen Dame auf das Editorfenster der Processing-IDE geschoben. Falls noch nicht vorhanden, erzeugt Processing dann automatisch ein data-Verzeichnis und legt das Bild (aber auch Schriften oder andere Dateien) darin ab. Processing und damit auch Processing.py finden alles in diesem Verzeichnis ohne daß eine genauere Pfadangabe nötig ist. Und so ist auch das fertige Programm von erfrischender Kürze:

font = None
greetings = u'Hallo Hörnchen!'

def setup():
    global img
    size(640, 480)
    img = loadImage("horngirl.png")
    font = createFont("Verdana-Bold", 64)
    textFont(font)

def draw():
    background(0, 80, 125)
    image(img, 275, 100)
    text(greetings, 25, 350)

Mehr ist nicht nötig, um obigen Screenshot zu bekommen. Vergleiche ich diese 14 Zeilen mit den 34 Zeilen der PyGame-Version, dann frage ich mich schon, warum ich nicht früher zu Processing.py gewechselt bin1.

An der zweiten Zeile kann man es erkennen: Processing.py basiert auf Jython und ist damit kompatibel zu Python 2.7.5, aber nicht zu Python 3. Daher muß man Unicode-Strings (zum Beispiel mit deutschen Umlauten) explizit mit dem vorangestellten u markieren, sonst bekommt man seltsame Zeichen im Programmfenster angezeigt.

Processing(.py) kann mit Fonts im TrueType- (.ttf), OpenType- (.otf) und in einem eigenen Bitmap-Format, genannt VLW, umgehen. Natürlich findet es alle auf dem eigenen Rechner installierte Fonts, mit

fontList = PFont.list()
print(fontList)

kann man sich diese in der Konsole anzeigen lassen. Wenn man den Sketch allerdings weitergeben will, ist es sinnvoll, einen Font mitzugeben2, da man nicht sicher sein kann, ob der gewählte Systemfont auf dem anderen Rechner vorhanden ist. Dafür schiebt man eine entsprechende Font-Datei einfach ebenfalls auf das Editorfenster der IDE, damit sie dem data-Ordner hinzugefügt wird. Ich habe testweise mal die Datei OpenSans-Semibold-webfont.ttf installiert, die entsprechende Zeile im Programm hieße dann:

font = createFont("OpenSans-Semibold-webfont.ttf", 64)

Der zweite Parameter der Funktion createFont() benennt die gewünschte Größe des Fonts. Mehr ist zu diesem ersten Sketch in Processing.py eigentlich nciht zu sagen. Im nächsten Schritt wird es darum gehen, die junge Dame über das Fenster zu bewegen. Nach meinen Erfahrungen mit PyGame werde ich sie nicht nur mit der Maus, sondern auch mit der Tastatur steuern. Still digging!

Moving Kitty

Im zweiten Teil möchte ich die im letzten Abschnitt auf den Monitor gezauberte Kitty mithilfe der Pfeiltasten der Tastatur sich über den Monitor bewegen lassen.

Moving Kitty

In Processing gehören die Pfeiltasten wie einige andere auch zu den coded keys, weil sie sich nicht einem Buchstaben zuordnen lassen und haben daher einen speziellen Namen. Die Pfeiltastten heißen LEFT, RIGHT, UP und DOWN, andere coded keys sind zum Beispiel ALT, CONTROL oder SHIFT. Diese müssen in Processing wie in Processing.py gesondert abgefragt werden, und zwar so:

    if keyPressed and key == CODED:
        if keyCode == LEFT:

während die »normalen« Tasten so abgefragt werden können:

    if keyPressed:
        if key == 'b' or key == 'B':

Das ist eigentlich alles, was man wissen muß, um das Progrämmchen zu verstehen. Wenn Kitty den linken Rand des Fensters erreicht hat, taucht sie am rechten Rand wieder auf und umgekehrt. Genauso habe ich mit oben unten verfahren. Die Variabeln radius_x und radius_y sorgen dafür, daß Kitty vollständig vom Bildschirm verschwunden ist, bevor sie am anderen Ende wieder auftaucht (ich mag keine halben Kittys 😜 ) und mit STEP bestimmt Ihr die Geschwindigkeit, mit der Kitty über den Bildschirm wuselt. Hier der vollständige Quellcode zum nachprogrammieren:

pos_x = 275
pos_y = 100
radius_x = 50  # Bildbreite/2
radius_y = 85  # Bildhöhe/2
STEP = 5       # Geschwindigkeit

def setup():
    global horngirl
    size(640, 480)
    horngirl = loadImage("horngirl.png")

def draw():
    global pos_x, pos_y
    background(0, 80, 125)
    image(horngirl, pos_x, pos_y)
    if keyPressed and key == CODED:
        if keyCode == LEFT:
            pos_x -= STEP
        elif keyCode == RIGHT:
            pos_x += STEP
        elif keyCode == UP:
            pos_y -= STEP
        elif keyCode == DOWN:
            pos_y += STEP
        if pos_x > width + radius_x:
            pos_x = -radius_x
        elif pos_x < -2*radius_x:
            pos_x = width + radius_x
        if pos_y < -2*radius_y:
            pos_y = height
        elif pos_y > height:
            pos_y = -radius_y

Kitty alias »Horn Girl« stammt wieder aus dem von Daniel Cook (Danc) in seinem Blog Lost Garden unter einer freien Lizenz (CC BY 3.0 US) zu Verfügung gestellten Tileset Planet Cute. Aber Ihr könnt natürlich auch jedes andere Bild nehmen, das gerade auf Eurer Festplatte herumliegt.

Klasse Kitty!

Nach den ersten beiden Abschnitten möchte ich erst einmal ein wenig aufräumen und daran erinnern, daß Akteure eines Computerspiels programmiertechnisch am besten in Klassen aufgehoben sind. Daher habe ich auch Kitty eine eigene Klasse spendiert:

# coding=utf-8

class Kitty(object):
    def __init__(self, tempX, tempY):
        self.x = tempX
        self.y = tempY
        self.radiusX = 50  # Bildbreite/2
        self.radiusY = 85  # Bildhöhe/2

    def loadPic(self):
        self.img = loadImage("horngirl.png")

    def move(self):
        self.x = mouseX - self.radiusX
        self.y = mouseY - self.radiusY

    def display(self):
        image(self.img, self.x, self.y)

Klassen kann man in Processing der Übersicht halber in separaten Dateien unterbringen, die in der IDE jeweils einen eigenen Reiter bekommen (siehe Screenshot).

Klasse Kitty

Hierbei ist jedoch zu beachten, daß im Gegensatz zu Processing und P5.js (jeweils aus anderen Gründen) die Klasse nicht automatisch dem Quelltext der Applikation bei der Ausführung hinzugefügt wird. Sie ist wenn sie nicht im Quelltext der Applikation steht – wie in Python üblich – ein Modul und muß gesondert mit

from kitty import Kitty

importiert werden. Und da sie ein reines Python2- (oder genauer Jython-) Modul ist, sollte man auch nicht vergessen # coding=utf-8 in die erste Zeile der Datei schreiben, denn sonst bekommt man Probleme mit dem ö im Kommentar (Bildhöhe).

Den Konventionen folgend, habe ich dem Objekt Kitty neben der eigentlichen Initialisierung drei Funktionen spendiert, nämlich loadPic(), move() und display(). Die beiden letzeren hätte man auch in einer Funktion zusammenfassen können (beispielsweise update() wie bei PyGame üblich), aber da die Philosophie sein sollte, jeder Aktivität eine eigene Funktion zu spendieren, bin ich der Konvention gefolgt3.

Ansonsten ist zu dem Programm nichts weiter zu sagen. Es zeigt einfach eine Kitty die der Maus hinterherrennt. Und dadruch, daß fast die gesamte Logik in die Klasse Kitty ausgelagert wurde, ist das Hauptprogramm von erfrischender Kürze:

from kitty import Kitty

kitty = Kitty(275, 100)

def setup():
    size(640, 480)
    kitty.loadPic()

def draw():
    background(0, 80, 125)
    kitty.move()
    kitty.display()

So muß es ja auch sein.

Horm Girl

»Cute Planet«

Im letzten Abschnitt hatte ich ja eine Klasse erstellt. Auf den ersten Blick erschien sie nicht besonders nützlich, da ich im eigentlichen Sketch ja nur eine Instanz der Klasse erzeugt hatte. So sah das schon ein wenig nach mehr Schreibaufwand ohne großen Nutzen aus. Um die Skeptiker zu überzeugen, werde ich in diesem Tutorial wieder eine Klasse erstellen, von der es im Sketch dann aber vier Instanzen geben wird. Vier Raumschiffe werden im Anschluß über den Bildschirm wuseln.

Cute Planet

Also erst einmal die Klasse selber, ich habe sie aus naheliegenden Gründen Spaceship genannt (auch wenn ein Planet ja im eigentlichen Sinne des Wortes kein Raumschiff ist, aber wie Ihr später sehen werdet, in »Space Cute« schon 😜 ):

class Spaceship():

    def __init__(self, pic, posX, posY):
        self.pic = pic
        self.x = posX
        self.y = posY
        self.dx = 0

    def loadPic(self):
        self.img = loadImage(self.pic)

    def move(self):
        self.x += self.dx
        if self.x >= width + 120:
            self.x = -120
            self.y = random(height-120)
        elif self.x < -120:
            self.x = width + 120
            self.y = random(height-120)

    def display(self):
        image(self.img, self.x, self.y)

Der Konstruktor der Klasse verlangt die URL eines Bildes, das das Raumschiff (oder den Planeten) auf dem Monitor darstellt und eine initiale Position, auf der es im Fenster erscheinen soll.

Dann gibt es die Funktion loadPic(), die dieses Bild dann lädt. Die Bilder stammen wieder aus dem von Daniel Cook (Danc) in seinem Blog Lost Garden unter einer freien Lizenz (CC BY 3.0 US) zu Verfügung gestellten Tileset Planet Cute. Ich habe sie mit dem Bildverarbeitungsprogramm meiner Wahl zurechtgeschnitten und auf eine Größe von 120x120 Pixeln heruntergerechnet und sie dann wie immer durch einfaches Schieben auf das Editor-Fenster der Processing IDE in den data-Ordner des Sketches transportiert. So findet Processing (und damit auch Processing.py) sie ohne zusätzliche Pfadangabe.

Planet Rocketship Octopussy Beetleship

Dann folgt die Funktion move(), die das Herzstück der Klasse darstellt. Hier werden die einzelnen Raumschiffe bewegt und wenn sie die Grenzen des Fenster verlassen haben, von der gegenüberliegenden Seite von einer zufällig gewählten Position wieder zurück ins Fenster geschickt. Die Funktion display() kümmert sich dann um die Darstellung des Raumschiffs.

Nun das Hauptprogramm: Dank der Klasse Spaceship ist es kurz und übersichtlich geblieben.

from spaceship import Spaceship

planet = Spaceship("planet.png", 500, 350)
rocket = Spaceship("rocketship.png", 300, 300)
octopussy = Spaceship("octopussy.png", 400, 150)
beetle = Spaceship("beetleship.png", 200, 100)

ships = [planet, rocket, octopussy, beetle]

def setup():
    size(640, 480)
    planet.loadPic()
    planet.dx = 1
    rocket.loadPic()
    rocket.dx = 10
    octopussy.loadPic()
    octopussy.dx = -5
    beetle.loadPic()
    beetle.dx = 5

def draw():
    background(0, 80, 125)
    for i in range(len(ships)):
        ships[i].move()
        ships[i].display()

Als erstes wird die Klasse Spaceship importiert und der Variablen spaceship zugewiesen. Dann werden vier Spaceships« erzeugt und einer Variablen zugewiesen, die den Konstruktor der Klasse aufruft. Dann wird noch eine Liste erstellt, die alle vier »Raumschiffe« enthält. Im setup() laden dann alle vier ihre Bilder und bekommen (mit dx) eine Geschwindigkeit verpaßt.

Das war es dann scho fast: In draw() wird dann nur noch eine Schleife durchlaufen, die für jedes der »Raumschiffe« die Funktionen move() und display() aufruft. Wenn Ihr nun den Sketch laufen laßt, werdet Ihr sehen, daß im Weltall rund um den Planeten »Space Cute« ein Verkehr wie auf dem Kudamm herrscht. Stellt Euch mal vor, ich hätte noch mehr Instanzen der Klasse Spaceship erzeugt.

Fluffy Fish - ein Flappy-Bird-Klon in Processing.py

In seiner 100. Coding-Challenge (mit Fortsetzung) hatte Daniel Shiffman gezeigt, wie man mit Hilfe eines Neuronalen Netzwerkes und genetischen Algorithmen seinem Rechner beibringen kann, erfolgreich Flappy Bird zu spielen. Grundlage war sein eigener Flappy-Bird-Klon, den er in P5.js, dem JavaScript-Mode von Processing, implementiert hatte. Natürlich reizt das zur Nachahmung in Python, doch bevor ich mich an neuroevolutionäre Algorithmen (NEAT) wage, möchte ich zur Übung in der Programmiersprache meines Vertrauens doch erst einmal selber einen Flappy-Bird-Klon bauen. Damit ich nicht zu weit von Shiffmans Vorgaben abweiche, programmierte ich das in Processing.py, dem Python-Mode von Processing.

Fluffy Fish

Um das Ganze aufzuhübschen, habe ich statt des Vogels einen Fisch gewählt, dessen Bild ich Twitters Twemojis, einer freien (MIT-Lizenz für den Code und CC-BY 4.0 für die Graphiken) Emoji-Implementierung entnommen habe. Ich habe den Fisch in der Bildverarbeitung meines Vertrauens gespiegelt und auf eine Größe von 32x32 Pixeln zurechtgestutzt. Natürlich heißt das Programm nun auch nicht Flappy Bird, sondern Fluffy Fish.

Zuerst habe ich im Hauptsketch die Grundlagen gelegt:

from fish import Fish

fluffyFish = Fish()

def setup():
    size(640, 320)
    fluffyFish.loadPic()

def draw():
    background(0, 153, 204)

    fluffyFish.update()
    fluffyFish.display()

def keyPressed():
    if (key == " "):
        # print("SPACE")
        fluffyFish.up()

Wie man leicht sieht, wird ein Modul fish.py benötigt. Also habe ich dieses Modul erst einmal angelegt,

class Fish():

    def __init__(self):
        self.x = 50
        self.y = 240
        self.r = 32

        self.gravity = 0.6
        self.lift = -12
        self.velocity = 0

    def loadPic(self):
        self.pic = loadImage("fisch2.png")

    def up(self):
        self.velocity += self.lift

    def display(self):
        image(self.pic, self.x, self.y)

    def update(self):
        self.velocity += self.gravity
        self.velocity *= 0.9
        self.y += self.velocity
        if (self.y >= height - self.r):
            self.y = height - self.r
            self.velocity = 0
        elif (self.y <= 0):
            self.y = 0
            self.velocity = 0

damit es importiert werden kann. Neben dem Konstruktor hat die Klasse Fish drei Methoden: Die Methode loadPic() lädt einfach nur das Bild des Fisches und die Methode display() zeigt den Fisch an der aktuellen x- und y-Position. In der Methode update() fällt der Fisch, falls der Spieler nicht eingreift, einfach nach unten. Die Geschwindigkeit wird durch eine Gravitationskonstante erhöht und durch eine leichte Reibungskonstante etwas gebremst. Bei den Werten dieser Konstanten habe ich mich an Shiffmans Beispiel orientiert, doch sind diese durchaus noch optimierungsfähig. Der experimentierfreudige Leser ist aufgefordert, hier selber mit anderen Werten zu spielen. Wenn der Spieler die Leertaste drückt, wird die Methode up() aufgerufen, die den Fisch nach oben hüpfen läßt.

Damit der Fisch dabei nicht oben oder unten über den Fensterrand hinausschießt, sorgen die sechs letzten Zeilen der update()- Methode dafür, daß der Bewegungsraum des Fisches oben und unten begrenzt ist.

Damit ist die eigentliche Spielmechanik implementiert. Der Fisch fällt, falls der Spieler nicht eingreift, nach unten, der Spieler kann ihn durch Betätigen der Leertaste nach oben katapultieren. Jetzt müssen dem Fisch nur noch die Säulen entgegenkommen, die er auf gar keinen Fall berühren darf. Dafür habe ich eine Datei pipes.py angelegt, die die Klasse Pipe beherbergt:

class Pipe():

    def __init__(self):
        self.top = random(height) - 60
        if self.top < 60:
            self.top = 60
        if self.top > height - 180:
            self.top = height - 180
        self.bottom = height - self.top - 120
        # self.bottom = random(height/2)
        self.x = width
        self.w = 40
        self.speed = 2
        self.hilite = False

    def display(self):
        fill(0, 125, 0)
        if self.hilite:
            fill(255, 0, 0)
        rect(self.x, 0, self.w, self.top)
        rect(self.x, height - self.bottom, self.w, self.bottom)

    def update(self):
        self.x -= self.speed

    def offscreen(self):
        if (self.x < -self.w):
            return True
        else:
            return False

    def collidesWith(self, otherObject):
        if ((otherObject.y < self.top)
            or (otherObject.y + otherObject.r > height - self.bottom)):
            if ((otherObject.x + otherObject.r > self.x) and
               (otherObject.x < self.x + self.w)):
                return True
        else:
            return False

Im Gegensatz zu Shiffman, der die Höhe beider Säulen komplett vom Zufall abhängig machte und dadurch riskierte, daß der Abstand zwischen zwei Säulen so klein wurde, daß sein Vogel dort nicht durchfliegen konnte, hatte ich mir ein paar andere Flappy-Bird-Implementierungen angeschaut und gesehen, daß dort der Abstand zwischen den beiden Säulen immer konstant war. Daher brauchte ich nur die obere Säule per Zufall zu generieren, die untere Säule wurde einfach mit 120 Pixel Abstand zum Ende der oberen Säule konstruiert. Damit auch beide Säulen immer zu sehen sind, habe ich mit den Werten 60 und 180 Mindesthöhen festgelegt. Diese Werte habe ich experimentell herausgefunden.

Die Methode display() zeigt einfach die beiden Säulen an der aktuellen Position, und zwar im Normalfall in Lindgrün, falls aber der Fisch mit einer der Säulen kollidiert (hilite = True) in Knallrot. Die Methode update() bewegt die Säulen mit der Geschwindigkeit speed auf den Fisch zu.

Die Methode offscreen() ist eine Hilfsfunktion, die benötigt wird, um im Hauptprogramm die Säulen, die das Fenster links verlassn haben, auch zu löschen. Und mit der Methode collidesWith() wird überprüft, ob Fisch und Säule zusammenstoßen. Diese Methode kann man sicher noch optimieren, aber für die Zwecke des Spiels reicht sie völlig aus.

Natürlich ist jetzt auch das Hauptprogramm ein wenig angeschwollen. Es sieht in der Endfassung nun so aus:

from fish import Fish
from pipes import Pipe

fluffyFish = Fish()
pipes = []

def setup():
    size(640, 320)
    fluffyFish.loadPic()
    pipe = Pipe()
    pipes.append(pipe)

def draw():
    background(0, 153, 204)

    for i in range(len(pipes) - 1, -1, -1):
        if pipes[i].offscreen():
            pipes.pop(i)
    # print(str(len(pipes)))
        if pipes[i].collidesWith(fluffyFish):
            pipes[i].hilite = True
        else:
            pipes[i].hilite = False
        pipes[i].update()
        pipes[i].display()

    fluffyFish.update()
    fluffyFish.display()

    if (frameCount % 100 == 0):
        pipe = Pipe()
        pipes.append(pipe)

def keyPressed():
    if (key == " "):
        # print("SPACE")
        fluffyFish.up()

Neben dem Fisch wird nun auch die Liste pipes[] initialisiert, die im setup() mit einer einzigen Säule gefüllt wird. Alle weiteren Säulen kommen in der draw()-Funktion jeweils dann hinzu, wenn der frameCount modulo 100 Null ergiebt, also bei jedem hundertsten Durchlauf.

Wichtig ist, daß, wenn man Elemente aus einer Liste entfernt, man diese Liste rückwärts durchlaufen läßt. Andernfalls werden nicht nur einzelne Elemente der Liste übersprungen, sondern man erhält auch den berüchtigten index out of range-Fehler. Daher wurde dies mit

    for i in range(len(pipes) - 1, -1, -1):
        if pipes[i].offscreen():
            pipes.pop(i)

implementiert. Der erste Parameter in der range()-Funktion ist der Startwert, der zweite Parameter der Endwert und der dritte Parameter die Schrittlänge. Hier gilt es nun aufzupassen, denn der Endwert ist exklusive (mathematisch gesprochen wird das halboffen Intervall [startwert, endwert[ aufgerufen). Würde man also als zweiten Parameter 0 eingeben, würde die Schleife nicht bei 0, sondern bei 1 ende, das erste Element der Liste würde also nie abgefragt.

Anschließend wird überprüft, ob der Fisch mit einer der beiden Säulen kollidiert. Hier wird momentan nur hilite auf True oder False gesetzt, aber das wäre der Ort, an dem Punkte vergeben werden können oder der Fisch stirbt und damit das Spiel beendet ist.

Nun funktioniert das Spiel wie gewünscht. Wie gesagt, an den Parametern kann und muß sicher noch geschraubt werden, aber es entspricht im Großen und Ganzen der Implementierung von Daniel Shiffman und steht nun für weitere Experimente bereit.


  1. Ich will ehrlich sein: Die »Geschwätzigkeit« von PyGame ist nicht nur dem hohen Alter der Bibliothek geschuldet, sie erlaubt eine große Vielseitigkeit und erspart dem Programmierer dann wieder bei komplizierten Dingen viel Schreibarbeit. So ist zum Beispiel die eingebaute Sprite-Klasse und die Kollisionsprüfung etwas, was ich in Processing.py von Hand programmieren muß. 

  2. Natürlich sollte man sicherstellen, daß man diese Fonts auch verwerten darf, aber im Netz findet man viele Fonts zur freien Verwendung. Gute Anlaufstellen dafür sind zum Beispiel Google Fonts, die (Open) Font Library oder The League of Moveable Type

  3. Ein bei Processing.py durchgehend zu beobachtender Konventionsbruch macht mich allerdings wuschig. Während die PEP8 für Variablennamen die Trennung durch Unterstriche empfiehlt (z.B. mouse_x) folgen die Programmierer der Beispielprogramme durchgehend der Java-Konvention des camelCase (mouseX). Ich habe mich erst einmal entschlossen, ebenfalls den camelCase zu nutzen, ob ich dabei aber bleiben werde, weiß ich noch nicht.