image image


image

Tutorial: Yeah, I did it! Spieleprogrammierung in Py5

Ich benötigte eine Pause von Pygame Zero und meinen Experimenten mit dem kleinen roten Doppeldecker und den fliegenden Pizzen (Teil 1 und Teil 2). Und da das Endziel meiner Bemühungen sowieso die Implementierung eines kleinen Spiele-Frameworks nach Py5, der Python-3-Implementierung von Processing ist (von einer »Engine« zu reden, wäre geprahlt), habe ich mich gestern mal hingesetzt und als kleine Fingerübung mein in TigerJython implementiertes kleines, bonbonbuntes Aquarium nach Py5 portiert.

Dabei wollte ich natürlich das geplante Spiele-Framework im Blick behalten. Doch eine erste Erkenntnis war: Bei solch einem einfachen Programm ist eine Oberklasse Actor() oder Sprite(), die die Spiele-Sprites beerben, noch sinnlos. Sie würde nur zwei leere Methoden update() und display() besitzen, die Unterklassen – in diesem Fall Background() und Fish() überschreiben müßten. Also habe ich erst einmal darauf verzichtet. Aber spätestens dann, wenn es mehrere Sprite-Klassen für den Spieler und die Gegner geben wird, komme ich wieder darauf zurück. Denn spätestens wenn zusätzliche Methoden wie Kollisionserkennung implementiert werden müssen, ist eine Oberklasse Actor() – die dies einmal implementiert – sinnvoll.

Das Aquarium besteht, wie oben erwähnt, aus den Background() und Fish(). Background() besteht nur aus einem einzeiligen Konstrukutor und der ebenfalls einzeiligen Methode display(). Wenn ich nicht schon den endloss scrollenden Hintergrund aus Pizza-Plane im Hinterkopf gehabt hätte, könnte sie auch direkt im eigentlichen Programm implementiert werden, so simpel ist die Klasse:

class Background():
    
    def __init__(self, img):
        self.img = load_image("data/" + img + ".png")
    
    def display(self):
        image(self.img, 0, 0)

Interessanter ist da schon die Klasse Fish(). Sie muß mit sieben verschiedenen Fischbildern umgehen können, von denen jeder Fisch aus zwei Einzelbildern besteht. Und da Py5 auch nicht die komfortable Methode setHorzMirror() aus TigerJythons GameGrid kennt, muß jedes Bild auch noch einmal gespiegelt geladen werden. Das heißt, jeder Fisch besteht aus vier Bildern und das es in dem Spiel sieben verschiedene Fische gibt, müssen insgesamt 28 Bilder geladen werden. Das war schon eine gewisse Herausforderung, um das einigermaßen optimiert über die Bühne laufen zu lassen. Der Konstruktor dieser Klasse sieht daher wie folgt aus:

    def __init__(self, idx, x, y, dr, speed):
        self.imgr0 = load_image("data/fish" + str(idx) + "r_0.png")
        self.imgl0 = load_image("data/fish" + str(idx) + "l_0.png")
        self.imgr1 = load_image("data/fish" + str(idx) + "r_1.png")
        self.imgl1 = load_image("data/fish" + str(idx) + "l_1.png")
        self.x = x
        self.y = y
        self.dir = dr
        if self.dir == "rt":
            self.img = self.imgr0
        elif self.dir == "lt":
            self.img = self.imgl0
        self.speed = speed*randint(1, 3)
        self.switch = 5
        self.timer = self.switch

Ich habe das so gelöst, daß ich dem Konstruktor einen Index-Parameter idx (eine Zahl zwischen eins und sieben) mitgegeben habe. Und da die Dateinamen alle von der Form fish1l_0.png sind (wobei die 1 die laufende Nummer des Fisches ist, l oder r für die Bewegungsrichtung (links oder rechts) des Fisches steht). _0 und _1 sind die Einzelbilder in jeder Bewegungsrichtung.

(Ich hoffe, daß Py5 erkennt, wenn ein Bildchen schon einmal geladen wurde und es dann nicht noch einmal hochgeladen wird. Bei 25 Fischen wäre das eventuell noch zu vertreten, aber bei einer größeren Menge …)

Ob der Fisch sich zuerst nach rechts oder links bewegt, bestimmt der Zufallszahlengenerator, genauso wie den Standort und die Geschwindigkeit jedes einzelnen Fisches. Der Einfachheit halber beginnt jeder Fisch seine Bewegung mit Bild _0 (die daraus resuliterende Gleichförmigkeit der Bewegung fällt kaum auf – wen das dennoch stört, der kann ja bei der Initialisierung noch einmal den Zufallszahlengenerator bemühen).

Die Konstante fish.switch hat noch keine Funktion. Ich habe sie nur schon einmal bereitgestellt, um unterschiedlich lange Flossenschläge zu ermöglichen. So könnten zum Beispiel der blaue fish3 und der rote Fisch fish4 sich mit langsameren Flossenschlägen durch das Aquarium schlängeln. Die Implementierung dieser Variante seien der geneigten Leserin oder dem geneigten Leser als Übung überlassen.

    def update(self):
        self.x += self.speed
        if self.timer <= 0:
            self.timer = self.switch
            if self.img == self.imgr0:
                self.img = self.imgr1
            elif self.img == self.imgr1:
                self.img = self.imgr0
            elif self.img == self.imgl0:
                self.img = self.imgl1
            elif self.img == self.imgl1:
                self.img = self.imgl0
        if self.x > width + randint(40, 200):
            self.img = self.imgl0
            self.y = randint(20, 300)
            self.speed = randint(-3, -1)
        if self.x < randint(-200, -40):
            self.img = self.imgr0
            self.y = randint(20, 300)
            self.speed = randint(1, 3)
        self.timer -= 1

Die update()-Methode des Fisches ist nur deswegen so lang geraten, weil Py5 auch die nette GameGrid-Methode showNextSprite() nicht kennt. Bei nur zwei Bildern je Bewegung ist sie dennoch recht übersichtlich (wenn _1 dann _0, respektive umgekehrt), aber wenn es mehr Bilder je Bewegung gibt, dann muß man doch schon einigermaßen geschickt mit dem Modulo-Operator hantieren. In diesem Fall gehört eine Methode Actor.show_next_sprite() auf jeden Fall in die geplante Oberklasse Actor().

Das war es eigentlich. Hier wie gewohnt noch der komplette Quelltext, damit Ihr meine Implementierung nachvollziehen und -programmieren könnt:

from random import randint

N_FISHES = 25      # Anzahl der Fische
        
class Background():
    
    def __init__(self, img):
        self.img = load_image("data/" + img + ".png")
    
    def display(self):
        image(self.img, 0, 0)
        
class Fish():
    
    def __init__(self, idx, x, y, dr, speed):
        self.imgr0 = load_image("data/fish" + str(idx) + "r_0.png")
        self.imgl0 = load_image("data/fish" + str(idx) + "l_0.png")
        self.imgr1 = load_image("data/fish" + str(idx) + "r_1.png")
        self.imgl1 = load_image("data/fish" + str(idx) + "l_1.png")
        self.x = x
        self.y = y
        self.dir = dr
        if self.dir == "rt":
            self.img = self.imgr0
        elif self.dir == "lt":
            self.img = self.imgl0
        self.speed = speed*randint(1, 3)
        self.switch = 5
        self.timer = self.switch
    
    def update(self):
        self.x += self.speed
        if self.timer <= 0:
            self.timer = self.switch
            if self.img == self.imgr0:
                self.img = self.imgr1
            elif self.img == self.imgr1:
                self.img = self.imgr0
            elif self.img == self.imgl0:
                self.img = self.imgl1
            elif self.img == self.imgl1:
                self.img = self.imgl0
        if self.x > width + randint(40, 200):
            self.img = self.imgl0
            self.y = randint(20, 300)
            self.speed = randint(-3, -1)
        if self.x < randint(-200, -40):
            self.img = self.imgr0
            self.y = randint(20, 300)
            self.speed = randint(1, 3)
        self.timer -= 1
        
    def display(self):
        image(self.img, self.x, self.y)

fishes = []
    
def setup():
    global bg
    size(640, 416)
    window_title("🐠 Jörgs kleines, bonbonbuntes Aquarium 🐡")
    window_move(1300, 40)
    bg = Background("background")
    for _ in range(N_FISHES):
        direction = randint(0, 1)
        if direction == 0:
            dr = "rt"
            speed = 1
        else:
            dr = "lt"
            speed = -1
        x = randint(-100, width + 200)
        y = randint(20, 300)
        fish = Fish(randint(1, 7), x, y, dr, speed)
        fishes.append(fish)

def draw():
    background(49, 197, 244)
    bg.display()
    for fish in fishes:
        fish.update()
        fish.display()

Ich gebe zu, ich bin ein Spielkalb. Daher habe ich noch zwei Fisch-Emojis in die Titelzeile geschummelt. Warum? Weil es geht!

Die Fischbildchen und der Hintergrund stammen aus dem freien (CC0 1.0) FishPack von Kenney.nl und den Quellcode und alle Assets gibt es in meinem Py5-GitHub-Repositorium (das ist zur Zeit noch recht leer, aber ich habe – wie schon mehrfach angekündigt – noch einiges mit Py5 vor). Still digging!


(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 ehemaliger 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