Der Spieleklassiker Snake begleitet uns schon lange und auf ganz unterschiedlichen Geräten – auf dem PC, auf Taschenrechnern von Texas Instruments oder auch auf dem ehrwürdigen Nokia 3210. Dank des einfachen Prinzips ist das Spiel fast überall dort umzusetzen, wo etwas Prozessorleistung vorhanden ist. Also warum nicht auch auf einem Arduino?
In diesem Projekt baust du dir zunächst eine Snake-Version auf einem Arduino UNO R4. Als Display verwendest du hierbei ein OLED-Display mit 128×64 Pixeln. Steuern wirst du das Spiel mit einem kleinen Joystick. Anschließend ziehst du das Spiel auf einen ESP32 um – falls du einen solchen besitzt, brauchst du dafür nur ein paar Zeilen zu ändern.
Diese Bauteile brauchst du
- 1× Arduino UNO R4 oder ESP32-Entwicklungsboard Für eine Variante genügt eines der beiden Boards; UNO R4 WiFi oder Minima sind geeignet.
- 1× OLED-Display 128 × 64 Pixel (I2C, SSD1306) I2C-Adresse im Sketch: 0x3C.
- 1× Analog-Joystick-Modul mit Taster Mit X-/Y-Achse und Schalteranschluss (z. B. VRx, VRy, SW).
- 1× Jumper-Kabel Zum Verbinden von Board, OLED und Joystick.
Affiliate-Links: Kaufst du über diese Links, erhält Pollux Labs eine kleine Provision – für dich ohne Mehrkosten.
Snake auf dem Arduino UNO
Zunächst also die Version auf dem Arduino UNO. Hier eignet sich der „neue“ UNO R4, da dieser deutlich mehr Leistung als sein Vorgänger R3 besitzt. Ob du die WiFi- oder die Minima-Version verwendest, ist für dieses Projekt egal. Falls du jedoch noch überlegst, dir einen R4 zuzulegen, empfehle ich dir auf jeden Fall erstere, da der R4 WiFi nur ein paar Euro mehr kostet und du damit mit deinen Projekten ins Internet kannst.
Nun aber zum Anschluss der Bauteile: Verbinde das OLED-Display und den Joystick folgendermaßen mit dem Arduino UNO:
Je nachdem, welchen Joystick du verwendest, kann die Beschriftung der Pins variieren. So kann zum Beispiel statt VERT auch VRy zu lesen sein. Mehr Infos hierüber findest du im Tutorial zum Arduino Joystick. Wenn du alles verkabelt hast, kann es direkt mit dem Sketch weitergehen.
Die benötigten Bibliotheken
Damit du am Arduino dein OLED-Display verwenden kannst, benötigst du zwei Bibliotheken. Öffne in der Arduino IDE den Bibliotheksverwalter, suche und installiere dort diese beiden Bibliotheken von Adafruit:
Adafruit SSD1306
Adafruit GFX
Im Sketch wirst du gleich noch eine dritte Bibliothek sehen, Wire.h – diese ist jedoch bereits vorinstalliert, sodass du dich um diese nicht kümmern musst.
Der gesamte Sketch
Hier nun der vollständige Sketch für Snake. Kopiere dir den folgenden Code, erstelle einen neuen Sketch und lade ihn auf deinen Arduino hoch.
// Snake spielen auf dem Arduino
// Pollux Labs
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Wire.h>
// OLED Display Konfiguration
#define SCREEN_WIDTH 128 // Breite des OLED-Displays
#define SCREEN_HEIGHT 64 // Höhe des OLED-Displays
#define OLED_RESET -1 // OLED-Reset-Pin (nicht verwendet)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Joystick Konfiguration
#define VRX_PIN A0 // Joystick X-Achsen-Pin (analoger Pin A0 des Arduino Uno)
#define VRY_PIN A1 // Joystick Y-Achsen-Pin (analoger Pin A1 des Arduino Uno)
#define SW_PIN 7 // Joystick Taster-Pin (digitaler Pin 7 des Arduino Uno)
#define SEED_PIN A2 // unbeschalteter analoger Pin, dient als Zufallsquelle
// Eigenschaften der Schlange
#define SNAKE_SIZE 4 // Größe jedes Segments der Schlange
#define MAX_SNAKE_LENGTH 100 // Maximale Länge der Schlange
int snakeX[MAX_SNAKE_LENGTH], snakeY[MAX_SNAKE_LENGTH]; // Arrays zur Speicherung der Segmentpositionen der Schlange
int snakeLength = 5; // Anfangslänge der Schlange
int directionX = 1, directionY = 0; // Anfangsrichtung der Bewegung (nach rechts)
// Eigenschaften des Futters
int foodX, foodY; // Koordinaten des Futters, werden von placeFood() gesetzt
// Score
int score = 0;
// Spielzustand
bool gameOver = false;
void setup() {
Serial.begin(9600); // Initialisiere serielle Kommunikation
// Initialisiere Display
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Initialisiere das OLED-Display mit der I2C-Adresse 0x3C
Serial.println(F("SSD1306 allocation failed"));
for (;;); // Stoppe, falls die Display-Initialisierung fehlschlägt
}
display.clearDisplay(); // Leere den Display-Puffer
display.display(); // Zeige den geleerten Puffer an
// Initialisiere Joystick
pinMode(VRX_PIN, INPUT); // Setze Joystick X-Achsen-Pin als Eingang
pinMode(VRY_PIN, INPUT); // Setze Joystick Y-Achsen-Pin als Eingang
pinMode(SW_PIN, INPUT_PULLUP); // Setze Joystick-Taster-Pin als Eingang mit internem Pull-up-Widerstand
// Starte den Zufallsgenerator mit dem Rauschen an einem offenen Pin -
// ohne diesen Startwert erscheint das Futter nach jedem Einschalten
// exakt an denselben Stellen
randomSeed(analogRead(SEED_PIN));
resetGame(); // Setze Schlange, Score und Futter auf den Startzustand
Serial.println("Setup abgeschlossen");
}
void loop() {
// Game Over: Ergebnis anzeigen und auf den Joystick-Taster warten
if (gameOver) {
showGameOver();
if (digitalRead(SW_PIN) == LOW) { // dank Pull-up bedeutet LOW: gedrückt
resetGame();
delay(200); // einfaches Entprellen des Tasters
}
return;
}
// Lese Joystick-Werte
int xValue = analogRead(VRX_PIN); // Lese X-Achsen-Wert vom Joystick
int yValue = analogRead(VRY_PIN); // Lese Y-Achsen-Wert vom Joystick
// Setze Richtung basierend auf Joystick-Eingaben, verhindere diagonale Bewegung
if (xValue < 300 && directionX == 0) { // Bewegung nach links, wenn derzeit nicht horizontal bewegt wird
directionX = -1;
directionY = 0;
Serial.println("Richtung: Links");
} else if (xValue > 700 && directionX == 0) { // Bewegung nach rechts, wenn derzeit nicht horizontal bewegt wird
directionX = 1;
directionY = 0;
Serial.println("Richtung: Rechts");
} else if (yValue < 300 && directionY == 0) { // Bewegung nach oben, wenn derzeit nicht vertikal bewegt wird
directionX = 0;
directionY = -1;
Serial.println("Richtung: Oben");
} else if (yValue > 700 && directionY == 0) { // Bewegung nach unten, wenn derzeit nicht vertikal bewegt wird
directionX = 0;
directionY = 1;
Serial.println("Richtung: Unten");
}
// Aktualisiere Position der Schlange
for (int i = snakeLength - 1; i > 0; i--) { // Bewege jedes Segment zur Position des vorherigen Segments
snakeX[i] = snakeX[i - 1];
snakeY[i] = snakeY[i - 1];
}
snakeX[0] += directionX * SNAKE_SIZE; // Aktualisiere Kopfposition in X-Richtung
snakeY[0] += directionY * SNAKE_SIZE; // Aktualisiere Kopfposition in Y-Richtung
// Prüfe auf Kollision mit dem Futter
if (snakeX[0] == foodX && snakeY[0] == foodY) { // Wenn der Schlangenkopf das Futter erreicht
if (snakeLength < MAX_SNAKE_LENGTH) {
// Das neue Segment übernimmt die Position des bisherigen Schwanzes.
// Ohne diese beiden Zeilen enthielte es noch einen alten Wert und würde
// für einen Frame an falscher Stelle aufblitzen.
snakeX[snakeLength] = snakeX[snakeLength - 1];
snakeY[snakeLength] = snakeY[snakeLength - 1];
snakeLength++; // Erhöhe die Schlangenlänge
}
score++; // Erhöhe den Score
Serial.println("Futter gegessen, Schlangenlänge: " + String(snakeLength));
placeFood(); // Generiere eine neue Futterposition
}
// Prüfe auf Kollision mit den Wänden
if (snakeX[0] < SNAKE_SIZE || snakeX[0] >= SCREEN_WIDTH - SNAKE_SIZE || snakeY[0] < 10 || snakeY[0] >= SCREEN_HEIGHT - SNAKE_SIZE) {
Serial.println("Kollision mit der Wand");
gameOver = true; // Spiel beenden, wenn die Schlange die Wand trifft
return;
}
// Prüfe auf Kollision mit sich selbst
for (int i = 1; i < snakeLength; i++) {
if (snakeX[0] == snakeX[i] && snakeY[0] == snakeY[i]) { // Wenn der Schlangenkopf mit ihrem eigenen Körper kollidiert
Serial.println("Kollision mit sich selbst");
gameOver = true; // Spiel beenden
return;
}
}
// Zeichne alles
display.clearDisplay(); // Leere den Display-Puffer
// Zeichne den Score
display.setTextSize(1); // Setze Textgröße
display.setTextColor(SSD1306_WHITE); // Setze Textfarbe
display.setCursor(0, 0); // Setze Cursor für Score
display.print("Score: ");
display.print(score); // Zeige aktuellen Score an
// Zeichne den Rand um das Spielfeld
display.drawRect(0, 10, SCREEN_WIDTH, SCREEN_HEIGHT - 10, SSD1306_WHITE); // Zeichne einen weißen Rahmen um das Spielfeld
// Zeichne die Schlange
for (int i = 0; i < snakeLength; i++) {
display.fillRect(snakeX[i], snakeY[i], SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE); // Zeichne jedes Segment der Schlange
}
// Zeichne das Futter
display.fillRect(foodX, foodY, SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE); // Zeichne das Futter
display.display(); // Zeige den aktualisierten Puffer an
delay(100); // Verzögerung zur Steuerung der Geschwindigkeit der Schlange
}
// Zeigt den Game-Over-Screen mit dem erreichten Score
void showGameOver() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(34, 16);
display.println("GAME OVER");
display.setCursor(34, 30);
display.print("Score: ");
display.print(score);
display.setCursor(4, 48);
display.print("Taster = neues Spiel");
display.display();
}
// Sucht eine freie Futterposition (nicht am Rand und nicht auf der Schlange)
void placeFood() {
bool aufSchlange;
do {
foodX = random(2, (SCREEN_WIDTH - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
foodY = random(12 / SNAKE_SIZE, (SCREEN_HEIGHT - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
aufSchlange = false;
for (int i = 0; i < snakeLength; i++) {
if (snakeX[i] == foodX && snakeY[i] == foodY) {
aufSchlange = true; // Treffer: neue Position würfeln
break;
}
}
} while (aufSchlange);
Serial.print("Neues Futter bei X: ");
Serial.print(foodX);
Serial.print(" Y: ");
Serial.println(foodY);
}
void resetGame() {
// Setze Eigenschaften der Schlange zurück
snakeLength = 5; // Setze die Schlangenlänge zurück
directionX = 1; // Setze die Richtung nach rechts zurück
directionY = 0;
score = 0; // Setze den Score zurück
gameOver = false; // Spiel läuft wieder
for (int i = 0; i < snakeLength; i++) {
snakeX[i] = SCREEN_WIDTH / 2 - (i * SNAKE_SIZE); // Setze die Position der Schlange zurück auf die Mitte
snakeY[i] = SCREEN_HEIGHT / 2;
}
placeFood(); // Generiere eine neue Futterposition
Serial.println("Spiel zurückgesetzt");
}
Nach dem Upload sollte es auf dem Display direkt losgehen: In der Mitte erscheint die zunächst 5 Segmente lange Schlange und bewegt sich. Ein Stück Futter erscheint zufällig platziert auf dem Spielfeld. Wie du sicherlich weißt, ist dein Ziel, so viel zu fressen wie möglich – ohne mit dem Spielfeldrand oder dir selbst zu kollidieren.
Oben links findest du deine Punktzahl. Für jedes gefressene Stück Futter erhöht sich dein Score um 1. Kollidierst du, erscheint ein Game-Over-Screen mit deinem Ergebnis – ein Druck auf den Joystick-Taster startet eine neue Runde.
So funktioniert der Sketch
Lass uns ein paar wichtige Stellen des Sketchs anschauen.
Setup
Die setup()-Funktion führt alle notwendigen Initialisierungen durch:
- Serielle Kommunikation wird gestartet, um Debugging-Informationen auszugeben.
- Das OLED-Display wird initialisiert. Falls die Initialisierung fehlschlägt, wird eine Fehlermeldung ausgegeben und das Programm angehalten.
void setup() {
Serial.begin(9600);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for (;;);
}
display.clearDisplay();
display.display();
...
}
- Der Joystick wird initialisiert, indem seine Pins als Eingänge konfiguriert werden.
- Der Zufallsgenerator bekommt einen Startwert.
- Zum Schluss übernimmt
resetGame()den Rest: Schlange in die Mitte, Score auf 0, erstes Futter platzieren.
Der Startwert für den Zufallsgenerator verdient einen kurzen Extra-Absatz, denn ohne ihn ist der Zufall gar keiner: random() liefert nach jedem Einschalten exakt dieselbe Zahlenfolge – dein Futter läge also in jeder Partie an denselben Stellen. Deshalb liest du einmalig einen unbeschalteten Analogpin aus. An dessen Eingang liegt nichts an außer elektrischem Rauschen, und genau das ergibt bei jedem Start einen anderen Wert:
randomSeed(analogRead(SEED_PIN)); // SEED_PIN ist A2
Wichtig: Nimm hierfür wirklich einen freien Pin. A4 und A5 scheiden am Arduino UNO aus – das sind die I2C-Pins, an denen bereits dein Display hängt.
Loop
Die loop()-Funktion läuft kontinuierlich und behandelt alle Aspekte des Spiels, wie die Steuerung, Bewegung, Kollisionserkennung und die Ausgabe auf dem Display.
Joystick-Steuerung
Der Joystick wird verwendet, um die Bewegungsrichtung der Schlange zu ändern. Die analogen Eingänge des Joysticks liefern Werte, die bestimmen, in welche Richtung sich die Schlange bewegt:
- Wenn der Joystick nach links bewegt wird, wird die Richtung auf links gesetzt, solange die Schlange sich nicht bereits nach rechts bewegt.
- Gleiches gilt für die anderen Richtungen.
if (xValue < 300 && directionX == 0) {
directionX = -1;
directionY = 0;
Serial.println("Richtung: Links");
}
Bewegung der Schlange
Die Position der Schlange wird durch eine Schleife aktualisiert, in der jedes Segment der Schlange zur Position des vorherigen Segments bewegt wird. Der Kopf der Schlange wird in die gewählte Richtung verschoben.
for (int i = snakeLength - 1; i > 0; i--) {
snakeX[i] = snakeX[i - 1];
snakeY[i] = snakeY[i - 1];
}
snakeX[0] += directionX * SNAKE_SIZE;
snakeY[0] += directionY * SNAKE_SIZE;
Fressen des Futters
Wenn die Schlange das Futter erreicht, wird ihre Länge erhöht, und ein neuer Punktestand wird berechnet. Danach kümmert sich placeFood() um eine neue Futterposition.
if (snakeX[0] == foodX && snakeY[0] == foodY) {
if (snakeLength < MAX_SNAKE_LENGTH) {
snakeX[snakeLength] = snakeX[snakeLength - 1];
snakeY[snakeLength] = snakeY[snakeLength - 1];
snakeLength++;
}
score++;
placeFood();
}
Die beiden Zeilen vor dem snakeLength++ sind wichtiger, als sie aussehen. Sie kopieren das letzte Segment auf die neue, hinzukommende Position. Ließe man sie weg, würde snakeLength++ zwar ein Segment mehr zeichnen lassen – dessen Koordinaten wären aber noch nie beschrieben worden. Beim ersten Fressen stünde dort eine 0, und für einen Frame würde ein weißer Block in der Ecke oben links aufblitzen. Erst im nächsten Schleifendurchlauf schiebt die Bewegungsschleife einen sinnvollen Wert nach. Mit den beiden Zeilen sitzt das neue Segment von Anfang an dort, wo eben noch das Schwanzende war – genau richtig, denn dort wächst die Schlange ja.
Das score++ steht übrigens bewusst außerhalb der Längenprüfung: Hat deine Schlange die maximale Länge erreicht, wächst sie zwar nicht weiter, deine Punkte zählen aber trotzdem weiter hoch.
Neues Futter platzieren
Die Funktion placeFood() würfelt eine Position aus, die nicht direkt am Rand liegt – so weit, so einfach. Danach prüft sie allerdings noch, ob das Futter zufällig auf einem Segment der Schlange gelandet ist. Falls ja, würfelt sie erneut. Ohne diese Prüfung könnte Futter mitten in deinem eigenen Körper erscheinen und wäre praktisch nicht erreichbar:
void placeFood() {
bool aufSchlange;
do {
foodX = random(2, (SCREEN_WIDTH - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
foodY = random(12 / SNAKE_SIZE, (SCREEN_HEIGHT - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
aufSchlange = false;
for (int i = 0; i < snakeLength; i++) {
if (snakeX[i] == foodX && snakeY[i] == foodY) {
aufSchlange = true;
break;
}
}
} while (aufSchlange);
}
Kollisionserkennung
Die loop()-Funktion prüft auch, ob die Schlange mit den Rändern des Spielfeldes oder mit sich selbst kollidiert:
- Wenn die Schlange mit einer Wand kollidiert, ist das Spiel vorbei.
- Wenn die Schlange ihren eigenen Körper berührt, ebenfalls.
if (snakeX[0] < SNAKE_SIZE || snakeX[0] >= SCREEN_WIDTH - SNAKE_SIZE || snakeY[0] < 10 || snakeY[0] >= SCREEN_HEIGHT - SNAKE_SIZE) {
gameOver = true;
return;
}
In beiden Fällen setzt du lediglich die Variable gameOver auf true und verlässt die Loop mit return. Das return ist kein Schönheitsfehler, sondern nötig: Ohne es liefe der restliche Code der Schleife noch mit einer Schlange weiter, die gerade eben verunglückt ist.
Game Over
Steht gameOver auf true, springt die Loop ganz am Anfang in einen eigenen kleinen Zweig: Statt zu spielen, zeigt sie deinen Score an und wartet darauf, dass du den Joystick-Taster drückst. Erst dann geht es mit einer frischen Runde weiter.
if (gameOver) {
showGameOver();
if (digitalRead(SW_PIN) == LOW) {
resetGame();
delay(200);
}
return;
}
Dass hier auf LOW geprüft wird, liegt am internen Pull-up-Widerstand, den du im Setup mit INPUT_PULLUP aktiviert hast: Der Pin liegt im Ruhezustand auf HIGH und wird beim Drücken auf Masse gezogen. Das delay(200) dahinter entprellt den Taster – ohne diese kurze Pause würde ein einziger Druck unter Umständen als mehrere Drücker gewertet.
Anzeige auf dem OLED-Display
Das OLED-Display wird in jeder Schleife aktualisiert:
- Der Punktestand wird oben links angezeigt.
- Ein weißer Rahmen wird um das Spielfeld gezeichnet, um die Grenzen des Spielfeldes anzuzeigen.
- Die Schlange und das Futter werden auf dem Display gezeichnet.
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.print("Score: ");
display.print(score);
display.drawRect(0, 10, SCREEN_WIDTH, SCREEN_HEIGHT - 10, SSD1306_WHITE);
for (int i = 0; i < snakeLength; i++) {
display.fillRect(snakeX[i], snakeY[i], SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE);
}
display.fillRect(foodX, foodY, SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE);
display.display();
Das Spiel zurücksetzen
Die resetGame()-Funktion setzt die Schlange zurück auf ihre Ausgangsposition und -länge. Auch der Punktestand wird zurückgesetzt, und eine neue Position für das Futter wird generiert. Praktisch daran: Diese Funktion stellt genau den Zustand her, den das Spiel zum Start braucht – deshalb rufst du sie nicht nur nach einem Game Over auf, sondern auch am Ende von setup().
void resetGame() {
snakeLength = 5;
directionX = 1;
directionY = 0;
score = 0;
gameOver = false;
for (int i = 0; i < snakeLength; i++) {
snakeX[i] = SCREEN_WIDTH / 2 - (i * SNAKE_SIZE);
snakeY[i] = SCREEN_HEIGHT / 2;
}
placeFood();
}
Snake auf dem ESP32 spielen
Falls du auch einen ESP32 dein Eigen nennst, kannst du Snake (fast) ganz einfach dorthin umziehen. Das ist eine gute Gelegenheit zu sehen, wie wenig ein solcher Wechsel eigentlich erfordert: Die Spiellogik bleibt Zeile für Zeile identisch, angepasst werden muss nur das, was wirklich vom Board abhängt.
Schließe das OLED-Display und den Joystick folgendermaßen am ESP32 an:
Der Sketch
Damit Snake auf dem ESP32 läuft, sind nur drei Anpassungen nötig. Die erste ist die naheliegendste: Du hinterlegst im Sketch andere Pins, denn der ESP32 hat keine Anschlüsse namens A0 und A1.
#define VRX_PIN 34 // Joystick X-Achsen-Pin
#define VRY_PIN 35 // Joystick Y-Achsen-Pin
#define SW_PIN 32 // Joystick Taster-Pin
Zweitens musst du die Schwellenwerte für den Joystick ändern, weil der ESP32 eine höhere Auflösung für die analogen Eingänge hat als der Arduino UNO. Der Arduino UNO verwendet eine 10-Bit-Auflösung, was bedeutet, dass die analogen Eingänge Werte von 0 bis 1023 zurückgeben. Der ESP32 hingegen verwendet eine 12-Bit-Auflösung, was einen Bereich von 0 bis 4095 ergibt. Die Werte 300 und 700 aus dem Arduino-Sketch lägen auf dem ESP32 also beide im linken Viertel des Joystick-Wegs – nach rechts könntest du gar nicht mehr lenken.
Für eine Bewegung nach links ist das zum Beispiel ein Wert von 1000. Im Sketch für den Arduino UNO lag dieser Wert bei 300:
if (xValue < 1000 && directionX == 0) { //Schwellenwert für den ESP32
Analog dazu wird aus dem oberen Schwellenwert 700 nun 3000 – für die Bewegungen nach rechts und nach unten.
Und drittens der Zufallsgenerator: Den Trick mit dem offenen Analogpin brauchst du hier nicht, denn der ESP32 bringt einen echten Hardware-Zufallsgenerator mit. Ein esp_random() genügt:
randomSeed(esp_random());
Das war es auch schon – alles andere bleibt, wie es ist. Übrigens: Die Geschwindigkeit der Schlange steuerst du auf beiden Boards mit dem delay() am Ende der Loop. Ist dir das Spiel zu gemächlich, setze den Wert herunter; wird es zu hektisch, erhöhe ihn. Beide Boards haben mit dieser Bildrate keinerlei Mühe, du kannst also frei nach deinem Geschmack schrauben.
Hier nun der gesamte Sketch für den ESP32:
// Snake spielen auf dem ESP32
// Pollux Labs
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Wire.h>
// OLED Display Konfiguration
#define SCREEN_WIDTH 128 // Breite des OLED-Displays
#define SCREEN_HEIGHT 64 // Höhe des OLED-Displays
#define OLED_RESET -1 // OLED-Reset-Pin (nicht verwendet)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Joystick Konfiguration
#define VRX_PIN 34 // Joystick X-Achsen-Pin
#define VRY_PIN 35 // Joystick Y-Achsen-Pin
#define SW_PIN 32 // Joystick Taster-Pin
// Eigenschaften der Schlange
#define SNAKE_SIZE 4 // Größe jedes Segments der Schlange
#define MAX_SNAKE_LENGTH 100 // Maximale Länge der Schlange
int snakeX[MAX_SNAKE_LENGTH], snakeY[MAX_SNAKE_LENGTH]; // Arrays zur Speicherung der Segmentpositionen der Schlange
int snakeLength = 5; // Anfangslänge der Schlange
int directionX = 1, directionY = 0; // Anfangsrichtung der Bewegung (nach rechts)
// Eigenschaften des Futters
int foodX, foodY; // Koordinaten des Futters, werden von placeFood() gesetzt
// Score
int score = 0;
// Spielzustand
bool gameOver = false;
void setup() {
Serial.begin(115200); // Initialisiere serielle Kommunikation
// Initialisiere Display
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Initialisiere das OLED-Display mit der I2C-Adresse 0x3C
Serial.println(F("SSD1306 allocation failed"));
for (;;); // Stoppe, falls die Display-Initialisierung fehlschlägt
}
display.clearDisplay(); // Leere den Display-Puffer
display.display(); // Zeige den geleerten Puffer an
// Initialisiere Joystick
pinMode(VRX_PIN, INPUT); // Setze Joystick X-Achsen-Pin als Eingang
pinMode(VRY_PIN, INPUT); // Setze Joystick Y-Achsen-Pin als Eingang
pinMode(SW_PIN, INPUT_PULLUP); // Setze Joystick-Taster-Pin als Eingang mit internem Pull-up-Widerstand
// Starte den Zufallsgenerator - der ESP32 hat dafür einen Hardware-Generator
// an Bord. Ohne diesen Startwert erschiene das Futter nach jedem Einschalten
// exakt an denselben Stellen.
randomSeed(esp_random());
resetGame(); // Setze Schlange, Score und Futter auf den Startzustand
Serial.println("Setup abgeschlossen");
}
void loop() {
// Game Over: Ergebnis anzeigen und auf den Joystick-Taster warten
if (gameOver) {
showGameOver();
if (digitalRead(SW_PIN) == LOW) { // dank Pull-up bedeutet LOW: gedrückt
resetGame();
delay(200); // einfaches Entprellen des Tasters
}
return;
}
// Lese Joystick-Werte
int xValue = analogRead(VRX_PIN); // Lese X-Achsen-Wert vom Joystick
int yValue = analogRead(VRY_PIN); // Lese Y-Achsen-Wert vom Joystick
// Setze Richtung basierend auf Joystick-Eingaben, verhindere diagonale Bewegung
if (xValue < 1000 && directionX == 0) { // Bewegung nach links, wenn derzeit nicht horizontal bewegt wird
directionX = -1;
directionY = 0;
Serial.println("Richtung: Links");
} else if (xValue > 3000 && directionX == 0) { // Bewegung nach rechts, wenn derzeit nicht horizontal bewegt wird
directionX = 1;
directionY = 0;
Serial.println("Richtung: Rechts");
} else if (yValue < 1000 && directionY == 0) { // Bewegung nach oben, wenn derzeit nicht vertikal bewegt wird
directionX = 0;
directionY = -1;
Serial.println("Richtung: Oben");
} else if (yValue > 3000 && directionY == 0) { // Bewegung nach unten, wenn derzeit nicht vertikal bewegt wird
directionX = 0;
directionY = 1;
Serial.println("Richtung: Unten");
}
// Aktualisiere Position der Schlange
for (int i = snakeLength - 1; i > 0; i--) { // Bewege jedes Segment zur Position des vorherigen Segments
snakeX[i] = snakeX[i - 1];
snakeY[i] = snakeY[i - 1];
}
snakeX[0] += directionX * SNAKE_SIZE; // Aktualisiere Kopfposition in X-Richtung
snakeY[0] += directionY * SNAKE_SIZE; // Aktualisiere Kopfposition in Y-Richtung
// Prüfe auf Kollision mit dem Futter
if (snakeX[0] == foodX && snakeY[0] == foodY) { // Wenn der Schlangenkopf das Futter erreicht
if (snakeLength < MAX_SNAKE_LENGTH) {
// Das neue Segment übernimmt die Position des bisherigen Schwanzes.
// Ohne diese beiden Zeilen enthielte es noch einen alten Wert und würde
// für einen Frame an falscher Stelle aufblitzen.
snakeX[snakeLength] = snakeX[snakeLength - 1];
snakeY[snakeLength] = snakeY[snakeLength - 1];
snakeLength++; // Erhöhe die Schlangenlänge
}
score++; // Erhöhe den Score
Serial.println("Futter gegessen, Schlangenlänge: " + String(snakeLength));
placeFood(); // Generiere eine neue Futterposition
}
// Prüfe auf Kollision mit den Wänden
if (snakeX[0] < SNAKE_SIZE || snakeX[0] >= SCREEN_WIDTH - SNAKE_SIZE || snakeY[0] < 10 || snakeY[0] >= SCREEN_HEIGHT - SNAKE_SIZE) {
Serial.println("Kollision mit der Wand");
gameOver = true; // Spiel beenden, wenn die Schlange die Wand trifft
return;
}
// Prüfe auf Kollision mit sich selbst
for (int i = 1; i < snakeLength; i++) {
if (snakeX[0] == snakeX[i] && snakeY[0] == snakeY[i]) { // Wenn der Schlangenkopf mit ihrem eigenen Körper kollidiert
Serial.println("Kollision mit sich selbst");
gameOver = true; // Spiel beenden
return;
}
}
// Zeichne alles
display.clearDisplay(); // Leere den Display-Puffer
// Zeichne den Score
display.setTextSize(1); // Setze Textgröße
display.setTextColor(SSD1306_WHITE); // Setze Textfarbe
display.setCursor(0, 0); // Setze Cursor für Score
display.print("Score: ");
display.print(score); // Zeige aktuellen Score an
// Zeichne den Rand um das Spielfeld
display.drawRect(0, 10, SCREEN_WIDTH, SCREEN_HEIGHT - 10, SSD1306_WHITE); // Zeichne einen weißen Rahmen um das Spielfeld
// Zeichne die Schlange
for (int i = 0; i < snakeLength; i++) {
display.fillRect(snakeX[i], snakeY[i], SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE); // Zeichne jedes Segment der Schlange
}
// Zeichne das Futter
display.fillRect(foodX, foodY, SNAKE_SIZE, SNAKE_SIZE, SSD1306_WHITE); // Zeichne das Futter
display.display(); // Zeige den aktualisierten Puffer an
delay(100); // Verzögerung zur Steuerung der Geschwindigkeit der Schlange
}
// Zeigt den Game-Over-Screen mit dem erreichten Score
void showGameOver() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(34, 16);
display.println("GAME OVER");
display.setCursor(34, 30);
display.print("Score: ");
display.print(score);
display.setCursor(4, 48);
display.print("Taster = neues Spiel");
display.display();
}
// Sucht eine freie Futterposition (nicht am Rand und nicht auf der Schlange)
void placeFood() {
bool aufSchlange;
do {
foodX = random(2, (SCREEN_WIDTH - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
foodY = random(12 / SNAKE_SIZE, (SCREEN_HEIGHT - 2 * SNAKE_SIZE) / SNAKE_SIZE) * SNAKE_SIZE;
aufSchlange = false;
for (int i = 0; i < snakeLength; i++) {
if (snakeX[i] == foodX && snakeY[i] == foodY) {
aufSchlange = true; // Treffer: neue Position würfeln
break;
}
}
} while (aufSchlange);
Serial.print("Neues Futter bei X: ");
Serial.print(foodX);
Serial.print(" Y: ");
Serial.println(foodY);
}
void resetGame() {
// Setze Eigenschaften der Schlange zurück
snakeLength = 5; // Setze die Schlangenlänge zurück
directionX = 1; // Setze die Richtung nach rechts zurück
directionY = 0;
score = 0; // Setze den Score zurück
gameOver = false; // Spiel läuft wieder
for (int i = 0; i < snakeLength; i++) {
snakeX[i] = SCREEN_WIDTH / 2 - (i * SNAKE_SIZE); // Setze die Position der Schlange zurück auf die Mitte
snakeY[i] = SCREEN_HEIGHT / 2;
}
placeFood(); // Generiere eine neue Futterposition
Serial.println("Spiel zurückgesetzt");
}
Wie geht es weiter?
Du hast nun eine spielbare Version von Snake samt einer Anzeige deiner Punkte und einem Game-Over-Screen. Wie könntest du das Spiel noch erweitern oder verbessern? Eine Idee könnte zum Beispiel ein Highscore sein: Dein Game-Over-Screen wäre der perfekte Ort dafür. Der aktuelle Halter dieses Highscores könnte mit dem Joystick seine Initialen eintragen, die neben der Punktezahl im Dateisystem des ESP32 gespeichert und auf dem Display angezeigt werden – ein guter Ansporn für eine weitere Partie Snake!