Retrogaming mit Python: Pyxel-Tutorial Stage 2

Pyxel
Python
Retrogaming
Spieleprogrammierung
Spyder
Autor:in

Jörg Kantel

Veröffentlichungsdatum

10. August 2024

Mein Ausflug vor drei Tagen zu der Retrokonsole TIC-80 hatte meinen Ehrgeiz angestachelt. Ich wollte herausfinden, ob ich die Integration von Maps (Karten) mit Pyxel und Python nicht ähnlich einfach wie mit TIC-80 und Lua hinbekomme. Und ich glaube, das ist mir gelungen.

Doch zuerst einmal den Quellcode, der nur geringfügig umfangreicher und etwas weniger kompakt als die Lua-Vorlage geraten ist:

import pyxel

# Ein paar nützliche Konstanten
TS = 8       # Tilesize
COLKEY = 0   # Color Key
WALL = 2     # Mauer Tile No.

def get_tile(tile_x, tile_y):
    # pyxel.tilemaps() gibt ein Tupel mit den x- und y-Koordinaten
    # aus der Tilemap des mit pget() identifizierten Tiles zurück
    return pyxel.tilemaps[0].pget(tile_x, tile_y)

class Player:
    
    def __init__(self, _x, _y):
        self.x = _x*TS
        self.y = _y*TS
        self.w = self.h = TS
        self.u = 0   
        self.v = 0
        self.imagebank = 0
        
    def move(self):
        if (pyxel.btnp(pyxel.KEY_LEFT) 
        and get_tile((self.x - TS)//TS, self.y//TS)[0] != WALL):            
            self.x -= TS
        elif (pyxel.btnp(pyxel.KEY_RIGHT)
        and get_tile((self.x + TS)//TS, self.y//TS)[0] != WALL):
            self.x += TS
        elif (pyxel.btnp(pyxel.KEY_UP)
        and get_tile(self.x//TS, (self.y - TS)//TS)[0] != WALL):
            self.y -= TS
        elif (pyxel.btnp(pyxel.KEY_DOWN)
        and get_tile(self.x//TS, (self.y + TS)//TS)[0] != WALL):
            self.y += TS

class App:
    
    def __init__ (self):
        pyxel.init(16*TS, 16*TS, "Pyxel Tutorial Stage 2", display_scale = 4)
        pyxel.load("assets/mazegame01.pyxres")
        
        # Initialisiere den Spieler
        # Position in Map-Koordinaten
        self.player = Player(1, 1)
        
    def run(self):
        pyxel.run(self.update, self.draw)
        
    def update(self):
        self.player.move()
        
    
    def draw(self):
        pyxel.cls(0)
        
        # Zeichen die Map
        pyxel.bltm(0, 0, 0, 0, 0, 16*TS, 16*TS, 0)
        
        # Zeichne den Player
        pyxel.blt(self.player.x, self.player.y, self.player.imagebank,
                  self.player.u, self.player.v, self.player.w, self.player.h,
                  COLKEY)
                
App().run()

Der Trick ist, daß ich die Klasse Player mit den Map-Koordinaten initialisiere und aufrufe und alle Umrechnungen zu den Sprite-Koordinaten nur in dieser Klasse vornehme. Die einzigen Ausnahmen sind die Fenster- und Kartengrößen, die – in diesem Fall – je mit (16*TS, 16*TS) aufgerufen werden. Doch das ist nur Kosmetik, die eine spätere Änderung der Kartengröße (siehe weiter unten) vereinfachen soll. Ich hätte genauso an dieser Stelle das Konstantenpaar (128, 128) einsetzen können.

Die Methoden pyxel.blit() für die Sprites und pyxel.blitm() für die Karten sind zwar nicht gerade parameterarm, aber gerade dadurch mächtig. Damit ich mich im Dickicht der Ziffern nicht verhaspele, habe ich den Aufruf von pyxel.blit() mit (hoffentlich) für sich sprechenden Parametermamen einsichtiger gemacht:

player.x           # Die x-Koordinate in Sprite-Koordinaten
player.y           # Die y-Koordinate in Sprite-Koordinaten
player.imagebank   # Die Bank des Sprites in der Ressourcendatei
player.u           # Die x-Position des Sprites in der Imagebank
player.v           # Die y-Position des Sprites in der Imagebank
player.w           # Die Breite des Sprites (in Pixeln)
player.h           # Die Höhe des Sprites (in Pixeln)
COLKEY             # Die Position der transparenten Farbe in der Farbpalette

Ähnlich sieht es bei der Methode pyxel.blitm() aus. Da ich hier bisher nur eine Karte verwende und daher fast alle Parameter bis auf die Höhe und Weite der Karte 0 sind, habe ich sie einfach mit den entsprechenden Zahlenwerten aufgerufen. In umfangreicheren Programmen empfiehlt sich natürlich, die Karten auch in einer Klasse (zum Beispiel Map) zu kapseln und ebenfalls sprechende Variablennamen zu verwenden.

Der Pyxel Map-Editor

Der Pyxel Map-Editor

Wie Ihr im obigen Screenshot des Map-Editors sehen könnt, habe ich ihn testweise mit einer weiteren, lindgrünen Seite vergrößert. Wenn Ihr nun die Zeile für die Fenstergröße mit

pyxel.init(32*TS, 16*TS, "Pyxel Tutorial Stage 2", display_scale = 4)

und die Zeile, mit der Ihr die Karte zeichnet, mit

pyxel.bltm(0, 0, 0, 0, 0, 32*TS, 16*TS, 0)

aufruft, bekommt Ihr ein doppelt so breites Fenster mit einer doppelt so breiten Karte angezeigt:

Die doppelt so breite Testkarte

Die doppelt so breite Testkarte

Damit läßt sich schon eine ähnliche Karte wie bei meinen TIC-80-Experimenten erzeugen.

Der zweite Trick, der dafür verantwortlich ist, daß das Pyxel-Skript ähnlich kompakt wie mein TIC-80-Versuch geraten ist, hängt mit der Hilfsfunktion get_tile(tyle_x, tile_y) zusammen. Zwar kann Pyxel keine Flags setzen, wenn man jedoch die Methode pyxel.tilemaps.pget(x, y) mit x und y als Pixelkoordinaten aufruft, bekommt man ein Tupel mit der x- und y-Koordinate der Tilemap zurück. Und da in meinem Fall die Mauer in der dritten Spalte (x = 2) der ersten (und einzigen) Zeile (y = 0) liegt, gibt der Aufruf von

pyxel.tilemaps[0].pget(tile_x, tile_y)

dann und nur dann eine 2 zurück, wenn das Tile eine Mauer ist. Das ist doch fast so gut, wie ein Flag. 🤓

Daher frage ich in in der Methode player.move() jedesmal ab, ob das gewünschte Zielfeld (in Map-Koordinaten) keine Mauer ist. Nur dann wird der Spieler auf das Zielfeld bewegt.

Wie Ihr selber sehen und ausprobieren könnt, kann der Spieler mit den Pfeiltasten bewegt werden, aber die Mauern sind für ihn unüberwindbare Hindernisse.

Auch diese Version meiner hoffentlich wachsenden Pyxel-Tutorials habe ich – wie das Bannerbild oben zeigt – wieder mit Spyder zusammengeschraubt. Je länger ich damit arbeite, um so mehr gefällt mir die Spynne und sie hat das Zeug dazu, Visual Studio Code als meine Python-IDE zu ersetzen. Sie ist wegen ihrer Konzentration auf Python nicht so überfrachtet wie die eierlegende Wollmilchsau VS Code und dadurch bedeutend übersichtlicher und schlanker. Und da sie sich ja auch mit Py5 wunderbar verträgt, sehe ich keine Hindernisse mehr für einen Wechsel (für Spezialfälle habe ich ja immer noch Thonny und JupyterLab (Desktop)).

Pyxel hat gegenüber TIC-80 und microStudio den Vorteil, daß ich auf das gesamte Python-Ökosystem zurückgreifen kann. Dadurch sollte diese Retrokonsole hervorragend dafür geeignet sein, mein lange eingeschlafenes Projekt »Retrogaming und Künstliche Intelligenz« (Nachschlag) wieder zum Leben zu erwecken. Ich werde dafür allerdings noch einige Tests und Tutorials durchführen müssen. Freut Euch also auf mehr. Still digging!