Arbol de Navidad con Arduino

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:
    • VCC de la pantalla a 3.3V del ESP32.
    • GND de la pantalla a GND del ESP32.
    • SCK de la pantalla a GPIO18 del ESP32.
    • SDA (MOSI) de la pantalla a GPIO23 del ESP32.
    • RES (Reset) de la pantalla a GPIO4 del ESP32.
    • DC (Data/Command) de la pantalla a GPIO2 del ESP32.
    • CS (Chip Select) de la pantalla a GPIO5 del ESP32.
    • BL (Backlight) de la pantalla a 3.3V del ESP32 (o a un GPIO si quieres controlarlo).
  • ESP32 a Pulsador:
    • Una pata del pulsador a GND.
    • La otra pata del pulsador a GPIO13 del ESP32 (se usará la resistencia pull-up interna del microcontrolador).
  • ESP32 a Buzzer:
    • Una pata del buzzer a GND.
    • La otra pata del buzzer a GPIO25 del ESP32 (o a través de una resistencia de 220Ω si es un buzzer pasivo para protegerlo y limitar el volumen).

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_GFX y Adafruit_ST7735 simplifican 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() y drawPipe() 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.

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