Breakout(Destructor de bloques) en Arduino Uno y OLED 128×32

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)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 (I2C)Mostrar información
Botón2PushMover izquierda/derecha
Resistencia210kΩ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)

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:

  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
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.


Deja un comentario

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

Carrito de compra