In diesem Projekt bringen wir das klassische Tetris-Spiel auf einen ESP8266 Mikrocontroller mit einem integrierten OLED-Display. Der Code steuert die Spiel-Logik und die Darstellung auf dem Display, wobei die Blöcke in Echtzeit bewegt und gedreht werden können. Dieser Aufbau ist ideal für Einsteiger und Fortgeschrittene, die Mikrocontroller, Display-Programmierung und einfache Spieleentwicklung mit Arduino und ESP8266 kennenlernen möchten.

Watch Now!

Projektüberblick

  1. Ziel: Ein Tetris-Spiel mit visueller Anzeige und Steuerung über die Tasten.
  2. Hardware: ESP8266 mit integriertem OLED-Display und GPIOs für die Tastersteuerung.
  3. Software: Arduino IDE, Adafruit GFX und SSD1306 Bibliotheken für Display-Ansteuerung.

Benötigte Komponenten

  1. ESP8266 Mikrocontroller (mit integriertem OLED-Display, z.B. Wemos D1 Mini Pro mit OLED Shield)
  2. 4 Taster für die Steuerung (links, rechts, drehen, schnelles Fallen)
  3. Kabel und Breadboard für die Verbindungen

Schaltplan und Verbindungen

  1. Taster-Belegung am ESP8266:

    • Links: Verbinden Sie den ersten Taster mit GPIO12 (D6).
    • Rechts: Verbinden Sie den zweiten Taster mit GPIO14 (D5).
    • Drehen: Verbinden Sie den dritten Taster mit GPIO0 (D3).
    • Schnelles Fallen: Verbinden Sie den vierten Taster mit GPIO2 (D4).
  2. Stromversorgung:

    • Der ESP8266 benötigt eine Versorgungsspannung von 3,3V und kann per USB-Kabel angeschlossen werden.
  3. OLED-Display:

    • Das OLED-Display ist bereits auf dem ESP8266 Shield montiert und über die I2C-Schnittstelle mit dem Mikrocontroller verbunden.

Schritt-für-Schritt-Anleitung

  1. Installieren der Arduino IDE und Bibliotheken:

    • Laden Sie die Arduino IDE herunter und installieren Sie die ESP8266 Board-Unterstützung.
    • Gehen Sie zu Sketch -> Include Library -> Manage Libraries und installieren Sie:
      • Adafruit SSD1306
      • Adafruit GFX
  2. Einrichten des ESP8266 in der Arduino IDE:

    • Fügen Sie die ESP8266 Board-URL in den Voreinstellungen hinzu (http://arduino.esp8266.com/stable/package_esp8266com_index.json).
    • Wählen Sie in der Board-Auswahl Ihren ESP8266 aus.
  3. Code für das Tetris-Spiel laden und anpassen:

    • Laden Sie den folgenden Code in die Arduino IDE und passen Sie, falls nötig, die GPIO-Einstellungen für die Tasten an. Dieser Code verwendet die Adafruit-Bibliotheken, um Blöcke und das Spielfeld zu zeichnen und Eingaben zu verarbeiten.
    • Die Blöcke fallen automatisch, und Spieler können sie durch Eingaben rotieren, nach links und rechts bewegen oder schneller fallen lassen.
  4. Code-Übersicht und Funktionsweise:

    • Setup-Bereich:
      • Initialisiert das Display und startet die I2C-Kommunikation mit dem OLED.
    • Loop-Bereich:
      • Menü und Spielstatus: Vor dem Spielstart wird ein Menü angezeigt, danach steuert die Schleife die Blöcke und Spielfeld-Darstellung.
      • Spiel-Logik: Der Code prüft Bewegungen, Kollisionen und rotierende Blöcke und verankert sie bei Bedarf im Spielfeld.
    • Display-Darstellung:
      • Blöcke und das Spielfeld werden mithilfe von display.drawRect() und display.fillRect() gezeichnet.
    • Spielsteuerung:
      • Über die GPIOs werden die Tastereingaben verarbeitet, um das Tetris-Spiel interaktiv zu gestalten.
  5. Upload des Codes:

    • Verbinden Sie das ESP8266-Board mit Ihrem Computer und wählen Sie den richtigen Port in der Arduino IDE aus.
    • Laden Sie den Code auf das ESP8266 hoch. Nach dem Upload sollte auf dem OLED das Tetris-Menü erscheinen.

Code-Besprechung

Der Code ist in mehrere logische Abschnitte unterteilt:

  1. Spielstart und Menü:

    • Zeigt das Menü auf dem Display an und wartet auf eine Eingabe zum Starten des Spiels.
  2. Spielfeld-Darstellung:

    • Das Spielfeld wird als Matrix dargestellt, wobei die Blöcke auf dem Display angezeigt und in ihrem Status aktualisiert werden.
  3. Block-Formen und Rotationen:

    • Jede Blockform ist als 4x4-Matrix codiert. Verschiedene Rotationen der Formen werden mit Bit-Manipulationen angezeigt und auf gültige Positionen überprüft.
  4. Automatische Bewegung und Tastensteuerung:

    • Blöcke fallen automatisch, und der Spieler kann sie durch die Tasten rotieren oder bewegen.
  5. Spielende:

    • Wenn ein Block nicht platziert werden kann, wird das Spiel beendet und ein „Game Over“-Bildschirm angezeigt.

Beispielcode für den Upload

Dieser Code kann als Basis verwendet und an Ihre Bedürfnisse angepasst werden:

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Spielfeld und Blöcke
const int fieldWidth = 16;
const int fieldHeight = 8;
const int blockWidth = 8;
const int blockHeight = 8;
int field[fieldHeight][fieldWidth] = {0}; 

const int shapes[7][4] = {
  {0x0F00, 0x2222, 0x00F0, 0x4444},
  {0x0E40, 0x4C40, 0x4E00, 0x4640},
  {0x0E80, 0x6C00, 0x8E00, 0xC600},
  {0x06C0, 0x8C80, 0x6C00, 0xC880},
  {0x0660, 0x0660, 0x0660, 0x0660},
  {0x0C60, 0x4C80, 0x0C60, 0x4C80},
  {0x0E20, 0x2C40, 0x8E00, 0x4C40}
};

int currentShape = 0;
int rotation = 0;
int posX = 0, posY = fieldHeight / 2 - 2;

unsigned long lastFallTime = 0;
const int fallDelay = 1000;

bool gameStarted = false;
bool gameOver = false;

void setup() {
  Wire.begin(12, 14);
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { 
    for(;;);
  }
  display.clearDisplay();
}

void loop() {
  if (!gameStarted) {
    drawMenu();
    handleMenuInput();
    delay(100);
  } else {
    display.clearDisplay();
    if (gameOver) {
      drawGameOver();
      handleGameOverInput();
    } else {
      drawFrame();
      drawField();
      drawShape();
    }
    display.display();
    handleInput();
    unsigned long currentTime = millis();
    if (currentTime - lastFallTime >= fallDelay) {
      moveShapeDown();
      lastFallTime = currentTime;
    }
    delay(50);
  }
}

 

code mit meine menü :

 

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Spielfeld und Blöcke
const int fieldWidth = 16;  // Breite des Spielfelds
const int fieldHeight = 8;  // Höhe des Spielfelds
const int blockWidth = 8;    // Blockbreite
const int blockHeight = 8;   // Blockhöhe
int field[fieldHeight][fieldWidth] = {0}; // 0 = leer, 1 = Block

// Tetrimino Formen
const int shapes[7][4] = {
  {0x0F00, 0x2222, 0x00F0, 0x4444}, // I-Form
  {0x0E40, 0x4C40, 0x4E00, 0x4640}, // T-Form
  {0x0E80, 0x6C00, 0x8E00, 0xC600}, // L-Form
  {0x06C0, 0x8C80, 0x6C00, 0xC880}, // Z-Form
  {0x0660, 0x0660, 0x0660, 0x0660}, // Quadrat
  {0x0C60, 0x4C80, 0x0C60, 0x4C80}, // S-Form
  {0x0E20, 0x2C40, 0x8E00, 0x4C40}  // J-Form
};

int currentShape = 0;
int rotation = 0;
int posX = 0, posY = fieldHeight / 2 - 2;

// Timing für automatisches Fallen
unsigned long lastFallTime = 0;
const int fallDelay = 1000; // Zeit in Millisekunden, bis der Block nach unten fällt

bool gameStarted = false; // Status, ob das Spiel gestartet ist
bool gameOver = false;    // Status, ob das Spiel vorbei ist
unsigned long gameOverTime = 0; // Zeitstempel für Game Over

void setup() {
  Wire.begin(12, 14); // SDA = GPIO12 (D6), SCL = GPIO14 (D5)
  
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { 
    Serial.println(F("SSD1306 konnte nicht initialisiert werden"));
    for(;;);
  }
  display.clearDisplay();
  display.display();
}

void loop() {
  if (!gameStarted) {
    display.clearDisplay();
    drawMenu();  // Menü zeichnen
    display.display();

    // Eingaben verarbeiten
    handleMenuInput(); 
    delay(100); // Verzögerung für das Menü
  } else {
    display.clearDisplay();
    if (gameOver) {
      drawGameOver();  // Game Over-Bildschirm zeichnen
      // Nur auf Tastendruck warten, um das Spiel neu zu starten
      handleGameOverInput();
    } else {
      drawFrame();
      drawField();
      drawShape();
    }
    display.display();
    
    // Eingaben verarbeiten
    handleInput(); 
    
    // Automatisches Fallen der Blöcke
    unsigned long currentTime = millis();
    if (currentTime - lastFallTime >= fallDelay) {
      moveShapeDown(); // Blöcke nach unten bewegen
      lastFallTime = currentTime; // Zeitstempel aktualisieren
    }
    
    delay(50); // Kurze Verzögerung, um die Reaktionszeit zu verbessern
  }
}

// Zeichnet das Startmenü
void drawMenu() {
  display.setTextSize(1); // Kleinere Schriftgröße für das Menü
  display.setTextColor(SSD1306_WHITE);
  
  // HD Robotics
  display.setCursor(30, 8); // Hoch und zentriert
  display.print("HD Robotics");
  
  // Tetris
  display.setTextSize(2);
  display.setCursor(35, 30);
  display.print("Tetris");
  
  // Webseite
  display.setTextSize(1);
  display.setCursor(30, 50);
  display.print("hdrobotics.de");
}

// Zeichnet den Game Over-Bildschirm
void drawGameOver() {
  display.setTextSize(1); // Kleinere Schrift für "Game Over"
  display.setTextColor(SSD1306_WHITE);
  
  display.setCursor(40, 20); // Zentriert
  display.print("Game Over"); // Game Over in Bold
  
  // L-Block als Logo
  int logoX = (SCREEN_WIDTH - blockWidth) / 2; // Zentrieren
  int logoY = 35; // Platz für das Logo unter "Game Over"
  
  // Zeichne L-Block liegend
  display.fillRect(logoX, logoY, blockWidth * 2, blockHeight, SSD1306_WHITE); // L-Block
}

// Der Rest des Codes bleibt gleich...
// Zeichnet den Rahmen um das Spielfeld
void drawFrame() {
  int frameX = (SCREEN_WIDTH - fieldWidth * blockWidth) / 2;
  int frameY = (SCREEN_HEIGHT - fieldHeight * blockHeight) / 2;
  int frameWidth = fieldWidth * blockWidth;
  int frameHeight = fieldHeight * blockHeight;

  // Rahmen zeichnen
  display.drawRect(frameX - 1, frameY - 1, frameWidth + 2, frameHeight + 2, SSD1306_WHITE);
  display.setCursor(frameX + 5, frameY - 10);
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.print("Tetris"); // Titel des Spiels
}

void drawField() {
  int offsetX = (SCREEN_WIDTH - fieldWidth * blockWidth) / 2;
  int offsetY = (SCREEN_HEIGHT - fieldHeight * blockHeight) / 2;
  for (int y = 0; y < fieldHeight; y++) {
    for (int x = 0; x < fieldWidth; x++) {
      if (field[y][x]) {
        display.fillRect(offsetX + x * blockWidth, offsetY + y * blockHeight, blockWidth - 1, blockHeight - 1, SSD1306_WHITE);
      }
    }
  }
}

void drawShape() {
  int shape = shapes[currentShape][rotation];
  int offsetX = (SCREEN_WIDTH - fieldWidth * blockWidth) / 2;
  int offsetY = (SCREEN_HEIGHT - fieldHeight * blockHeight) / 2;
  for (int i = 0; i < 16; i++) {
    int x = i % 4;
    int y = i / 4;
    if ((shape & (0x8000 >> i)) != 0) {
      display.fillRect(offsetX + (posX + x) * blockWidth, offsetY + (posY + y) * blockHeight, blockWidth - 1, blockHeight - 1, SSD1306_WHITE);
    }
  }
}

void moveShapeRight() {
  posX++;
  if (!checkValidMove(posX, posY)) {
    posX--; // Zurücksetzen, wenn die Bewegung ungültig ist
  }
}

void moveShapeLeft() {
  posX--;
  if (!checkValidMove(posX, posY)) {
    posX++; // Zurücksetzen, wenn die Bewegung ungültig ist
  }
}

void moveShapeDown() {
  posY++;
  if (!checkValidMove(posX, posY)) {
    posY--; // Zurücksetzen, wenn die Bewegung ungültig ist
    mergeShape(); // Block im Spielfeld verankern
    spawnNewShape(); // Neuen Block erzeugen
  }
}

void moveShapeUp() {
  posY--;
  if (!checkValidMove(posX, posY)) {
    posY++; // Zurücksetzen, wenn die Bewegung ungültig ist
  }
}

void rotateShape() {
  rotation = (rotation + 1) % 4; // Rotation zirkulär
  if (!checkValidMove(posX, posY)) {
    rotation = (rotation + 3) % 4; // Rückwärtsrotation
  }
}

bool checkValidMove(int newX, int newY) {
  int shape = shapes[currentShape][rotation];
  for (int i = 0; i < 16; i++) {
    int x = i % 4;
    int y = i / 4;
    if ((shape & (0x8000 >> i)) != 0) {
      int px = newX + x;
      int py = newY + y;
      if (px < 0 || px >= fieldWidth || py >= fieldHeight || field[py][px] != 0) {
        return false; // Ungültige Bewegung
      }
    }
  }
  return true; // Gültige Bewegung
}

void mergeShape() {
  int shape = shapes[currentShape][rotation];
  for (int i = 0; i < 16; i++) {
    int x = i % 4;
    int y = i / 4;
    if ((shape & (0x8000 >> i)) != 0) {
      field[posY + y][posX + x] = 1; // Block im Spielfeld verankern
    }
  }
}

void spawnNewShape() {
  currentShape = random(0, 7); // Zufällige Form auswählen
  rotation = 0;
  posX = fieldWidth / 2 - 2; // In der Mitte starten
  posY = 0; // Oben starten
  if (!checkValidMove(posX, posY)) {
    gameOver = true; // Spiel vorbei, wenn kein Platz für neue Form
    gameOverTime = millis(); // Zeitstempel für Game Over setzen
  }
}

void resetGame() {
  for (int y = 0; y < fieldHeight; y++) {
    for (int x = 0; x < fieldWidth; x++) {
      field[y][x] = 0; // Spielfeld zurücksetzen
    }
  }
  gameOver = false; // Spielstatus zurücksetzen
}

// Eingabeverarbeitung
void handleInput() {
  int key = analogRead(A0); // Analog Pin für die Tasteneingabe
  if (gameOver) {
    // Keine Eingabe verarbeiten, während Game Over
  } else {
    if (key < 200) { // SW1 - Links
      moveShapeLeft();
    } else if (key < 400) { // SW2 - Drehen
      rotateShape();
    } else if (key < 600) { // SW3 - Schnell nach unten
      moveShapeDown(); // Diese Bewegung ist für schnellen Fall
    } else if (key < 800) { // SW4 - Rechts oder nach oben
      moveShapeRight(); // SW4 bewegt nach rechts
      // Hier können wir auch die Bewegung nach oben hinzufügen
      if (key < 400) {
        moveShapeUp(); // Bewegung nach oben
      }
    }
  }
}

// Eingabeverarbeitung für das Menü
void handleMenuInput() {
  int key = analogRead(A0); // Analog Pin für die Tasteneingabe
  if (key < 200) { // Wenn eine Taste gedrückt wird, Spiel starten
    gameStarted = true; // Spielstatus auf "gestart" setzen
    resetGame(); // Spielfeld zurücksetzen
    spawnNewShape(); // Ersten Block erzeugen
  }
}

// Eingabeverarbeitung für den Game Over-Bildschirm
void handleGameOverInput() {
  int key = analogRead(A0); // Analog Pin für die Tasteneingabe
  if (key < 200) { // Wenn eine Taste gedrückt wird, Spiel neu starten
    resetGame(); // Spiel zurücksetzen
    spawnNewShape(); // Ersten Block erzeugen
    gameStarted = true; // Spielstatus auf "gestart" setzen
    gameOver = false; // Game Over Status zurücksetzen
  }
}

 

Mit dieser Anleitung und dem Beispielcode sollten Sie ein funktionierendes Tetris-Spiel auf Ihrem ESP8266 mit OLED-Display erstellen können!