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.
| Pos | Hardware | Link |
| 1 | Lötkolben + Lötzinn | https://www.berrybase.de/4-teiliges-loetset-ls-220-bestehend-aus-30w-loetkolben-ablagestaender-loetzinn-loetfett |
| 2 | Arduino Nano | https://www.berrybase.de/arduino-nano |
| 3 | Ein WS2812 – Ring (hier als Beispiel mit 12 LEDs) | https://www.berrybase.de/neopixel-ring-mit-12-ws2812-5050-rgb-leds |
| 4 | LCD – Display 16×2 | https://www.berrybase.de/alphanumerisches-lcd-16×2-blau-weiss |
| 5 | I2c-Adapter für LCD-Display | https://www.berrybase.de/iic-i2c-interface-fuer-1602-2004-displays |
| 6 | Ein einfacher Taster | https://www.berrybase.de/detail/019234a4ff0370609ca87ae618416c43 |
| Jumper-Kabel | https://www.berrybase.de/40pin-jumper-dupont-kabel-male-male-trennbar/laenge-0-10-m | |
| 8 | Breadboard | https://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.

Sollte das Bild etwas undeutlich sein oder nicht ganz klar, so hilft Tabelle 2 bei der Verdrahtung bestimmt weiter.
| Pin-Hardware | Pin-Arduino |
| 5V – WSB2812 | 5V |
| GND – WSB2812 | GND |
| D IN – WSB2812 | D5 |
| 5V – LCD | 5V |
| GND – LCD | GND |
| SDA – LCD | A4 |
| SCL – LCD | A5 |
| Ausgang Button | D3 |
| Eingang Button | GND |
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.
| Funktion | Beschreibung der Funktion |
| setup | Die Standardfunktion von Arduino, um alles einzustellen und zu Initialisieren |
| loop | Hauptfunktion in Arduino, die zyklisch abgearbeitet wird. Hier wird später das Spiel gesteuert und kontrolliert |
| writeDebugMsg | Wenn in PlatformIO der Debug ausgewählt wurde, werden über diese Funktion Debug-Nachrichten am seriellen Monitor ausgegeben. |
| InitRingTest | Eine Funktion, die nur beim Start vom MicroController ausgeführt wird, um den LED-Ring zu testen |
| CheckHighscore | Prüft den Highscore mit dem gespeicherten Highscore und überschreibt diesen im EEPROM |
| DebounceButton | Eine Funktion, um den Taster im Spiel zu entprellen |
| InitFirstRun | Funktion die alles beim Start vom MicroController vorbereitet |
| ChangePlayerDirection | Ändert die Drehrichtung der Spielfigur, wenn Taster gedrückt wurde |
| DrawGameOnLEDRing | Zeichnet 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 – Wert | Was macht der Programmteil |
| 1 | Löscht das Display und zeigt den Startscreen vom Spiel. Wechselt direkt in den mState = 2 |
| 2 | Wartet darauf, dass der Spieler den Taster drückt, um ein neues Spiel zu starten. Wechselt in mState = 3 |
| 3 | Wenn 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 |
| 4 | Hier 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 |
| 5 | Der Spieler hat das Ziel erreicht und nun wird die Spielfigurgeschwindigkeit angepasst der aktuelle Score inkrementiert und in den mState 3 gewechselt |
| 90 | Ende 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



































