Top Angebote und noch mehr!

Projekt Beschreibung
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.
Projektüberblick
- Ziel: Ein Tetris-Spiel mit visueller Anzeige und Steuerung über die Tasten.
- Hardware: ESP8266 mit integriertem OLED-Display und GPIOs für die Tastersteuerung.
- Software: Arduino IDE, Adafruit GFX und SSD1306 Bibliotheken für Display-Ansteuerung.
Benötigte Komponenten
- ESP8266 Mikrocontroller (mit integriertem OLED-Display, z.B. Wemos D1 Mini Pro mit OLED Shield)
- 4 Taster für die Steuerung (links, rechts, drehen, schnelles Fallen)
- Kabel und Breadboard für die Verbindungen
Schaltplan und Verbindungen
- 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).
- Stromversorgung:
- Der ESP8266 benötigt eine Versorgungsspannung von 3,3V und kann per USB-Kabel angeschlossen werden.
- 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
- 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
- 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.
- Fügen Sie die ESP8266 Board-URL in den Voreinstellungen hinzu (
- 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.
- 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()unddisplay.fillRect()gezeichnet.
- Blöcke und das Spielfeld werden mithilfe von
- Spielsteuerung:
- Über die GPIOs werden die Tastereingaben verarbeitet, um das Tetris-Spiel interaktiv zu gestalten.
- Setup-Bereich:
- 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:
- Spielstart und Menü:
- Zeigt das Menü auf dem Display an und wartet auf eine Eingabe zum Starten des Spiels.
- Spielfeld-Darstellung:
- Das Spielfeld wird als Matrix dargestellt, wobei die Blöcke auf dem Display angezeigt und in ihrem Status aktualisiert werden.
- Block-Formen und Rotationen:
- Jede Blockform ist als 4×4-Matrix codiert. Verschiedene Rotationen der Formen werden mit Bit-Manipulationen angezeigt und auf gültige Positionen überprüft.
- Automatische Bewegung und Tastensteuerung:
- Blöcke fallen automatisch, und der Spieler kann sie durch die Tasten rotieren oder bewegen.
- 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
#include
#include
#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
#include
#include
#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
}
}
