El proyecto Breakout en Arduino es un ejercicio excelente para adentrarse en la programación de juegos y el manejo de periféricos en sistemas embebidos. Utiliza la arquitectura simple pero potente del Arduino Uno y la claridad de una pantalla OLED para recrear una experiencia arcade completa.
Este artículo te guiará a través de la selección de componentes, el cableado y la estructura del código, haciendo énfasis en la modularidad y la eficiencia.
🔩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.
- Interrupciones: Para lograr que la base responda instantáneamente al presionar el botón, se utiliza una Interrupción Externa. En el Arduino UNO, los pines digitales 2 y 3 están asignados para estas interrupciones. La interrupción permite que el microcontrolador suspenda temporalmente su tarea actual (el bucle
loop()) para atender la solicitud crítica del botón, garantizando que ninguna pulsación se pierda debido al tiempo de ejecución del código principal.
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 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(I2C)
| 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 (I2C) | Mostrar información |
| Botón | 2 | Push | Mover izquierda/derecha |
| Resistencia | 2 | 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) |
0️⃣Código
El código se divide en secciones claras para la gestión de las librerías, la lógica del juego y la respuesta por interrupción.
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 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 | #include <Arduino.h> // ------------------------------------------------------------- // LIBRERÍAS REQUERIDAS // ------------------------------------------------------------- #include <SPI.h> #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> // ------------------------------------------------------------- // DEFINICIONES DE PINES Y PANTALLA // ------------------------------------------------------------- #define SCREEN_ADDRESS 0x3C #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 32 #define PIN_BTN_IZQ 2 #define PIN_BTN_DER 3 // Inicialización del objeto de la pantalla Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); // ------------------------------------------------------------- // VARIABLES GLOBALES DEL JUEGO // ------------------------------------------------------------- // --- Barrita (Paddle) --- const int PADDLE_W = 20; const int PADDLE_H = 3; const int PADDLE_VEL = 2; int paddle_x; const int paddle_y = SCREEN_HEIGHT - PADDLE_H - 1; // --- Pelota (Ball) --- float ball_x, ball_y; float ball_vx, ball_vy; const float BALL_START_VY = -1.0; // --- Ladrillos (Bricks) --- const int BRICK_ROWS = 3; const int BRICK_COLS = 8; const int BRICK_W = 14; const int BRICK_H = 3; const int BRICK_PADDING = 1; bool bricks[BRICK_ROWS][BRICK_COLS]; int score; int lives; bool game_over; bool game_won; int bricks_remaining; // ------------------------------------------------------------- // DECLARACIÓN DE MÉTODOS (Funciones) // ------------------------------------------------------------- void initGame(); void drawPaddle(); void drawBall(); void drawBricks(); void renderGame(); void handleInput(); void updateBallPosition(); void checkPaddleCollision(); void checkBrickCollision(); void updateGameLogic(); void displayMessage(const char* line1, const char* line2); // ------------------------------------------------------------- // DEFINICIÓN DE MÉTODOS DE INICIALIZACIÓN // ------------------------------------------------------------- /** * @brief Reinicia todas las variables del juego a su estado inicial. */ void initGame() { score = 0; lives = 3; game_over = false; game_won = false; bricks_remaining = BRICK_ROWS * BRICK_COLS; // Posición inicial de la barra paddle_x = (SCREEN_WIDTH / 2) - (PADDLE_W / 2); // Inicializar la pelota ball_x = SCREEN_WIDTH / 2.0; ball_y = (float)paddle_y - 2.0; ball_vx = 1.0; ball_vy = BALL_START_VY; // Inicializar la matriz de ladrillos for (int r = 0; r < BRICK_ROWS; r++) { for (int c = 0; c < BRICK_COLS; c++) { bricks[r][c] = true; } } } // ------------------------------------------------------------- // DEFINICIÓN DE MÉTODOS DE DIBUJO (RENDERIZADO) // ------------------------------------------------------------- void drawPaddle() { display.fillRect(paddle_x, paddle_y, PADDLE_W, PADDLE_H, SSD1306_WHITE); } void drawBall() { // Dibuja la pelota como un cuadrado de 2x2 píxeles display.fillRect((int)ball_x, (int)ball_y, 2, 2, SSD1306_WHITE); } void drawBricks() { for (int r = 0; r < BRICK_ROWS; r++) { for (int c = 0; c < BRICK_COLS; c++) { if (bricks[r][c]) { int x = c * (BRICK_W + BRICK_PADDING) + BRICK_PADDING; int y = r * (BRICK_H + BRICK_PADDING) + BRICK_PADDING; display.fillRect(x, y, BRICK_W, BRICK_H, SSD1306_WHITE); } } } } /** * @brief Borra la pantalla y dibuja todos los elementos del juego. */ void renderGame() { display.clearDisplay(); drawPaddle(); drawBall(); drawBricks(); display.display(); } /** * @brief Muestra un mensaje centrado en la pantalla (para game over/won). */ void displayMessage(const char* line1, const char* line2) { display.clearDisplay(); // Mensaje principal display.setTextSize(2); display.setTextColor(SSD1306_WHITE); display.setCursor(0, 5); display.println(line1); // Mensaje secundario (Puntuación/Vidas) display.setTextSize(1); display.setCursor(10, 25); display.println(line2); display.display(); } // ------------------------------------------------------------- // DEFINICIÓN DE MÉTODOS DE LÓGICA DE JUEGO // ------------------------------------------------------------- /** * @brief Lee la entrada de los botones y mueve la barra. */ void handleInput() { bool btnIzq = digitalRead(PIN_BTN_IZQ) == HIGH; bool btnDer = digitalRead(PIN_BTN_DER) == HIGH; if (btnIzq) { paddle_x -= PADDLE_VEL; } if (btnDer) { paddle_x += PADDLE_VEL; } // Limitar movimiento if (paddle_x < 0) { paddle_x = 0; } if (paddle_x > SCREEN_WIDTH - PADDLE_W) { paddle_x = SCREEN_WIDTH - PADDLE_W; } } /** * @brief Actualiza la posición de la pelota y verifica colisiones con los bordes. */ void updateBallPosition() { // 1. Mover la pelota ball_x += ball_vx; ball_y += ball_vy; // 2. Colisión con los bordes IZQUIERDO o DERECHO if (ball_x < 0 || ball_x > SCREEN_WIDTH - 2) { ball_vx *= -1; // Corrección de posición if (ball_x < 0) ball_x = 0; if (ball_x > SCREEN_WIDTH - 2) ball_x = SCREEN_WIDTH - 2; } // 3. Colisión con el borde SUPERIOR if (ball_y < 0) { ball_vy *= -1; ball_y = 0; } // 4. Colisión con el borde INFERIOR (PERDER VIDA) if (ball_y > SCREEN_HEIGHT) { lives--; if (lives <= 0) { game_over = true; } else { // Reinicio de la pelota después de perder vida paddle_x = (SCREEN_WIDTH / 2) - (PADDLE_W / 2); ball_x = SCREEN_WIDTH / 2.0; ball_y = (float)paddle_y - 2.0; ball_vy = -abs(ball_vy); // Asegura que empiece subiendo // Mostrar mensaje de pérdida de vida char life_status[16]; sprintf(life_status, "VIDAS: %d", lives); displayMessage("PERDISTE UNA VIDA!", life_status); delay(1500); } } } /** * @brief Verifica la colisión de la pelota con la barra y ajusta el rebote. */ void checkPaddleCollision() { // Verifica si la pelota está en la zona vertical y horizontal de la barra if (ball_y + 2 >= paddle_y && ball_y <= paddle_y + PADDLE_H && ball_x + 2 >= paddle_x && ball_x <= paddle_x + PADDLE_W) { // Solo rebotar si la pelota viene bajando (ball_vy > 0) if (ball_vy > 0) { ball_vy *= -1; // Ajuste de ángulo de rebote (mayor velocidad horizontal si golpea los extremos) float hit_point = (ball_x + 1) - (paddle_x + PADDLE_W / 2.0); ball_vx = (hit_point / (PADDLE_W / 2.0)) * 1.5; } } } /** * @brief Verifica la colisión de la pelota con todos los ladrillos. */ void checkBrickCollision() { for (int r = 0; r < BRICK_ROWS; r++) { for (int c = 0; c < BRICK_COLS; c++) { if (bricks[r][c]) { int brick_x = c * (BRICK_W + BRICK_PADDING) + BRICK_PADDING; int brick_y = r * (BRICK_H + BRICK_PADDING) + BRICK_PADDING; // Detección de Colisión AABB if (ball_x + 2 >= brick_x && ball_x <= brick_x + BRICK_W && ball_y + 2 >= brick_y && ball_y <= brick_y + BRICK_H) { // Ladrillo destruido bricks[r][c] = false; score += 10; bricks_remaining--; // Rebote simplificado (cambio de dirección vertical) ball_vy *= -1; if (bricks_remaining == 0) { game_won = true; } // Importante: Salir después de la primera colisión detectada return; } } } } } /** * @brief Contiene toda la lógica de movimiento y colisiones por frame. */ void updateGameLogic() { // Mover la pelota y verificar colisiones con los bordes updateBallPosition(); // Verificar colisión con la barra checkPaddleCollision(); // Verificar colisión con los ladrillos checkBrickCollision(); } // ------------------------------------------------------------- // FUNCIONES ESTÁNDAR DE ARDUINO // ------------------------------------------------------------- void setup() { Serial.begin(9600); // Inicializar comunicación I2C de la pantalla if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { // Si la pantalla falla, detener el programa for(;;); } // Configurar pines de entrada para los botones pinMode(PIN_BTN_IZQ, INPUT); pinMode(PIN_BTN_DER, INPUT); // Inicializar todas las variables y el estado del juego initGame(); // Mostrar mensaje de inicio displayMessage("¡BREAKOUT!", "Presiona cualquier boton"); while(true) { delay(200); if(digitalRead(PIN_BTN_IZQ) == 1 || digitalRead(PIN_BTN_DER) == 1) break; } delay(200); } void loop() { // --- Estados de Fin de Juego --- if (game_over || game_won) { char score_line[30]; sprintf(score_line, "PUNTUACION: %d", score); if (game_over) { displayMessage("GAME OVER", score_line); } else { // game_won displayMessage("¡GANASTE!", score_line); } // Esperar a que el jugador presione ambos botones para reiniciar if (digitalRead(PIN_BTN_IZQ) == HIGH && digitalRead(PIN_BTN_DER) == HIGH) { initGame(); } delay(50); return; } // --- Ciclo Principal del Juego --- // 1. INPUT: Leer botones y mover la barra handleInput(); // 2. UPDATE: Ejecutar toda la lógica del juego (movimiento, colisiones) updateGameLogic(); // 3. RENDER: Dibujar el nuevo frame renderGame(); // Control de velocidad/framerate delay(10); } |
🖌️Diseños

🎬Videos
📑Conclusión
El desarrollo del juego Breakout en la plataforma Arduino Uno con una pantalla OLED 128×32 es un proyecto ejemplar que sintetiza los desafíos y las soluciones del diseño de sistemas embebidos.
El logro central de este proyecto reside en el manejo eficiente de las limitaciones de hardware y software. La pequeña pantalla OLED de 128 x 32 píxeles, aunque minimalista, obligó a una ingeniería de software concisa para maximizar el área de juego. La escasez de memoria SRAM impuso la necesidad de una estructura de código altamente modular y optimizada, utilizando métodos dedicados para cada tarea: handleInput(), updateGameLogic(), y renderGame().
Más allá de la funcionalidad básica, el proyecto demostró la implementación exitosa de física de juego en tiempo real. La gestión de la detección de colisiones AABB y la incorporación de una lógica de rebote dependiente del ángulo elevan el proyecto de una simple demostración a una aplicación interactiva que requiere habilidad y precisión del usuario.
En última instancia, este proyecto de Breakout en Arduino es una prueba de concepto sólida: demuestra que los microcontroladores de consumo pueden ser la base para la creación de experiencias interactivas sofisticadas, sirviendo como un punto de partida perfecto para cualquier desarrollador que aspire a construir sistemas de juego o interfaces gráficos de bajo nivel.

