Introducción
¡Revive la nostalgia con este emocionante proyecto! Te mostramos cómo puedes construir tu propia versión del clásico juego Flappy Bird utilizando un microcontrolador ESP32. Ideal para aficionados a la electrónica y la programación, este proyecto combina una pequeña pantalla LCD, un simple botón (que en este caso usaremos un buzzer como pulsador) y el potente ESP32 para crear una experiencia de juego portátil y completamente funcional. Prepárate para sumergirte en el mundo del desarrollo de videojuegos con hardware y aprender sobre la interacción entre componentes electrónicos y software.
Materiales Necesarios
- Placa ESP32 (preferiblemente un ESP32 DevKitC o similar).
- Pantalla LCD TFT de 1.8″ (modelo ST7735).
- Protoboard (placa de pruebas).
- Cables jumper macho-macho.
- Buzzer pasivo (para los efectos de sonido).
- Pulsador (para controlar el aleteo del pájaro).
- Cable USB para programar y alimentar el ESP32.
Esquema del montaje
El montaje de este proyecto es relativamente sencillo y se realiza sobre una protoboard. Conectaremos la pantalla LCD al ESP32 utilizando el protocolo SPI, un pulsador para la interacción del usuario y un buzzer para los sonidos del juego. Asegúrate de que todas las conexiones sean firmes y correctas para evitar problemas.
Conexiones:
- ESP32 a Pantalla LCD TFT ST7735:
VCCde la pantalla a3.3Vdel ESP32.GNDde la pantalla aGNDdel ESP32.SCKde la pantalla aGPIO18del ESP32.SDA (MOSI)de la pantalla aGPIO23del ESP32.RES (Reset)de la pantalla aGPIO4del ESP32.DC (Data/Command)de la pantalla aGPIO2del ESP32.CS (Chip Select)de la pantalla aGPIO5del ESP32.BL (Backlight)de la pantalla a3.3Vdel ESP32 (o a un GPIO si quieres controlarlo).
- ESP32 a Pulsador:
- Una pata del pulsador a
GND. - La otra pata del pulsador a
GPIO13del ESP32 (se usará la resistencia pull-up interna del microcontrolador).
- Una pata del pulsador a
- ESP32 a Buzzer:
- Una pata del buzzer a
GND. - La otra pata del buzzer a
GPIO25del ESP32 (o a través de una resistencia de 220Ω si es un buzzer pasivo para protegerlo y limitar el volumen).
- Una pata del buzzer a
Código Arduino
Este es el código principal para ejecutar Flappy Bird en tu ESP32. Asegúrate de tener instaladas las librerías Adafruit GFX, Adafruit ST7735 y ESP32Tone en tu IDE de Arduino. Puedes buscarlas en el Gestor de Librerías.
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SPI.h>
#include <ESP32Tone.h> // Para el buzzer
// Definiciones de pines
#define TFT_CS 5
#define TFT_DC 2
#define TFT_RST 4
#define BUTTON_PIN 13
#define BUZZER_PIN 25
// Dimensiones de la pantalla (ST7735 puede ser 128x160 o 128x128)
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 160
// Colores estándar (RGB565)
#define ST77XX_BLACK 0x0000
#define ST77XX_WHITE 0xFFFF
#define ST77XX_BLUE 0x001F
#define ST77XX_GREEN 0x07E0
#define ST77XX_RED 0xF800
#define ST77XX_YELLOW 0xFFE0
#define ST77XX_ORANGE 0xFD20
Adafruit_ST7735 tft = Adafruit_ST7735(&SPI, TFT_CS, TFT_DC, TFT_RST);
// Estado del juego
enum GameState {
START_SCREEN,
PLAYING,
GAME_OVER_SCREEN
};
GameState currentState = START_SCREEN;
// Propiedades del pájaro
int birdX = 20;
int birdY;
float birdVelocityY;
const int birdRadius = 3; // Para dibujar un cuadrado pequeño
// Propiedades de las tuberías
struct Pipe {
int x;
int gapY; // Parte superior del hueco
const int gapHeight = 40; // Altura del hueco
const int width = 10;
};
Pipe pipes[2]; // Dos tuberías para un desplazamiento continuo
int score;
unsigned long lastButtonPressTime = 0;
const unsigned long debounceDelay = 50; // ms
// Prototipos de funciones
void setup();
void loop();
void drawBird();
void drawPipe(const Pipe& p);
void updateGame();
void handleInput();
bool checkCollision();
void resetGame();
void drawStartScreen();
void drawGameOverScreen();
void playFlapSound();
void playHitSound();
void playScoreSound();
void setup() {
Serial.begin(115200);
tft.initR(INITR_144GREENTAB); // O INITR_BLACKTAB, INITR_REDTAB según tu pantalla
tft.fillScreen(ST77XX_BLUE);
tft.setRotation(1); // Ajustar si es necesario (horizontal)
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
noTone(BUZZER_PIN); // Asegurarse de que el buzzer esté apagado inicialmente
resetGame();
}
void loop() {
handleInput();
switch (currentState) {
case START_SCREEN:
drawStartScreen();
if (digitalRead(BUTTON_PIN) == LOW) { // Botón presionado para empezar
resetGame();
currentState = PLAYING;
lastButtonPressTime = millis(); // Reiniciar debounce
}
break;
case PLAYING:
updateGame();
if (checkCollision()) {
playHitSound();
currentState = GAME_OVER_SCREEN;
}
// Redibujar todo
tft.fillScreen(ST77XX_BLUE); // Limpiar pantalla para redibujar
drawBird();
drawPipe(pipes[0]);
drawPipe(pipes[1]);
tft.setCursor(5, 5);
tft.setTextColor(ST77XX_WHITE);
tft.setTextSize(1);
tft.print("Score: ");
tft.print(score);
break;
case GAME_OVER_SCREEN:
drawGameOverScreen();
if (digitalRead(BUTTON_PIN) == LOW && millis() - lastButtonPressTime > debounceDelay) { // Botón presionado para reiniciar
resetGame();
currentState = PLAYING;
lastButtonPressTime = millis(); // Reiniciar debounce
}
break;
}
delay(20); // Pequeño retardo para la velocidad del bucle del juego
}
void handleInput() {
if (digitalRead(BUTTON_PIN) == LOW && millis() - lastButtonPressTime > debounceDelay) {
if (currentState == PLAYING) {
birdVelocityY = -5; // Aletear hacia arriba
playFlapSound();
}
lastButtonPressTime = millis();
}
}
void updateGame() {
// Física del pájaro
birdVelocityY += 0.5; // Gravedad
birdY += birdVelocityY;
// Mantener al pájaro dentro de los límites de la pantalla
if (birdY < birdRadius) {
birdY = birdRadius;
birdVelocityY = 0;
}
// Movimiento de las tuberías
pipes[0].x -= 2; // Velocidad de las tuberías
pipes[1].x -= 2;
// Reiniciar tuberías cuando salen de la pantalla
if (pipes[0].x + pipes[0].width < 0) {
pipes[0].x = SCREEN_WIDTH;
pipes[0].gapY = random(SCREEN_HEIGHT / 4, SCREEN_HEIGHT * 3 / 4 - pipes[0].gapHeight);
score++;
playScoreSound();
}
if (pipes[1].x + pipes[1].width < 0) {
pipes[1].x = SCREEN_WIDTH;
pipes[1].gapY = random(SCREEN_HEIGHT / 4, SCREEN_HEIGHT * 3 / 4 - pipes[1].gapHeight);
score++;
playScoreSound();
}
// Asegurar que las tuberías estén espaciadas inicialmente y después del reinicio
if (pipes[0].x < pipes[1].x - SCREEN_WIDTH / 2 && pipes[0].x + pipes[0].width < 0) {
pipes[0].x = pipes[1].x + SCREEN_WIDTH / 2; // Colocar después de la otra tubería
} else if (pipes[1].x < pipes[0].x - SCREEN_WIDTH / 2 && pipes[1].x + pipes[1].width < 0) {
pipes[1].x = pipes[0].x + SCREEN_WIDTH / 2;
}
}
bool checkCollision() {
// Colisión con el suelo
if (birdY > SCREEN_HEIGHT - birdRadius) {
return true;
}
// Colisión con tuberías (simplificado para pájaro cuadrado)
for (int i = 0; i < 2; i++) {
Pipe currentPipe = pipes[i];
// Comprobar si el pájaro está dentro del rango X de la tubería
if (birdX + birdRadius > currentPipe.x && birdX - birdRadius < currentPipe.x + currentPipe.width) {
// Comprobar si el pájaro golpea la parte superior o inferior de la tubería
if (birdY - birdRadius < currentPipe.gapY || birdY + birdRadius > currentPipe.gapY + currentPipe.gapHeight) {
return true;
}
}
}
return false;
}
void drawBird() {
tft.fillRect(birdX - birdRadius, birdY - birdRadius, birdRadius * 2, birdRadius * 2, ST77XX_YELLOW);
}
void drawPipe(const Pipe& p) {
// Tubería superior
tft.fillRect(p.x, 0, p.width, p.gapY, ST77XX_GREEN);
// Tubería inferior
tft.fillRect(p.x, p.gapY + p.gapHeight, p.width, SCREEN_HEIGHT - (p.gapY + p.gapHeight), ST77XX_GREEN);
}
void resetGame() {
birdY = SCREEN_HEIGHT / 2;
birdVelocityY = 0;
score = 0;
// Inicializar tuberías
pipes[0].x = SCREEN_WIDTH + 50;
pipes[0].gapY = random(SCREEN_HEIGHT / 4, SCREEN_HEIGHT * 3 / 4 - pipes[0].gapHeight);
pipes[1].x = SCREEN_WIDTH + 50 + SCREEN_WIDTH / 2 + pipes[0].width; // Escalonadas
pipes[1].gapY = random(SCREEN_HEIGHT / 4, SCREEN_HEIGHT * 3 / 4 - pipes[1].gapHeight);
tft.fillScreen(ST77XX_BLUE);
}
void drawStartScreen() {
tft.fillScreen(ST77XX_BLACK);
tft.setCursor((SCREEN_WIDTH - 6*12)/2, SCREEN_HEIGHT/2 - 20); // Centrar "FLAPPY"
tft.setTextColor(ST77XX_YELLOW);
tft.setTextSize(2);
tft.print("FLAPPY");
tft.setCursor((SCREEN_WIDTH - 4*12)/2, SCREEN_HEIGHT/2); // Centrar "BIRD"
tft.setTextColor(ST77XX_RED);
tft.print("BIRD");
tft.setCursor((SCREEN_WIDTH - 15*6)/2, SCREEN_HEIGHT/2 + 30); // Centrar "Press Button"
tft.setTextColor(ST77XX_WHITE);
tft.setTextSize(1);
tft.print("Press Button to Start");
}
void drawGameOverScreen() {
tft.fillScreen(ST77XX_ORANGE);
tft.setCursor((SCREEN_WIDTH - 9*12)/2, SCREEN_HEIGHT/2 - 20); // Centrar "GAME OVER"
tft.setTextColor(ST77XX_BLACK);
tft.setTextSize(2);
tft.print("GAME OVER");
tft.setCursor((SCREEN_WIDTH - 9*6)/2, SCREEN_HEIGHT/2 + 10); // Centrar puntuación
tft.setTextColor(ST77XX_BLACK);
tft.setTextSize(1);
tft.print("Score: ");
tft.print(score);
tft.setCursor((SCREEN_WIDTH - 15*6)/2, SCREEN_HEIGHT/2 + 40); // Centrar "Press Button to Restart"
tft.print("Press Button to Restart");
}
void playFlapSound() {
tone(BUZZER_PIN, 1000, 50); // Frecuencia, duración
}
void playHitSound() {
tone(BUZZER_PIN, 200, 200);
delay(200); // Permitir que el sonido se reproduzca
noTone(BUZZER_PIN);
}
void playScoreSound() {
tone(BUZZER_PIN, 1500, 30);
delay(30);
noTone(BUZZER_PIN);
}
Cómo funciona
El juego Flappy Bird en el ESP32 se basa en la interacción de varios componentes y una lógica de programación bien definida.
El Corazón del Juego: ESP32
El ESP32 es el cerebro de nuestro proyecto. Se encarga de:
- Procesar la lógica del juego: Calcula la posición del pájaro, el movimiento de las tuberías y las colisiones.
- Gestionar las entradas: Lee el estado del pulsador para detectar cuándo el usuario quiere que el pájaro aletee.
- Controlar las salidas: Envía los datos a la pantalla LCD para dibujar los gráficos y activa el buzzer para reproducir los efectos de sonido.
La Interfaz Visual: Pantalla LCD ST7735
La pantalla LCD de 1.8 pulgadas con el controlador ST7735 es la encargada de mostrar el juego. Se comunica con el ESP32 a través del protocolo SPI (Serial Peripheral Interface), que es rápido y eficiente para enviar datos gráficos.
- Las librerías
Adafruit_GFXyAdafruit_ST7735simplifican el dibujo de formas, texto y colores en la pantalla. - En cada fotograma del juego, el código «borra» la pantalla y vuelve a dibujar todos los elementos (pájaro, tuberías, puntuación) en sus nuevas posiciones, creando la ilusión de movimiento.
Interactuando con el Juego: El Pulsador
Un simple pulsador, conectado a uno de los pines GPIO del ESP32, es nuestro único medio de entrada. Cuando se presiona el botón, se detecta un cambio de estado en el pin (de HIGH a LOW, gracias a la configuración INPUT_PULLUP), lo que indica al juego que el pájaro debe aletear. Se implementa un pequeño retardo (debounce) para evitar múltiples detecciones por una sola pulsación.
Efectos de Sonido: El Buzzer
El buzzer, conectado a otro pin GPIO, proporciona retroalimentación auditiva. Utilizando la función tone() de la librería ESP32Tone, podemos generar diferentes tonos y duraciones para simular el sonido del aleteo, la colisión o el paso de una tubería.
La Lógica del Juego (Bucle Principal)
El programa se divide en dos funciones principales: setup() y loop().
setup(): Se ejecuta una sola vez al inicio. Aquí se inicializan la pantalla LCD, el puerto serie para depuración, los pines del botón y el buzzer, y se establece el estado inicial del juego.loop(): Esta función se ejecuta repetidamente de forma continua y contiene la lógica principal del juego:- Manejo de entrada: Llama a
handleInput()para ver si el botón ha sido presionado y ajustar la velocidad vertical del pájaro. - Actualización del juego: La función
updateGame()gestiona la física del pájaro (gravedad, movimiento vertical), el desplazamiento y generación de las tuberías, y la actualización de la puntuación. - Detección de colisiones:
checkCollision()verifica si el pájaro ha chocado con el suelo o con alguna tubería. Si hay una colisión, el estado del juego cambia a «GAME OVER». - Dibujo: Las funciones
drawBird()ydrawPipe()se encargan de renderizar los elementos en la pantalla. - Estados del juego: El juego tiene tres estados:
START_SCREEN: Muestra un mensaje de inicio y espera a que el usuario presione el botón.PLAYING: El juego está en curso, actualizando la lógica y dibujando.GAME_OVER_SCREEN: Se muestra la puntuación final y se espera una nueva pulsación para reiniciar el juego.
- Manejo de entrada: Llama a
Con esta combinación de hardware y software, hemos recreado un divertido juego retro que demuestra el potencial de los microcontroladores como el ESP32 para proyectos interactivos.
Material
Aquí os dejamos los materiales que nosotros compramos cuando empezamos. Si quieres darnos una pequeña ayuda puedes comprar desde este link. Gracias por leernos!
Placa arduino: https://amzn.to/43LM4k4
Protoboard: https://amzn.to/3LZaZdM
Cables: https://amzn.to/3K5N3ox
Leds: https://amzn.to/4iryuse
Buzzer: https://amzn.to/4ilPlfX
Pack de lo necesario para empezar: https://amzn.to/3MmuDAs
