Entwicklung eines eigenen Spiels
Durch die Entwicklung eines eigenen Spiels kann die Spielekonsole einfach erweitert werden. Dazu wird hier beispielhaft das klassische Spiel "Snake" entwickelt. Dazu wird eine neue Datei in Thonny angelegt, die mit "g_" beginnt und die Dateiendung ".py" hat (Hier "g_Snake.py").
Zunächst werden die benötigten Bibliotheken importiert. Auf jeden Fall benötigt wird die Bibliothek "PicoGameBoy", die die Ansteuerung von Display und Knöpfen für uns übernimmt. Um das Futter zufällig auf dem Spielfeld zu platzieren, benötigen wir "randint" aus der Bibliothek "random". Außerdem wollen wir durch eine Timer-Funktion die Snake in regelmäßigen Abständen bewegen, dazu benötigen wir "Timer" aus der Bibliothek "machine".
from PicoGameBoy import PicoGameBoy
from random import randint
from machine import Timer
Nun muss ein Objekt aus der PicoGameBoy Bibliothek initialisiert werden. Über dieses Objekt, das wir "pgb" nennen, steuern wir dann die Hardware der Konsole an.
pgb = PicoGameBoy()
Das verbaute Display hat eine Auflösung von 240x240 Pixeln. Für unser Spiel benötigen wir keine so große Auflösung, deswegen wird das Spielfeld in Quadrate von jeweils 10x10 Pixeln unterteilt. Diesen Faktor speichern wir in der Konstante "PIXEL_FACTOR". Mit dieser Konstante können wir aus den Pixelanzahlen berechnen, wie viele dieser Blöcke es in Höhe und Breite gibt. Auch das speichern wir zunächst in Variablen ab. So können wir im Weiteren immer mit den Koordinaten der einzelnen Blöcke arbeiten, ohne direkt die Pixel zu berücksichtigen.
PIXEL_FACTOR = 10
GAME_WIDTH = int(pgb.width / PIXEL_FACTOR)
GAME_HEIGHT = int(pgb.height / PIXEL_FACTOR)
Als nächstes können wir uns überlegen, welche Variablen wir brauchen, um den aktuellen Status des Spiels zu
speichern. Zunächst ist das die Position der Snake. Diese speichern wir als Liste von jeweils x- und y-Koordinaten
ab. Dabei ist der Kopf der Snake am Ende dieser Liste. Zu Beginn enthält die Liste nur einen Satz von Koordinaten,
nämlich die Mitte des Spielfelds.
Außerdem benötigen wir die Koordinaten, an denen das Futter liegt. Auch hier werden x- und y-Koordinaten gespeichert.
Wenn das Futter erreicht wurde, müssen diese Koordinaten zufällig neu generiert werden und die Snake wachsen. Dazu
wird in einer Boolean-Variable abgespeichert, ob dies der Fall ist.
Als letzte Variable wird gespeichert, in welche Richtung sich die Snake bewegt. Dies wird als Ganzzahl gespeichert.
Dabei ist 0 oben, die restlichen Richtungen sind im Uhrzeigersinn durchnummeriert.
snake = [[int(GAME_WIDTH / 2), int(GAME_HEIGHT / 2)]]
reward = [0, 0]
reward_captured = True
direction = 0
Nun können wir uns Gedanken um den Spielablauf machen: Das Bild soll mit einer Frequenz von 4 Hz aktualisiert werden. Dies steuern wir mit einem Timer, den wir zunächst definieren müssen. Bei jedem Ablauf des Timers wird eine Funktion aufgerufen, die eine Variable "game_step" auf "True" setzt. Auch diese Variable und Funktion müssen definiert werden.
timer = Timer()
game_step = False
def game_iteration(t):
global game_step
game_step = True
timer.init(freq=4, mode=Timer.PERIODIC, callback=game_iteration)
Der hauptsächliche Spielablauf passiert in einer while True:-Schleife, also einer Endlosschleife. Diese
wird so schnell wie möglich immer wieder wiederholt. Nur wenn die Variable "game_step" True ist, wird das Bild im
Schleifendurchlauf aktualisiert. Der Ablauf sieht also so aus:
while True:
if game_step:
# Bild aktualisieren
Zum Aktualisieren des Bildes müssen wir zunächst die Richtungs-Variable auswerten. Dazu zerlegen wir sie in x- und y- Komponente und setzen diese je nach Wert der Richtungs-Variable auf 0, 1 oder -1.
if direction == 0:
dir_x = 0
dir_y = -1
elif direction == 1:
dir_x = 0
dir_y = 1
elif direction == 2:
dir_x = 1
dir_y = 0
else:
dir_x = -1
dir_y = 0
Diese Komponenten können wir nun zu den Koordinaten des Schlangenkopfes dazurechnen, um die neuen Koordinaten des Schlangenkopfes zu ermitteln. Wird der Spielfeldrand überschritten, werden die Koordinaten auf den gegenüberliegenden Spielfeldrand gesetzt. Ein Koordinatenpaar mit diesen Koordinaten hängen wir dann an das Ende der Snake-Liste an.
snake_head_x = snake[-1][0] + dir_x
snake_head_y = snake[-1][1] + dir_y
if snake_head_x < 0:
snake_head_x = GAME_WIDTH - 1
elif snake_head_x >= GAME_WIDTH:
snake_head_x = 0
if snake_head_y < 0:
snake_head_y = GAME_HEIGHT - 1
elif snake_head_y >= GAME_HEIGHT:
snake_head_y = 0
snake.append([snake_head_x, snake_head_y])
Nun müssen wir die Snake noch auf dem Display anzeigen. Um den Code übersichtlicher zu machen, definieren wir uns dafür drei Farben am Anfang der Datei:
BLACK = PicoGameBoy.color(0, 0, 0)
WHITE = PicoGameBoy.color(255, 255, 255)
RED = PicoGameBoy.color(255, 0, 0)
Zuerst wird das Display komplett mit schwarz gefüllt, um das letzte Bild zu löschen. Dann wird die Snake durchgegangen und jeweils an der definierten Stelle ein Quadrat auf das Display gezeichnet. Zuletzt wird das generierte Bild angezeigt.
pgb.fill(BLACK)
for pixel in snake:
pgb.fill_rect(pixel[0]*PIXEL_FACTOR+1, pixel[1]*PIXEL_FACTOR+1, PIXEL_FACTOR-2, PIXEL_FACTOR-2, WHITE)
pgb.show()
Als letztes wird die Variable game_step wieder auf False gesetzt, damit die nächste Aktualisierung erst nach dem nächsten Auslösen des Timers ausgeführt wird.
Damit sieht unsere Datei nun so aus:
from PicoGameBoy import PicoGameBoy
from random import randint
from machine import Timer
BLACK = PicoGameBoy.color(0, 0, 0)
WHITE = PicoGameBoy.color(255, 255, 255)
RED = PicoGameBoy.color(255, 0, 0)
pgb = PicoGameBoy()
# Konstanten für die Unterteilung des Displays in größere Pixel
PIXEL_FACTOR = 10
GAME_WIDTH = int(pgb.width / PIXEL_FACTOR)
GAME_HEIGHT = int(pgb.height / PIXEL_FACTOR)
# Snake als Liste von Koordinaten, startet in der Mitte des Spielfelds
snake = [[int(GAME_WIDTH / 2), int(GAME_HEIGHT / 2)]]
# Position des Futters
reward = [0, 0]
reward_captured = True
# aktuelle Richtung: 0 ist oben, dann im Uhrzeigersinn durchnummeriert
direction = 0
# Zur Steuerung der regelmäßigen Iteration
timer = Timer()
game_step = False
def game_iteration(t):
# Funktion zur Aktivierung der nächsten Spieliteration
global game_step
game_step = True
timer.init(freq=4, mode=Timer.PERIODIC, callback=game_iteration)
# Hauptschleife
while True:
if game_step: # Iteration nur, wenn die entsprechende Zeit abgelaufen ist
# Richtung in x- und y-Komponenten zerlegen
if direction == 0:
dir_x = 0
dir_y = -1
elif direction == 1:
dir_x = 0
dir_y = 1
elif direction == 2:
dir_x = 1
dir_y = 0
else:
dir_x = -1
dir_y = 0
# neue Koordinaten für Schlangenkopf
snake_head_x = snake[-1][0] + dir_x
snake_head_y = snake[-1][1] + dir_y
if snake_head_x < 0:
snake_head_x = GAME_WIDTH - 1
elif snake_head_x >= GAME_WIDTH:
snake_head_x = 0
if snake_head_y < 0:
snake_head_y = GAME_HEIGHT - 1
elif snake_head_y >= GAME_HEIGHT:
snake_head_y = 0
# neuen Schlangenkopf ans Ende der Snake hängen
snake.append([snake_head_x, snake_head_y])
# Display aktualisieren
pgb.fill(BLACK) # clear
for pixel in snake: # Snake zeichnen
pgb.fill_rect(pixel[0] * PIXEL_FACTOR, pixel[1] * PIXEL_FACTOR, PIXEL_FACTOR, PIXEL_FACTOR, WHITE)
pgb.show() # Alles anzeigen
game_step = False
Diese Datei können wir nun schonmal auf der Spielekonsole testen:
Die grundlegenden Funktionen funktionieren schon wie gewollt: Die Snake bewegt sich in regelmäßigen Zeitabständen und wenn sie das Spielfeld am oberen Rand verlässt, taucht sie am unteren wieder auf. Sie wächst dabei allerdings noch kontinuierlich und wir können sie nicht steuern. Das wollen wir auf den nächsten Seiten ändern.