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) | Protocolo | Notas Importantes |
|---|---|---|
| VCC | Alimentación | Uso de 5V. |
| GND | Tierra | Conexión de referencia. |
| SCL | I2C | Línea de Reloj Serial. |
| SDA | I2C | Línea de Datos Serial. |
🔨Componentes
| Componente | Cantidad | Especificación | Función |
|---|---|---|---|
| Placa Arduino | 1 | Arduino Uno, Nano, etc. | Control de componentes(cerebro) |
| Pantalla OLED | 1 | 128×32 (SSD1306) | Mostrar información |
| Botón | 3 | Push | Mover/Disparar |
| Resistencias | 3 | 10kΩ | Pull-down para el botón |
| Cables de conexión | n | Unir los componentes |
🔌Conexiones
Conexión OLED 128×32
| OLED 128×32(I2C) Pin | Arduino Pin |
|---|---|
| GND | GND |
| VCC | 5V |
| SCL | A5 |
| SDA | A4 |
Conexión del botón(Configuración Pull-Down):
| Arduino Pin | Arduino Pin |
|---|---|
| 5v | Pin 2 (con resistencia de 10kΩ a GND) |
| 5v | Pin 3 (con resistencia de 10kΩ a GND) |
| 5v | Pin 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:
- Adafruit GFX Library (librería gráfica principal)
- 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.

