Pixeljäger: Microcontroller-Spiel zum Nachbauen

Wir sind inzwischen im Frühjahr angekommen und vielleicht hast du dir für dieses Jahr vorgenommen, tiefer in das Thema Microcontroller einzusteigen oder deine Kenntnisse weiter auszubauen. Ich selbst nutze gerne ruhigere Phasen, um neue Ideen zu entwickeln, Inspiration aus anderen Projekten zu sammeln oder bestehende Ansätze weiterzudenken. Häufig notiere ich mir dabei Probleme oder Ideen, die ich dann nach und nach umsetze.

In diesem Beitrag möchte ich ein simples Spiel vorstellen, das ich in einem YouTube-Short gesehen habe und direkt nachbauen musste. Auf Deutsch nennt sich das Spiel Pixeljäger und basiert auf einem einfachen, aber genialen Prinzip, das ich gleich näher erläutern werde. Das Projekt habe ich an einem Abend umgesetzt und es bietet noch einiges an Optimierungspotenzial, auf das ich am Ende des Beitrags eingehen werde.

Wenn ich dein Interesse geweckt habe, dann schnapp dir Lötkolben und die nötigen Bauteile und los geht’s.

Hinweis zu diesem Beitrag

Ich nutze für meine MicroController-Projekte Visual Studio Code mit PlatformIO. Beides ist kostenlos und für alle gängigen Betriebssysteme verfügbar. Da Visual Studio Code diverse Programmiersprachen beherrscht und super erweitert werden kann, bietet sich diese Entwicklerumgebung durchaus an. Den Quellcode findest du unter: https://github.com/BerryBase-de/Pixeljaeger

Das Spielprinzip von Pixeljäger

Pixeljäger ist ein einfaches Spiel. Es gibt einen Spielerpunkt, der mit einer pro Runde neu definierten Geschwindigkeit auf einem LED-Ring wandert, und man muss versuchen, einen Schatzpunkt zu erreichen. Der Spieler kann durch eine Taste die Richtung – mit oder gegen den Uhrzeigersinn – des Spielerpunktes ändern. Dies ist wichtig, weil es eine Wand gibt, die der Spielerpunkt nicht treffen darf, da es sonst Game Over heißt.

Interessant ist das Spiel deswegen, da sich pro Runde der Schatzpunkt, die Wand und auch die Geschwindigkeit des Spielerpunkts verändern, weswegen man teilweise recht schnell die Laufrichtung des Spielerpunkts ändern muss. Da die Geschwindigkeit komplett zufällig, jedoch in einem definierten Bereich liegt, sind schnelle Reflexe ein Muss.

Die Hardware zu diesem Projekt

Die benötigte Hardware für dieses Projekt ist recht überschaubar. Tabelle 1 zeigt die wichtigsten Bauteile, wobei gerade beim WS2812-Ring und dem Taster auch Alternativen genutzt werden können.

PosHardwareLink
1Lötkolben + Lötzinnhttps://www.berrybase.de/4-teiliges-loetset-ls-220-bestehend-aus-30w-loetkolben-ablagestaender-loetzinn-loetfett
2Arduino Nanohttps://www.berrybase.de/arduino-nano
3Ein WS2812 – Ring (hier als Beispiel mit 12 LEDs)https://www.berrybase.de/neopixel-ring-mit-12-ws2812-5050-rgb-leds
4LCD – Display 16×2https://www.berrybase.de/alphanumerisches-lcd-16×2-blau-weiss
5I2c-Adapter für LCD-Displayhttps://www.berrybase.de/iic-i2c-interface-fuer-1602-2004-displays
6Ein einfacher Tasterhttps://www.berrybase.de/detail/019234a4ff0370609ca87ae618416c43
Jumper-Kabelhttps://www.berrybase.de/40pin-jumper-dupont-kabel-male-male-trennbar/laenge-0-10-m
8Breadboardhttps://www.berrybase.de/breadboard-mit-400-kontakten


Tabelle 1: Benötigte Bauteile für das Projekt

Sollte man gerade für den WS2812-Ring eine Alternative suchen, empfehle ich tatsächlich ähnliche Produkte mit entsprechenden WS2812-LEDs. Der Vorteil ist, dass im Quellcode dann nur die Anzahl der verfügbaren LEDs angepasst werden muss.

Zusätzlich braucht es noch einen Lötkolben, etwas Lötzinn und passende Kabelenden für den WS2812-Ring, da dieser nicht mit vorgelöteten Kontakten geliefert wird. Theoretisch kannst du auch drei Jumper-Kabel auf der einen Seite abschneiden, abisolieren und an den Ring anlöten.

Verdrahtung

Die Schaltung für dieses kleine Projekt ist recht simpel. Das LCD-Display muss mit dem I2C-Adapter an die I2C-Pins vom Arduino angeschlossen werden. Der Datenpin vom WS2812-Ring an den Pin D5 und der Pin vom Button an D3 des Arduinos. Die Spannungsversorgung von 5 V und Masse wird vom Arduino Nano genutzt. Da wir für ein besseres Signal ein Pull-down verwenden, muss der zweite Anschluss vom Taster ebenfalls mit Masse verbunden werden.

Die komplette Schaltung gibt es noch einmal in Abbildung 1.

Abbildung 1: Die Verdrahtung der Bauteile, mit Fritzing erstellt

Sollte das Bild etwas undeutlich sein oder nicht ganz klar, so hilft Tabelle 2 bei der Verdrahtung bestimmt weiter.

Pin-HardwarePin-Arduino
5V – WSB28125V
GND – WSB2812GND
D IN – WSB2812D5
5V – LCD5V
GND – LCDGND
SDA – LCDA4
SCL – LCDA5
Ausgang ButtonD3
Eingang ButtonGND

Tabelle 2: Tabellarische Verkabelung des Projekts

Sollte andere Hardware als von mir genutzt werden, so muss natürlich auch die Verdrahtung angepasst werden.

Der Programmcode

Wie schon weiter oben geschrieben, verwende ich für meine Projekte Visual Studio Code und PlatformIO. Damit spielt sich ein wesentlicher Hauptteil der Konfiguration in der platformio.ini ab, siehe Abbildung 2. Man kann an der Stelle auch sagen, dass ich teilweise die #defines vorgebe und auch weitere Konfigurationen, die z. B. in der Arduino IDE an anderer Stelle versteckt sind, direkt dort vornehme.

Abbildung 2: Typische Konfiguration eines kleinen Projekts

Interessant an der Stelle zu erwähnen: Ich habe, wie bei modernen Entwicklerumgebungen, die Möglichkeit, in verschiedenen Modi zu arbeiten. Neben dem zu nutzenden MicroController, der Belegung der Pins, der Entprellzeit des Tasters oder der minimalen und maximalen Geschwindigkeit der Spielfigur ist auch die Erweiterung für zusätzliche MicroController möglich. An der Stelle habe ich keine weiteren Environments eingetragen, kann dies aber bequem für jeden Arduino-Controller vornehmen.

Das meinte ich zuvor mit verschiedenen Modi, da ich damit die Möglichkeit habe, denselben Code für verschiedene MicroController zur Verfügung zu stellen. Benötigte Bibliotheken werden beim ersten Kompilieren direkt heruntergeladen, sofern eine Internetverbindung vorhanden ist. Andernfalls ist dieses Projekt nicht umsetzbar.

Das bringt mich zu dem eigentlichen Quellcode dieses Projekts. Im Wesentlichen besteht der Programmcode aus neun Funktionen, siehe Tabelle 3, die sowohl die Initialisierung beim Start als auch später das Spiel steuern und kontrollieren.

FunktionBeschreibung der Funktion
setupDie Standardfunktion von Arduino, um alles einzustellen und zu Initialisieren
loopHauptfunktion in Arduino, die zyklisch abgearbeitet wird. Hier wird später das Spiel gesteuert und kontrolliert
writeDebugMsgWenn in PlatformIO der Debug ausgewählt wurde, werden über diese Funktion Debug-Nachrichten am seriellen Monitor ausgegeben.
InitRingTestEine Funktion, die nur beim Start vom MicroController ausgeführt wird, um den LED-Ring zu testen
CheckHighscorePrüft den Highscore mit dem gespeicherten Highscore und überschreibt diesen im EEPROM
DebounceButtonEine Funktion, um den Taster im Spiel zu entprellen
InitFirstRunFunktion die alles beim Start vom MicroController vorbereitet
ChangePlayerDirectionÄndert die Drehrichtung der Spielfigur, wenn Taster gedrückt wurde
DrawGameOnLEDRingZeichnet das aktuelle Spielgeschehen auf dem LED-Ring

Tabelle 3: Beschreibung der Funktionen im Programmcode

Das Programm in der main.cpp ist „gerade einmal“ 300 Zeilen lang, was relativ wenig erscheint. Tatsächlich habe ich das Programm so einfach wie möglich, aber so komplex wie nötig gehalten, damit gerade Einsteiger schnell in den Code finden. Auch habe ich an diversen Stellen Kommentare eingefügt, damit verständlich ist, was einzelne Passagen im Code machen.

Der wesentliche Teil vom Spiel passiert ab Zeile 51 bis 172, da über die globale Variable mState genau definiert ist, in welchem Status sich das Spiel befindet. Tabelle 4 zeigt mit kurzer Erläuterung die verschiedenen Stadien.

mState – WertWas macht der Programmteil
1Löscht das Display und zeigt den Startscreen vom Spiel. Wechselt direkt in den mState = 2
2Wartet darauf, dass der Spieler den Taster drückt, um ein neues Spiel zu starten. Wechselt in mState = 3
3Wenn der Taster wieder losgelassen wird, wird die aktuelle Runde im Display angezeigt, der aktuelle Score, der zufangende Punkt, die Wand und am Anfang die Spielerposition gesetzt. Danach wird in den mState = 4 gewechselt
4Hier passiert die „Magie“. Die Spielfigur wird in definierter Zeit bewegt, es erfolgt die Abfrage, ob der Spieler die Wand oder das Ziel getroffen hat oder ob ggf. die Spielfigurrichtung, durch betätigen vom Taster, verändert wurde. Wechselt in den mState: 5 – Wenn Ziel getroffen 90 – Wenn Wand getroffen
5Der Spieler hat das Ziel erreicht und nun wird die Spielfigurgeschwindigkeit angepasst der aktuelle Score inkrementiert und in den mState 3 gewechselt
90Ende des Spiels, Highscore wird mit aktuellen Score verglichen und ggf. der neue Highscore in das EEPROM geschrieben

Tabelle 4: Erklärung der einzelnen Stadien im Spiel

Wichtig anzumerken ist: Sollte ein mState-Status auftreten, der nicht in Tabelle 4 vorkommt, wird automatisch in den mState 1 gewechselt, siehe Zeile 171 im Quellcode. Damit wird verhindert, dass durch einen bösen Zufall der Code in einem Status landet, den er eigentlich nie hätte erreichen können. Das ist eine gängige Praxis, um Fehler im Code zu vermeiden.

Eine Sache habe ich an der Stelle noch nicht angesprochen, aber aufmerksame Leserinnen und Leser werden schon in Abbildung 2 die Zeile 21 entdeckt haben. Hierbei gibt es einen Input auf den digitalen Pin 6 mit der Bezeichnung PIN_RESET, welcher nur Auswirkungen in der Funktion InitFirstRun im Quellcode hat. Hier war die Idee, dass irgendwann mal ein zu großer Highscore oder Restdaten im EEPROM gelöscht werden können.

Sprich: Beim Start vom MicroController muss der Pin 6 mit GND verbunden werden, und in der Initialisierungsphase wird das EEPROM-Register, in dem der Highscore gespeichert wird, entsprechend auf 0 gesetzt. Danach sollte diese Verbindung wieder abgebaut werden, damit auch weiterhin ein Highscore beim nächsten Neustart vom MicroController vorhanden ist.

Grundlegende Ideen für die Verbesserung

Der vorgestellte Programmcode ist nicht optimal, z. B. ist gerade der Spielablauf in der loop()-Funktion durch die vielen if- und if-else-Anweisungen „schlecht“ programmiert. An dieser Stelle bietet sich eine State Machine an, die mittels einer switch-case-Anweisung umgesetzt werden kann.

Die Frage, was eine State Machine ist, möchte ich an der Stelle nicht unbeantwortet lassen. Dabei handelt es sich um ein Konzept, bei dem eine Maschine – in diesem Fall unser MicroController – verschiedene Zustände durchlaufen kann. Du kannst dir das so vorstellen, dass es sich hier um einen Ablauf handelt, den der MicroController nachbildet. Ist ein Zustand vollständig abgearbeitet oder passiert etwas, wodurch ein anderer Zustand erreicht wird, so springt der MicroController in einen anderen Ablauf.

Bildlich gesprochen ist es wie beim Kochen oder Backen: Du bereitest deine Zutaten vor, mischst diese dann zusammen und brätst oder kochst sie anschließend. Dies sind auch drei Zustände, wobei du, wenn beim Kochen oder Backen etwas schiefgeht, zum Beispiel wieder von vorne anfängst.

Der Umbau in eine switch-case-Anweisung ist wesentlich effizienter, was sich auch in der Zykluszeit eines Programmdurchlaufs bemerkbar macht. Zugegeben: In diesem einfachen Programm ist das noch vertretbar, aber Verbesserungen sind immer möglich.

Eine zweite Optimierung wäre, dass z. B. der Spielerpunkt automatisch die Laufrichtung beim Erreichen des Schatzpunktes ändert, sofern in unmittelbarer Nähe die neue Wand positioniert wird. Hierzu müsste in dem mState 5 eine weitere Abfrage eingebaut werden.

Ein Gehäuse wäre an der Stelle auch noch sinnvoll, da mit dem Breadboard und den Kabeln schnell mal die Schaltung von kleineren Kindern auseinandergenommen werden kann. Mit einem 3D-Drucker und etwas Zeit sollte dies auch kein Hexenwerk sein. Zudem wäre die letzte größere Optimierung noch eine eigene Platine für das Projekt. Gerade in Zusammenhang mit dem Gehäuse eine sinnvolle Umsetzung, da das Breadboard in erster Linie zunächst für das Testen und Probieren gedacht ist.

Abschließende Worte

An der Stelle ist das vorgestellte Projekt sehr einsteigerfreundlich und mit etwas Fingerspitzengefühl beim Löten durchaus machbar. Gerade weil ich meine Projekte gerne mit PlatformIO programmiere, kann ich die den Artikel “Von Arduino zur PlatformIO IDE” empfehlen, der einen sehr schönen Einblick in PlatformIO bietet. Das soll keine Schleichwerbung sein, aber Visual Studio Code mit PlatformIO ist eine sehr mächtige Entwicklerumgebung, und das Redaktionsteam der c’t MAKE hat es super geschafft, die wesentlichen Grundfunktionen anschaulich zu illustrieren.

Ich habe an der Stelle nicht gelogen, dass ich dieses Spiel an einem Abend nachgebaut habe. Zwar sah das YouTube-Short-Video deutlich komplizierter aus, mit ein bisschen Vorbereitung und Nachdenken hat sich aber schnell herausgestellt, dass die Logik hinter dem Spiel ziemlich primitiv ist. Mittels eines einfachen Ablaufdiagramms auf einem A4-Blatt war der Quellcode recht flott geschrieben.

Solltest du unter Hinweise den Link zu dem Quellcode übersehen haben, so findest du ihn noch einmal nachfolgend: https://github.com/BerryBase-de/Pixeljaeger

Die mobile Version verlassen