Construcción de Galaga con Arduino y Pantalla OLED

Corría el año 1981 cuando Namco lanzó al mundo Galaga, la secuela del ya exitoso Galaxian. Su mecánica de enemigos que descienden en formación y la posibilidad de que tu nave fuera capturada para luego ser rescatada (creando una nave dual) revolucionó los salones recreativos.

Hoy, gracias a la democratización de la electrónica, no necesitamos un gabinete de madera de 2 metros de altura ni pesadas placas de circuitos integrados. Con un Arduino Nano o Uno, podemos encapsular esa esencia en la palma de nuestra mano.



🔩Funcionamiento

El Cerebro del Sistema: Arduino UNO

El Arduino UNO utiliza el microcontrolador ATmega328P de Atmel (ahora Microchip). Este chip opera típicamente a 16 MHz y ofrece una arquitectura sencilla pero potente con 32 KB de memoria Flash para el código.

I2C es un protocolo de comunicación serial que permite que un microcontrolador (el Maestro) se comunique con múltiples periféricos (los Esclavos) usando solo dos cables de señal, además de la alimentación.

Los modelos más modernos de Arduino (como el Uno R3, el Mega, o el Leonardo) también tienen pines I2C dedicados cerca del pin AREF, pero internamente, en el Uno, están conectados a A4 y A5. Por compatibilidad, siempre se recomienda usar A4 y A5.

La Comunicación I2C y la Pantalla OLED

La pantalla OLED (Organic Light-Emitting Diode) es ideal por su alto contraste, bajo consumo de energía y tamaño compacto. La mayoría de los módulos de 128 x 32 píxeles utilizan el chip controlador SSD1306 o SSH1106 y se comunican a través del bus I2C.

  • Protocolo de Dos Hilos: I2C es un bus de comunicación serial que, de manera inteligente, requiere solo dos líneas para transferir datos entre el maestro (Arduino) y los esclavos (la pantalla):
    • SDA (Serial Data Line): Línea de datos.
    • SCL (Serial Clock Line): Línea de sincronización del reloj.

PINES del OLED 128×32(SSD1306)

Pin OLED 128×32(I2C)ProtocoloNotas Importantes
VCCAlimentaciónUso de 5V.
GNDTierraConexión de referencia.
SCLI2CLínea de Reloj Serial.
SDAI2CLínea de Datos Serial.

🔨Componentes

ComponenteCantidadEspecificaciónFunción
Placa Arduino1Arduino Uno, Nano, etc.Control de componentes(cerebro)
Pantalla OLED1128×32 (SSD1306)Mostrar información
Botón3PushMover/Disparar
Resistencias310kΩPull-down para el botón
Cables de conexiónnUnir los componentes

🔌Conexiones

Conexión OLED 128×32

OLED 128×32(I2C) PinArduino Pin
GNDGND
VCC5V
SCLA5
SDAA4

Conexión del botón(Configuración Pull-Down):

Arduino PinArduino Pin
5vPin 2 (con resistencia de 10kΩ a GND)
5vPin 3 (con resistencia de 10kΩ a GND)
5vPin 4 (con resistencia de 10kΩ a GND)

0️⃣Código

Asegúrate de instalar las librerías necesarias desde el Gestor de Librerías del IDE de Arduino:

  1. Adafruit GFX Library (librería gráfica principal)
  2. Adafruit SSD1306 (driver específico para la pantalla)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
#include <Arduino.h>

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

void inicializarEnemigos();

// ----------------------------------------------------
// --- 1. DEFINICIONES DE HARDWARE Y CONSTANTES ---
// ----------------------------------------------------

// Definición de la Pantalla OLED 128x32
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32
#define OLED_RESET -1 // Pin de Reset
// Inicialización de la pantalla usando la dirección I2C 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Definición de Pines de Control
#define PIN_BTN_DER 2
#define PIN_BTN_IZQ 3
#define PIN_BTN_DISPARO 4

// Constantes Físicas del Juego
#define NAVE_W 5             // Ancho de la nave del jugador
#define NAVE_H 5             // Altura de la nave del jugador
#define VEL_NAVE 2           // Velocidad de movimiento por paso
#define PROYECTIL_W 1        // Ancho del proyectil
#define PROYECTIL_H 3        // Altura del proyectil
#define VEL_PROYECTIL 3      // Velocidad de ascenso del proyectil
#define ENEMIGO_W 6          // Ancho de los enemigos
#define ENEMIGO_H 3          // Altura de los enemigos
#define MAX_ENEMIGOS 5       // Número total de enemigos en el escuadrón
#define MAX_PROYECTILES 1    // Límite de proyectiles en pantalla a la vez


int contadorEnemigos = MAX_ENEMIGOS;
// ----------------------------------------------------
// --- 2. ESTRUCTURAS Y VARIABLES GLOBALES ---
// ----------------------------------------------------

// Estructura para la Nave del Jugador
struct Nave {
  int x; // Coordenada x de la esquina superior izquierda
};

// Estructura para los Proyectiles (Misiles)
struct Proyectil {
  int x, y; // Coordenadas
  bool activo; // Estado (en vuelo o no)
};

// Estructura para los Enemigos
struct Enemigo {
  int x, y; // Coordenadas
  bool vivo; // Estado de vida
  int velX; // Dirección y velocidad de movimiento horizontal
};

Nave jugador = {SCREEN_WIDTH / 2}; // Inicializa la nave en el centro
Proyectil misil[MAX_PROYECTILES];
Enemigo escuadron[MAX_ENEMIGOS];

int puntuacion = 0;
bool juegoActivo = true;

// ----------------------------------------------------
// --- 3. FUNCIONES DE DIBUJO ---
// ----------------------------------------------------

/**
 * Dibuja la nave del jugador en la posición y el tamaño predefinidos.
 * @param x Posición horizontal (esquina superior izquierda).
 */
void dibujarNave(int x) {
  // Posición Y fija cerca del fondo de la pantalla
  int y = SCREEN_HEIGHT - NAVE_H;
  // Dibujar la nave como un rectángulo relleno para visibilidad
  display.drawRect(x, y, NAVE_W, NAVE_H, SSD1306_WHITE);
  display.fillRect(x + 1, y + 1, NAVE_W - 2, NAVE_H - 2, SSD1306_WHITE);
}

/**
 * Dibuja un solo enemigo.
 * @param x Posición horizontal (esquina superior izquierda).
 * @param y Posición vertical (esquina superior izquierda).
 */
void dibujarEnemigo(int x, int y) {
  display.drawRect(x, y, ENEMIGO_W, ENEMIGO_H, SSD1306_WHITE);
  // Pequeño detalle interno para darle forma
  display.drawPixel(x + 3, y + 2, SSD1306_WHITE);
}

/**
 * Dibuja un proyectil.
 * @param x Posición horizontal (esquina superior izquierda).
 * @param y Posición vertical (esquina superior izquierda).
 */
void dibujarProyectil(int x, int y) {
  display.drawRect(x, y, PROYECTIL_W, PROYECTIL_H, SSD1306_WHITE);
}

/**
 * Borra la pantalla y redibuja todos los elementos del juego:
 * puntuación, nave, proyectiles y enemigos.
 */
void dibujarTodo() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  // 1. Dibujar Puntuación
  display.setCursor(0, 0);
  display.print(F("SCORE: "));
  display.print(puntuacion);

  // 2. Dibujar Nave
  dibujarNave(jugador.x);

  // 3. Dibujar Proyectiles
  for (int i = 0; i < MAX_PROYECTILES; i++) {
    if (misil[i].activo) {
      dibujarProyectil(misil[i].x, misil[i].y);
    }
  }

  // 4. Dibujar Enemigos
  for (int i = 0; i < MAX_ENEMIGOS; i++) {
    if (escuadron[i].vivo) {
      dibujarEnemigo(escuadron[i].x, escuadron[i].y);
    }
  }

  display.display(); // Envía el buffer a la pantalla física
}

/**
 * Muestra la pantalla de Game Over y la puntuación final.
 */
void pantallaGameOver() {
  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(10, 5);
  display.println(F("GAME OVER"));
  display.setTextSize(1);
  display.setCursor(10, 25);
  display.print(F("Puntaje Final: "));
  display.print(puntuacion);
  display.display();
  delay(8000); // Muestra por 5 segundos

  // Reiniciar estado del juego
  puntuacion = 0;
  jugador.x = SCREEN_WIDTH / 2;
  // Se llama a inicializarEnemigos() al salir de la función
  inicializarEnemigos();
  juegoActivo = true;
}

// ----------------------------------------------------
// --- 4. FUNCIONES DE LÓGICA DE JUEGO ---
// ----------------------------------------------------

/**
 * Inicializa el escuadrón de enemigos en sus posiciones iniciales.
 */
void inicializarEnemigos() {
  for (int i = 0; i < MAX_ENEMIGOS; i++) {
    escuadron[i].x = 10 + i * 20; // Espaciado horizontal
    escuadron[i].y = 5; // Fila superior
    escuadron[i].vivo = true;
    escuadron[i].velX = 1; // Dirección inicial a la derecha
  }
}

/**
 * Lee el estado de los botones y actualiza la posición del jugador
 * y el disparo de proyectiles.
 */
void leerEntradas() {
  // Los botones están en modo INPUT_PULLUP, por lo que LOW significa presionado.

  // Movimiento a la izquierda
  if (digitalRead(PIN_BTN_IZQ) == LOW) {
    jugador.x -= VEL_NAVE;
    if (jugador.x < 0) jugador.x = 0;
  }

  // Movimiento a la derecha
  if (digitalRead(PIN_BTN_DER) == LOW) {
    jugador.x += VEL_NAVE;
    if (jugador.x > SCREEN_WIDTH - NAVE_W) jugador.x = SCREEN_WIDTH - NAVE_W;
  }

  // Disparo (Solo se permite 1 proyectil activo a la vez)
  if (digitalRead(PIN_BTN_DISPARO) == HIGH) {
    if (!misil[0].activo) {
      // Posiciona el proyectil centrado encima de la nave
      misil[0].x = jugador.x + (NAVE_W / 2) - (PROYECTIL_W / 2);
      misil[0].y = SCREEN_HEIGHT - NAVE_H - PROYECTIL_H - 1;
      misil[0].activo = true;
    }
  }
}

/**
 * Mueve los proyectiles activos hacia arriba y los desactiva si salen de la pantalla.
 */
void actualizarProyectiles() {
  for (int i = 0; i < MAX_PROYECTILES; i++) {
    if (misil[i].activo) {
      misil[i].y -= VEL_PROYECTIL;
      // Desactivar si sale de la pantalla (y < 0)
      if (misil[i].y < 0) {
        misil[i].activo = false;
      }
    }
  }
}

/**
 * Mueve los enemigos horizontalmente y maneja la inversión de dirección
 * y el descenso al tocar los bordes.
 */
void actualizarEnemigos() {
  bool bordeAlcanzado = false;

  // 1. Mover enemigos y chequear si se alcanzó un borde
  for (int i = 0; i < MAX_ENEMIGOS; i++) {
    if (escuadron[i].vivo) {
      escuadron[i].x += escuadron[i].velX;

      if (escuadron[i].x <= 0 || escuadron[i].x >= SCREEN_WIDTH - ENEMIGO_W) {
        bordeAlcanzado = true;
      }
    }
  }

  // 2. Si se alcanzó un borde, invertir dirección y descender
  if (bordeAlcanzado) {
    for (int i = 0; i < MAX_ENEMIGOS; i++) {
      if (escuadron[i].vivo) {
        escuadron[i].velX *= -1; // Invertir
        escuadron[i].y += ENEMIGO_H + 2; // Descender (salto vertical)

        // Colisión con el jugador (El enemigo ha llegado al fondo)
        if (escuadron[i].y >= SCREEN_HEIGHT - NAVE_H) {
          juegoActivo = false; // Game Over
        }
      }
    }
  }
}

/**
 * Comprueba las colisiones entre proyectiles y enemigos (AABB).
 */
void chequearColisiones() {
  for (int i = 0; i < MAX_PROYECTILES; i++) {
    if (misil[i].activo) {
      for (int j = 0; j < MAX_ENEMIGOS; j++) {
        if (escuadron[j].vivo) {
          // Chequeo de colisión AABB (Axis-Aligned Bounding Box)
          // Si el proyectil y el enemigo se superponen en X y Y:
          if (misil[i].x < escuadron[j].x + ENEMIGO_W &&
              misil[i].x + PROYECTIL_W > escuadron[j].x &&
              misil[i].y < escuadron[j].y + ENEMIGO_H &&
              misil[i].y + PROYECTIL_H > escuadron[j].y)              
          {
            // Impacto: desactivar misil, matar enemigo, aumentar puntuación
            misil[i].activo = false;
            escuadron[j].vivo = false;
            puntuacion += 10;
            contadorEnemigos--;
              if(contadorEnemigos <= 0){       
                contadorEnemigos = MAX_ENEMIGOS;         
                inicializarEnemigos();
              }
          }
        }
      }
    }
  }
}

// ----------------------------------------------------
// --- 5. SETUP (Configuración Inicial) ---
// ----------------------------------------------------

void setup() {
  // Configuración de Pines de Botón con Pull-up Interno
  pinMode(PIN_BTN_IZQ, INPUT_PULLUP);
  pinMode(PIN_BTN_DER, INPUT_PULLUP);
  pinMode(PIN_BTN_DISPARO, INPUT_PULLUP);

  // Inicialización de la pantalla OLED
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { 
    // Si falla la inicialización, nos quedamos en un bucle infinito
    for(;;); 
  }
  display.clearDisplay();
  display.setTextSize(3);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(10, 5);
  display.println(F("GALAGA"));
  display.display();
  delay(5000); // Pequeño saludo inicial
  display.clearDisplay();

  // Inicialización de las entidades del juego
  inicializarEnemigos();
  for (int i = 0; i < MAX_PROYECTILES; i++) {
    misil[i].activo = false;
  }
}

// ----------------------------------------------------
// --- 6. LOOP (Bucle Principal del Juego) ---
// ----------------------------------------------------

void loop() {
  if (juegoActivo) {
    leerEntradas();
    actualizarProyectiles();
    actualizarEnemigos();
    chequearColisiones();
    dibujarTodo();
    
    // Control de velocidad del juego: 30ms = ~33 FPS
    delay(50); 
  } else {
    pantallaGameOver();
    // Al salir de pantallaGameOver, juegoActivo vuelve a ser true y loop reinicia.
  }
}


🖌️Diseños


🎬Videos


📑Conclusión

La culminación de este proyecto de Galaga en Arduino representa mucho más que el simple ensamblaje de componentes electrónicos; es un testimonio de cómo la ingeniería moderna permite democratizar el desarrollo de software y hardware. A través de este ejercicio, hemos logrado sintetizar conceptos complejos de computación en un sistema compacto y funcional.

Reflexiones Técnicas y Educativas

El desarrollo de este sistema nos permite extraer varias lecciones fundamentales para cualquier entusiasta de la tecnología:

  • Optimización de Recursos: Hemos aprendido que no se requiere una supercomputadora para crear experiencias interactivas. La gestión de la memoria RAM del Arduino (apenas 2KB en un ATmega328P) para manejar el buffer de la pantalla OLED nos enseña a escribir código eficiente y limpio.
  • Interacción Hombre-Máquina (HMI): La implementación de los tres botones nos recuerda la importancia de la latencia. En un juego de disparos, un retraso de milisegundos entre la pulsación y la acción puede arruinar la experiencia. La programación mediante interrupciones o ciclos de lectura rápidos es clave para el éxito del proyecto.
  • Diseño Modular: Al separar la lógica de movimiento, el sistema de colisiones y el renderizado gráfico, hemos creado un motor de juego básico que puede ser adaptado para otros clásicos como Space Invaders o Asteroids con cambios mínimos.

El Futuro de tu Consola DIY

Este proyecto es una base sólida, pero el horizonte de mejoras es amplio. La arquitectura que hemos construido permite escalar el diseño hacia nuevas fronteras. Podrías considerar la adición de un módulo de vibración para retroalimentación táctil al recibir daño, o incluso la implementación de una pantalla TFT a color para capturar la estética vibrante del arcade original de 1981.

En última instancia, este Galaga portátil es una prueba de que, con curiosidad y los componentes adecuados, es posible traer una pieza de la historia de la computación al presente, manteniéndola viva en un circuito diseñado por ti mismo. La satisfacción de ver tu propio código esquivando naves alienígenas en una pequeña pantalla es la verdadera recompensa de este viaje tecnológico.


Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Carrito de compra