Juegos con Arduino: Pong, T-Rex, Snake y Simon paso a paso
Cómo hacer juegos con Arduino: Pong y T-Rex en OLED, Snake en matriz LED, Simon, un runner en LCD y un test de reacción, con bucle de juego y récord en EEPROM.
Hacer juegos con Arduino es la forma más divertida de aprender electrónica y programación: con una pantalla OLED, unos pocos pulsadores y un joystick puedes construir tú mismo clásicos como Pong, el T-Rex runner, Snake y el juego de memoria Simon, y retar a tus amigos a batir tu récord. Mientras programas un juego aprendes casi sin darte cuenta los temas más importantes de la programación: temporización, máquinas de estados, sistemas de coordenadas, cálculo de colisiones, números aleatorios y memoria permanente. Todos estos juegos funcionan en un solo Arduino Uno; sus 2 KB de RAM bastan si usas la memoria con cabeza.
En esta guía construimos seis juegos de principio a fin: Pong y el T-Rex runner en una pantalla OLED, Snake en una matriz LED NeoPixel de 8x8 y de 16x16, Simon con cuatro LED y cuatro pulsadores, un juego de carreras en una LCD 16x2 y un test de reacción con precisión de milisegundos. Cada juego incluye lista de componentes, conexiones y código que compila. Primero aprenderás una vez el esqueleto que todos comparten, es decir, el bucle de juego, la detección de colisiones, la puntuación y el récord guardado en la EEPROM, y luego lo reutilizarás en cada juego. Al final también encontrarás versiones para ESP32 y Raspberry Pi Pico.

¿Qué componentes necesita cada juego?
Cuando haces un juego con Arduino, la primera decisión es la pantalla, porque determina qué tipo de juego puedes crear. Con solo unos LED ya puedes hacer Simon o un test de reacción, mientras que una OLED de 128×64 píxeles hace posibles Pong y T-Rex, con gráficos en movimiento y física. La tabla resume los juegos de esta guía de más fácil a más difícil:
| Juego | Pantalla | Entrada | Otros componentes | Dificultad |
|---|---|---|---|---|
| Test de reacción | 1 LED + monitor serie | 1 pulsador | Resistencia de 220 Ω, buzzer opcional | Fácil |
| Juego de memoria Simon | 4 LED de colores | 4 pulsadores | 4 resistencias de 220 Ω, buzzer pasivo | Fácil–media |
| Runner en LCD | LCD 16x2 I2C | 1 pulsador | Buzzer pasivo | Media |
| Pong | OLED SSD1306 de 0,96" | Joystick o potenciómetro de 10 kΩ | Buzzer pasivo | Media |
| T-Rex runner | OLED SSD1306 de 0,96" | 1 pulsador o el botón del joystick | Buzzer pasivo | Media |
| Snake (8x8) | Matriz NeoPixel 8x8 | Joystick | Resistencia de 330 Ω, condensador de 1000 µF | Media–avanzada |
| Snake (16x16) | Matriz NeoPixel 16x16 | Joystick | Fuente externa de 5 V y 3–4 A | Avanzada |






Conexión común: un solo circuito como consola de juegos
Elegimos pines comunes para que todos los juegos excepto Simon funcionen en el mismo circuito. La OLED y la LCD I2C comparten los mismos dos cables (SDA y SCL); el joystick va a las entradas analógicas, el pulsador de acción a D3 y el buzzer a D8. Así, una vez montada esta "consola", cambias de juego simplemente cargando otro código. Puedes ver el mapa completo de pines de tu placa en la herramienta Pinout.
| Componente / pin | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| OLED o LCD I2C: SDA | A4 | GPIO 21 | GP4 |
| OLED o LCD I2C: SCL | A5 | GPIO 22 | GP5 |
| Joystick VRx (eje X) | A0 | GPIO 34 | GP26 |
| Joystick VRy (eje Y) | A1 | GPIO 35 | GP27 |
| Joystick SW (botón) | D2 | GPIO 25 | GP15 |
| Pulsador de acción (la otra pata a GND) | D3 | GPIO 26 | GP14 |
| Buzzer pasivo (+) | D8 | GPIO 27 | GP16 |
| NeoPixel DIN (a través de 330 Ω) | D6 | GPIO 13 | GP22 |
¿Cómo funciona el bucle de juego (game loop)?
Todo videojuego se basa en un bucle de cuatro pasos que se repite decenas de veces por segundo: leer la entrada (pulsadores, joystick), actualizar el mundo (posición, velocidad, puntos), comprobar colisiones y dibujar la pantalla. Cada vuelta del bucle se llama fotograma (frame). Treinta fotogramas por segundo (30 FPS) bastan para que se vea fluido, lo que significa que cada fotograma dura unos 33 ms. La función loop() de Arduino ya es un bucle, pero gira muy rápido y de forma irregular, así que hay que dividirla en fotogramas fijos con millis().
Aquí es fundamental no usar delay(). Si escribes delay(33), el Arduino no hace nada durante esos 33 ms, puede perderse una pulsación corta y la velocidad del juego cambia cada vez que dibujar tarda más o menos. Con millis(), loop() lee la entrada en cada vuelta pero solo avanza el juego cuando toca un nuevo fotograma. La diferencia entre ambos métodos se explica en detalle en Arduino millis vs delay. Este esqueleto es la base de todos los juegos de la guía:
1// Oyun döngüsü iskeleti / Game loop skeleton
2enum State { MENU, PLAYING, GAME_OVER }; // oyunun durumları / game states
3State state = MENU;
4
5const byte BUTTON_PIN = 3; // aksiyon butonu / action button
6const unsigned long FRAME_MS = 33; // ~30 kare/sn / ~30 frames per second
7unsigned long lastFrame = 0;
8unsigned int score = 0;
9
10// Sıçramasız basış: yalnız basıldığı an true / bounce-free: true only at the moment of a press
11bool buttonPressed() {
12 static bool stable = HIGH, lastRead = HIGH;
13 static unsigned long changedAt = 0;
14 bool r = digitalRead(BUTTON_PIN);
15 if (r != lastRead) { lastRead = r; changedAt = millis(); }
16 if (millis() - changedAt > 15 && r != stable) { // 15 ms sabit kaldı mı? / stable for 15 ms?
17 stable = r;
18 return stable == LOW;
19 }
20 return false;
21}
22
23void setup() {
24 pinMode(BUTTON_PIN, INPUT_PULLUP); // basınca LOW / LOW when pressed
25 Serial.begin(9600);
26}
27
28void loop() {
29 bool pressed = buttonPressed(); // 1) giriş: her turda oku / input: read on every pass
30 if (pressed && state != PLAYING) { // menüden ya da oyun sonundan başlat / start from menu or game over
31 score = 0;
32 state = PLAYING;
33 }
34 if (millis() - lastFrame < FRAME_MS) return; // kare zamanı gelmedi / not time for a frame yet
35 lastFrame = millis();
36
37 if (state == PLAYING) {
38 score++; // 2) güncelle: konum, hız, puan / update: position, speed, score
39 if (score >= 300) state = GAME_OVER; // 3) çarpışma ya da bitiş kontrolü / collision or end check
40 Serial.println(score); // 4) çiz: burada ekrana çizilir / draw: the screen is drawn here
41 }
42}La segunda idea clave del código es la máquina de estados. En cada momento el juego está en uno de tres estados: MENU, PLAYING o GAME_OVER. Cada estado reacciona de forma distinta al mismo pulsador: en el menú inicia la partida, durante el juego hace saltar al personaje y tras el game over abre una nueva ronda. Dividir el código así elimina los if anidados y facilita añadir un estado nuevo, como PAUSE. Tratamos el tema con más detalle en máquina de estados con Arduino.
Leer pulsadores y un joystick
Conectar los pulsadores con INPUT_PULLUP es el método más limpio: no hace falta resistencia externa, el pin lee HIGH en reposo y LOW al pulsar. Sin embargo, un pulsador mecánico rebota durante unos milisegundos al pulsarlo o soltarlo, y el Arduino lo interpreta como varias pulsaciones seguidas. En un juego eso significa saltar dos veces con una sola pulsación o que Simon cuente un color equivocado. La función buttonPressed() del esqueleto solo acepta un estado nuevo cuando el pin se ha mantenido igual durante 15 ms, y devuelve true una única vez, en el momento de pulsar. Para el circuito y otros métodos, consulta cómo conectar un pulsador a Arduino.
Un joystick está formado por dos potenciómetros y un botón. En el Uno cada eje da valores de 0 a 1023 y, con la palanca suelta, el centro está en torno a 512, aunque varía de un módulo a otro entre 490 y 530 aproximadamente. Por eso los juegos de cuadrícula usan umbrales en lugar de valores exactos: por debajo de 300 significa izquierda o arriba, por encima de 700 derecha o abajo. La amplia zona intermedia (zona muerta) evita que el personaje se desplace solo cuando sueltas la palanca. En Pong, en cambio, llevamos el valor analógico directamente a la pala: map() convierte el rango 0–1023 en el recorrido de 0 a 48 píxeles de la pala. La calibración y la tabla de direcciones están en la guía del módulo joystick.
¿Cómo se detectan las colisiones?
La detección de colisiones pregunta si dos objetos están en el mismo lugar al mismo tiempo. En los juegos con pantalla, el método más habitual es la prueba AABB (caja delimitadora alineada con los ejes): tratas cada objeto como un rectángulo. Dos rectángulos se solapan solo si se cumplen a la vez cuatro condiciones: el borde izquierdo de A está a la izquierda del borde derecho de B, el borde derecho de A está a la derecha del borde izquierdo de B, y lo mismo ocurre en el eje vertical. En código es una sola línea: ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by. En los juegos de cuadrícula (Snake, el runner en LCD) es aún más sencillo: si dos celdas tienen las mismas coordenadas, hay colisión.
| Juego | ¿Qué choca con qué? | Método |
|---|---|---|
| Pong | Pelota con paredes y palas | AABB + invertir la velocidad |
| T-Rex | Dinosaurio con los cactus | AABB (con una caja algo más pequeña) |
| Snake | Cabeza con paredes, cuerpo y comida | Igualdad de celdas de la cuadrícula |
| Runner en LCD | Corredor con un cactus | Misma columna y misma fila |
| Simon | Color pulsado con el color de la secuencia | Comparación de elementos del array |
| Test de reacción | Momento de la pulsación con el de la señal | Comparación de tiempos (salida en falso) |
Puntuación y récord guardado en la EEPROM
La puntuación vive en una variable de la RAM y desaparece en cuanto desenchufas el Arduino. Para que el récord sea permanente se usa la EEPROM: el ATmega328P del Arduino Uno tiene 1024 bytes de EEPROM que conservan su contenido sin alimentación. Para un récord bastan dos bytes de tipo uint16_t (0–65535). El único punto débil de la EEPROM es su vida útil: cada celda admite unas 100.000 escrituras. Parece mucho, pero si escribes la puntuación en cada fotograma, 30 veces por segundo, la celda se desgasta en unos 55 minutos. Lo correcto es escribir solo al terminar la partida y solo si se ha batido el récord; aunque batas 10 récords al día, la celda durará 27 años.
1#include <EEPROM.h>
2
3const int HISCORE_ADDR = 0; // rekor EEPROM'un 0-1. baytında / high score in EEPROM bytes 0-1
4uint16_t hiScore = 0;
5
6uint16_t loadHiScore() {
7 uint16_t v;
8 EEPROM.get(HISCORE_ADDR, v); // 2 bayt oku / read 2 bytes
9 if (v == 0xFFFF) v = 0; // hiç yazılmamış EEPROM 0xFF okunur / blank EEPROM reads 0xFF
10 return v;
11}
12
13void saveIfRecord(uint16_t score) {
14 if (score > hiScore) { // yalnız rekor kırılınca yaz / write only on a new record
15 hiScore = score;
16 EEPROM.put(HISCORE_ADDR, hiScore); // put() yalnız değişen baytları yazar / put() writes only changed bytes
17 }
18}
19
20void setup() {
21 Serial.begin(9600);
22 hiScore = loadHiScore();
23 Serial.print(F("Rekor / High score: "));
24 Serial.println(hiScore);
25 saveIfRecord(120); // örnek: oyun 120 puanla bitti / example: a game ended with 120 points
26}
27
28void loop() {}El código tiene dos detalles finos. Primero, cada byte de una EEPROM que nunca se ha escrito vale 0xFF, por eso en el primer arranque el récord aparece como 65535 y la comprobación de 0xFFFF lo convierte en 0. En el test de reacción ocurre lo contrario: 0xFFFF significa "todavía no hay récord", porque allí un tiempo más corto es mejor. Segundo, EEPROM.put() lee cada byte antes de escribir y solo escribe los que han cambiado, lo que alarga todavía más la vida útil. Para el resto de comandos, consulta cómo usar la EEPROM de Arduino. Para que puedas cargar todos los juegos de esta guía en el mismo Uno uno tras otro, cada juego tiene su propia dirección:
| Juego | Dirección EEPROM | Valor guardado |
|---|---|---|
| T-Rex runner | 0–1 | Puntuación más alta |
| Snake | 2–3 | Mayor número de comidas |
| Runner en LCD | 4–5 | Puntuación más alta |
| Test de reacción | 6–7 | Tiempo más corto (ms) |
| Simon | 8–9 | Nivel más alto alcanzado |
Pong: pantalla OLED y joystick
Pong es el juego de tenis de mesa de 1972 con el que empezó la era de los videojuegos. En nuestra versión controlas la pala izquierda con el eje Y del joystick (o con un potenciómetro en A1) y el Arduino juega con la pala derecha. Componentes: un Arduino Uno, una OLED SSD1306 I2C de 0,96 pulgadas, un joystick y un buzzer pasivo. Al instalar la librería Adafruit SSD1306 desde el gestor de librerías, instala también su dependencia, Adafruit GFX. La conexión de la pantalla y sus comandos de dibujo se explican en detalle en pantalla OLED SSD1306.
1#include <Wire.h>
2#include <Adafruit_GFX.h>
3#include <Adafruit_SSD1306.h>
4
5Adafruit_SSD1306 display(128, 64, &Wire, -1);
6
7const byte JOY_Y_PIN = A1; // joystick VRy ya da potansiyometre / joystick VRy or potentiometer
8const byte BUZZER_PIN = 8; // pasif buzzer / passive buzzer
9const byte PADDLE_H = 16, PADDLE_W = 3;
10const byte WIN_SCORE = 5; // 5 sayı alan kazanır / first to 5 wins
11const float MAX_SPEED = 2.8; // raketten kalın olmasın / keep below paddle thickness + ball size
12const unsigned long FRAME_MS = 30; // ~33 kare/sn / ~33 frames per second
13
14float ballX, ballY, velX, velY;
15float playerY = 24, cpuY = 24;
16byte playerScore = 0, cpuScore = 0;
17unsigned long lastFrame = 0;
18
19void serve(int dir) { // topu ortadan at / serve from the center
20 ballX = 63; ballY = 31;
21 velX = 1.8 * dir;
22 velY = random(-10, 11) / 10.0; // -1,0 ile 1,0 arası / between -1.0 and 1.0
23}
24
25// Topun rakete çarptığı nokta yeni açıyı belirler / where the ball hits the paddle sets the new angle
26void hitPaddle(float paddleY) {
27 float offset = (ballY + 1 - (paddleY + PADDLE_H / 2.0)) / (PADDLE_H / 2.0); // -1 üst, +1 alt / -1 top, +1 bottom
28 velX = constrain(-velX * 1.05, -MAX_SPEED, MAX_SPEED); // yön değiştir, %5 hızlan / reverse, 5% faster
29 velY = offset * 1.5;
30 tone(BUZZER_PIN, 880, 15);
31}
32
33void setup() {
34 Serial.begin(9600);
35 if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
36 Serial.println(F("SSD1306 allocation failed"));
37 for (;;); // ekran yoksa dur / halt without a display
38 }
39 display.setTextColor(SSD1306_WHITE);
40 randomSeed(analogRead(A3)); // boştaki analog pin / a floating analog pin
41 serve(1);
42}
43
44void loop() {
45 if (millis() - lastFrame < FRAME_MS) return;
46 lastFrame = millis();
47
48 // 1) Giriş: oyuncu raketi / Input: player paddle
49 playerY = map(analogRead(JOY_Y_PIN), 0, 1023, 0, 64 - PADDLE_H);
50
51 // 2) Basit yapay zekâ: CPU topu sınırlı hızla izler / Simple AI: the CPU follows the ball at limited speed
52 float target = ballY - PADDLE_H / 2.0;
53 cpuY += constrain(target - cpuY, -1.2, 1.2);
54 cpuY = constrain(cpuY, 0, 64 - PADDLE_H);
55
56 // 3) Topu hareket ettir / Move the ball
57 ballX += velX;
58 ballY += velY;
59
60 // 4) Çarpışmalar / Collisions
61 if (ballY <= 0 || ballY >= 62) { // üst ve alt duvar / top and bottom walls
62 velY = -velY;
63 ballY = constrain(ballY, 0, 62);
64 }
65 if (velX < 0 && ballX <= 4 && ballX > 0 && ballY + 2 >= playerY && ballY <= playerY + PADDLE_H) {
66 ballX = 4;
67 hitPaddle(playerY);
68 }
69 if (velX > 0 && ballX >= 122 && ballX < 126 && ballY + 2 >= cpuY && ballY <= cpuY + PADDLE_H) {
70 ballX = 122;
71 hitPaddle(cpuY);
72 }
73
74 // 5) Sayı / Point
75 if (ballX <= 0) { cpuScore++; tone(BUZZER_PIN, 220, 200); serve(1); }
76 if (ballX >= 126) { playerScore++; tone(BUZZER_PIN, 660, 200); serve(-1); }
77
78 // 6) Çiz / Draw
79 display.clearDisplay();
80 for (byte y = 0; y < 64; y += 6) display.drawFastVLine(63, y, 3, SSD1306_WHITE); // orta çizgi / center line
81 display.fillRect(1, (int)playerY, PADDLE_W, PADDLE_H, SSD1306_WHITE);
82 display.fillRect(124, (int)cpuY, PADDLE_W, PADDLE_H, SSD1306_WHITE);
83 display.fillRect((int)ballX, (int)ballY, 2, 2, SSD1306_WHITE);
84 display.setTextSize(1);
85 display.setCursor(52, 0); display.print(playerScore);
86 display.setCursor(70, 0); display.print(cpuScore);
87 display.display();
88
89 // 7) Oyun bitti mi? / Game over?
90 if (playerScore >= WIN_SCORE || cpuScore >= WIN_SCORE) {
91 display.setTextSize(2);
92 if (playerScore >= WIN_SCORE) { display.setCursor(22, 24); display.print(F("YOU WIN")); }
93 else { display.setCursor(10, 24); display.print(F("GAME OVER")); }
94 display.display();
95 delay(3000);
96 playerScore = cpuScore = 0;
97 serve(1);
98 }
99}Lo que hace divertido a Pong es la función hitPaddle(). Si la pelota golpea el centro de la pala vuelve casi recta; si golpea el borde, rebota con un ángulo cerrado y pone en apuros al rival. Para ello escalamos la distancia de la pelota al centro de la pala a un valor entre -1 y +1 y la convertimos en velocidad vertical. Con cada golpe la pelota acelera un 5 % y queda limitada por MAX_SPEED. La pala del Arduino se mueve como máximo 1,2 píxeles por fotograma; si le das a la pelota un ángulo cerrado no llega a tiempo y te llevas el punto. Para hacer el juego más difícil sube ese valor a 1,5; para dos jugadores, borra las líneas de la CPU y controla la segunda pala con un potenciómetro en A2.
¿Por qué 30 ms por fotograma? El Uno envía el búfer de imagen de 1024 bytes a la OLED por I2C a 400 kHz en unos 25 ms, así que la pantalla no puede mostrar más de 35–40 fotogramas por segundo. Mantener el tiempo de fotograma justo por encima de ese tiempo de envío hace que la velocidad del juego sea constante. Y que no te engañe el aviso del compilador "Global variables use 28%": el búfer de la SSD1306 se reserva en tiempo de ejecución y el uso real de RAM sube a cerca del 78 %. Si la memoria no alcanza verás el error SSD1306 allocation failed; hemos reunido las soluciones en Arduino sin memoria.
T-Rex runner: física del salto y récord
Llevamos a la pantalla OLED el juego del dinosaurio que muestra Chrome cuando no hay internet. El dinosaurio corre en su sitio, los cactus pasan de derecha a izquierda y tú los saltas pulsando el botón. Para que el salto parezca realista usamos una física sencilla: al pulsar, la velocidad vertical pasa a -4,2 píxeles por fotograma, en cada fotograma la gravedad le suma 0,3 y la posición cambia según la velocidad. Así el dinosaurio sube frenando, se queda un instante en lo alto y cae cada vez más rápido; sube unos 27 píxeles y permanece 27 fotogramas en el aire, alrededor de 0,8 segundos. Los componentes son los mismos que en Pong; para saltar usas el pulsador de D3 o el botón del joystick.
1#include <Wire.h>
2#include <EEPROM.h>
3#include <Adafruit_GFX.h>
4#include <Adafruit_SSD1306.h>
5
6Adafruit_SSD1306 display(128, 64, &Wire, -1);
7
8const byte JUMP_PIN = 3; // aksiyon butonu / action button
9const byte JOY_SW_PIN = 2; // joystick butonu da zıplatır / the joystick button jumps too
10const byte BUZZER_PIN = 8;
11const int HISCORE_ADDR = 0; // EEPROM 0-1. bayt / EEPROM bytes 0-1
12const byte GROUND_Y = 56; // zemin çizgisi / ground line
13const byte DINO_X = 10, DINO_W = 14, DINO_H = 14;
14const float JUMP_V = -4.2; // zıplama hızı (piksel/kare) / jump speed (px/frame)
15const float GRAVITY = 0.3; // yer çekimi (piksel/kare²) / gravity (px/frame²)
16const unsigned long FRAME_MS = 30; // ~33 kare/sn / ~33 frames per second
17
18enum State { MENU, PLAYING, GAME_OVER };
19State state = MENU;
20
21struct Cactus { float x; byte w, h; };
22Cactus cacti[2];
23
24float dinoY, velY, speed;
25uint16_t score = 0, hiScore = 0;
26unsigned long lastFrame = 0;
27
28bool jumpHeld() {
29 return digitalRead(JUMP_PIN) == LOW || digitalRead(JOY_SW_PIN) == LOW;
30}
31
32uint16_t loadHiScore() {
33 uint16_t v;
34 EEPROM.get(HISCORE_ADDR, v);
35 return (v == 0xFFFF) ? 0 : v; // boş EEPROM 0xFFFF okunur / blank EEPROM reads 0xFFFF
36}
37
38void placeCactus(byte i, float startX) {
39 cacti[i].x = startX;
40 cacti[i].w = random(4, 9); // 4-8 piksel genişlik / 4-8 px wide
41 cacti[i].h = random(8, 17); // 8-16 piksel yükseklik / 8-16 px tall
42}
43
44void resetGame() {
45 dinoY = GROUND_Y - DINO_H;
46 velY = 0;
47 speed = 2.0;
48 score = 0;
49 placeCactus(0, 140);
50 placeCactus(1, 140 + random(70, 140));
51}
52
53// İki dikdörtgen üst üste biniyor mu? (AABB) / Do two rectangles overlap? (AABB)
54bool overlap(int ax, int ay, int aw, int ah, int bx, int by, int bw, int bh) {
55 return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
56}
57
58void drawDino(int x, int y, bool legA) {
59 display.fillRect(x + 6, y, 8, 5, SSD1306_WHITE); // kafa / head
60 display.drawPixel(x + 9, y + 1, SSD1306_BLACK); // göz / eye
61 display.fillRect(x, y + 5, 10, 6, SSD1306_WHITE); // gövde / body
62 display.fillRect(x + 2, y + 11, 2, legA ? 3 : 1, SSD1306_WHITE); // sol bacak / left leg
63 display.fillRect(x + 6, y + 11, 2, legA ? 1 : 3, SSD1306_WHITE); // sağ bacak / right leg
64}
65
66void drawScene() {
67 display.clearDisplay();
68 display.drawFastHLine(0, GROUND_Y, 128, SSD1306_WHITE);
69 bool onGround = dinoY >= GROUND_Y - DINO_H;
70 drawDino(DINO_X, (int)dinoY, onGround ? (score / 4) % 2 : true); // koşma animasyonu / running animation
71 for (byte i = 0; i < 2; i++) {
72 display.fillRect((int)cacti[i].x, GROUND_Y - cacti[i].h, cacti[i].w, cacti[i].h, SSD1306_WHITE);
73 }
74 display.setTextSize(1);
75 display.setCursor(0, 0); display.print(score);
76 display.setCursor(80, 0); display.print(F("HI ")); display.print(hiScore);
77}
78
79void showMessage(const __FlashStringHelper *msg) {
80 display.fillRect(14, 18, 100, 28, SSD1306_BLACK);
81 display.drawRect(14, 18, 100, 28, SSD1306_WHITE);
82 display.setCursor(64 - strlen_P((const char *)msg) * 3, 28); // ortala (karakter 6 piksel) / center (6 px per char)
83 display.print(msg);
84 display.display();
85}
86
87void gameOver() {
88 tone(BUZZER_PIN, 150, 300);
89 if (score > hiScore) { // yeni rekor: yalnız şimdi yaz / new record: write only now
90 hiScore = score;
91 EEPROM.put(HISCORE_ADDR, hiScore);
92 }
93 drawScene();
94 showMessage(F("GAME OVER"));
95 delay(800); // basılı buton hemen yeniden başlatmasın / a held button must not restart at once
96 state = GAME_OVER;
97}
98
99void setup() {
100 pinMode(JUMP_PIN, INPUT_PULLUP);
101 pinMode(JOY_SW_PIN, INPUT_PULLUP);
102 Serial.begin(9600);
103 if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
104 Serial.println(F("SSD1306 allocation failed"));
105 for (;;);
106 }
107 display.setTextColor(SSD1306_WHITE);
108 randomSeed(analogRead(A3));
109 hiScore = loadHiScore();
110 if (digitalRead(JUMP_PIN) == LOW) { // açılışta basılıysa rekoru sıfırla / held at power-up: reset the record
111 hiScore = 0;
112 EEPROM.put(HISCORE_ADDR, hiScore);
113 }
114 resetGame();
115 drawScene();
116 showMessage(F("PRESS START"));
117}
118
119void loop() {
120 if (millis() - lastFrame < FRAME_MS) return;
121 lastFrame = millis();
122
123 if (state != PLAYING) { // menü ya da oyun sonu: basınca başla / menu or game over: press to start
124 if (jumpHeld()) {
125 while (jumpHeld()); // bırakılmasını bekle / wait for release
126 resetGame();
127 state = PLAYING;
128 }
129 return;
130 }
131
132 // 1) Giriş ve fizik / Input and physics
133 bool onGround = dinoY >= GROUND_Y - DINO_H;
134 if (onGround && jumpHeld()) {
135 velY = JUMP_V;
136 tone(BUZZER_PIN, 1200, 20);
137 }
138 velY += GRAVITY; // her karede hız artar / velocity grows every frame
139 dinoY += velY;
140 if (dinoY > GROUND_Y - DINO_H) { dinoY = GROUND_Y - DINO_H; velY = 0; } // yere indi / landed
141
142 // 2) Engeller ve çarpışma / Obstacles and collision
143 for (byte i = 0; i < 2; i++) {
144 cacti[i].x -= speed;
145 if (cacti[i].x < -(float)cacti[i].w) { // ekrandan çıktı / left the screen
146 float otherX = cacti[1 - i].x;
147 placeCactus(i, max(128.0, otherX + random(70, 140)));
148 }
149 // biraz küçük çarpışma kutusu daha adil hissettirir / a slightly smaller hitbox feels fairer
150 if (overlap(DINO_X + 2, (int)dinoY + 1, DINO_W - 4, DINO_H - 2,
151 (int)cacti[i].x, GROUND_Y - cacti[i].h, cacti[i].w, cacti[i].h)) {
152 gameOver();
153 return;
154 }
155 }
156
157 // 3) Puan ve zorluk / Score and difficulty
158 score++;
159 if (score % 200 == 0 && speed < 5.0) speed += 0.25; // yaklaşık 6 sn'de bir hızlan / speed up every ~6 s
160
161 // 4) Çiz / Draw
162 drawScene();
163 display.display();
164}| Ajuste | Valor | Efecto |
|---|---|---|
| JUMP_V | -4.2 | Fuerza del salto; con -5 el dinosaurio sube unos 40 píxeles, casi hasta el borde superior |
| GRAVITY | 0.3 | Un valor mayor hace el salto más corto y brusco |
| speed | 2,0 → 5,0 | Velocidad de los cactus; aumenta 0,25 cada 200 fotogramas |
| random(70, 140) | píxeles | Distancia entre dos cactus; valores menores hacen el juego más difícil |
| Caja de colisión | 10×12 píxeles | Más pequeña que el dinosaurio dibujado de 14×14, así los roces por poco no cuentan como choque |
El récord se escribe en la EEPROM en el momento en que termina la partida, y solo si se ha batido; aparece en la esquina superior derecha como HI. Si mantienes pulsado el botón de D3 al encender la placa, el récord se borra, algo útil para empezar un torneo nuevo con tus amigos. La altura y el ancho de los cactus salen de random(). Para que cada partida empiece distinta, la línea randomSeed(analogRead(A3)) usa como semilla el ruido de un pin analógico sin conectar.
Snake en una matriz LED (8x8 y 16x16)
El juego de la serpiente que hicieron famoso los antiguos Nokia se juega mejor en una matriz LED, porque cada LED es una celda del juego. Usamos una matriz de color WS2812B NeoPixel: la versión 8x8 tiene 64 píxeles y la de 16x16, 256, todos controlados con un único cable de datos. El cuerpo de la serpiente se guarda en dos arrays (snakeX y snakeY) y el elemento 0 es la cabeza. En cada paso el cuerpo avanza una celda y la nueva posición se escribe en la cabeza; cuando come, la cola no se borra, así que crece una celda. Para compilar instala la librería Adafruit NeoPixel; los detalles de conexión están en la guía WS2812B NeoPixel.
1#include <Adafruit_NeoPixel.h>
2#include <EEPROM.h>
3
4const byte W = 8, H = 8; // 16x16 matris için 16 yap / set to 16 for a 16x16 matrix
5const bool SERPENTINE = false; // zikzak dizilimli panelde true / true for zigzag-wired panels
6const byte DATA_PIN = 6; // DIN, 330 Ω üzerinden / DIN through 330 Ω
7const byte JOY_X = A0, JOY_Y = A1, JOY_SW = 2;
8const byte BUZZER_PIN = 8;
9const int HISCORE_ADDR = 2; // EEPROM 2-3. bayt / EEPROM bytes 2-3
10
11Adafruit_NeoPixel matrix(W * H, DATA_PIN, NEO_GRB + NEO_KHZ800);
12
13byte snakeX[W * H], snakeY[W * H]; // [0] baş, sonrası gövde / [0] head, then the body
14int snakeLen;
15int8_t dirX, dirY, nextDX, nextDY;
16byte foodX, foodY;
17unsigned int stepMs;
18unsigned long lastStep = 0;
19uint16_t hiScore;
20bool alive = false;
21
22// (x, y) → LED sırası / (x, y) → LED index
23uint16_t xyToIndex(byte x, byte y) {
24 if (SERPENTINE && (y & 1)) return y * W + (W - 1 - x); // tek satırlar ters yönde / odd rows run backwards
25 return y * W + x;
26}
27
28bool onSnake(byte x, byte y) {
29 for (int i = 0; i < snakeLen; i++) {
30 if (snakeX[i] == x && snakeY[i] == y) return true;
31 }
32 return false;
33}
34
35void placeFood() {
36 do { // yem yılanın üstüne düşmesin / food must not land on the snake
37 foodX = random(W);
38 foodY = random(H);
39 } while (onSnake(foodX, foodY));
40}
41
42void resetGame() {
43 snakeLen = 3;
44 for (int i = 0; i < snakeLen; i++) { snakeX[i] = 3 - i; snakeY[i] = H / 2; }
45 dirX = nextDX = 1; // sağa doğru başla / start moving right
46 dirY = nextDY = 0;
47 stepMs = 300;
48 placeFood();
49 alive = true;
50}
51
52void readJoystick() {
53 int x = analogRead(JOY_X), y = analogRead(JOY_Y);
54 // Geri dönüş yasak: kontrol şimdiki yöne göre / no reversing: check against the current direction
55 if (x < 300 && dirX != 1) { nextDX = -1; nextDY = 0; }
56 else if (x > 700 && dirX != -1) { nextDX = 1; nextDY = 0; }
57 else if (y < 300 && dirY != 1) { nextDX = 0; nextDY = -1; }
58 else if (y > 700 && dirY != -1) { nextDX = 0; nextDY = 1; }
59}
60
61void draw() {
62 matrix.clear();
63 matrix.setPixelColor(xyToIndex(foodX, foodY), matrix.Color(255, 0, 0)); // yem kırmızı / food red
64 for (int i = snakeLen - 1; i >= 0; i--) {
65 uint32_t c = (i == 0) ? matrix.Color(255, 200, 0) : matrix.Color(0, 160, 0); // baş sarı / head yellow
66 matrix.setPixelColor(xyToIndex(snakeX[i], snakeY[i]), c);
67 }
68 matrix.show();
69}
70
71void gameOver() {
72 alive = false;
73 uint16_t score = snakeLen - 3; // yenen yem sayısı / food eaten
74 tone(BUZZER_PIN, 150, 400);
75 if (score > hiScore) { hiScore = score; EEPROM.put(HISCORE_ADDR, hiScore); }
76 for (byte k = 0; k < 3; k++) { // kırmızı yanıp sön / flash red
77 matrix.fill(matrix.Color(80, 0, 0)); matrix.show(); delay(200);
78 matrix.clear(); matrix.show(); delay(200);
79 }
80 for (uint16_t i = 0; i < score && i < W * H; i++) matrix.setPixelColor(i, matrix.Color(0, 0, 120)); // puan kadar LED / one LED per point
81 matrix.show();
82 Serial.print(F("Skor / Score: ")); Serial.print(score);
83 Serial.print(F(" Rekor / Best: ")); Serial.println(hiScore);
84}
85
86void step() {
87 dirX = nextDX; dirY = nextDY;
88 int nx = snakeX[0] + dirX, ny = snakeY[0] + dirY;
89 if (nx < 0 || nx >= W || ny < 0 || ny >= H) { gameOver(); return; } // duvar / wall
90
91 bool eat = (nx == foodX && ny == foodY);
92 // Yem yoksa kuyruk ucu boşalacağı için ona çarpmak sayılmaz / without food the tail tip moves away, so it is not a hit
93 int checkLen = eat ? snakeLen : snakeLen - 1;
94 for (int i = 0; i < checkLen; i++) {
95 if (snakeX[i] == nx && snakeY[i] == ny) { gameOver(); return; } // kendine çarptı / hit itself
96 }
97
98 if (eat) snakeLen++;
99 for (int i = snakeLen - 1; i > 0; i--) { // gövdeyi bir hücre kaydır / shift the body one cell
100 snakeX[i] = snakeX[i - 1];
101 snakeY[i] = snakeY[i - 1];
102 }
103 snakeX[0] = nx; snakeY[0] = ny;
104
105 if (eat) {
106 tone(BUZZER_PIN, 1000, 40);
107 if (stepMs > 120) stepMs -= 10; // her yemde hızlan / faster after every food
108 if (snakeLen == W * H) { gameOver(); return; } // tahta doldu: kazandın / board full: you win
109 placeFood();
110 }
111}
112
113void setup() {
114 pinMode(JOY_SW, INPUT_PULLUP);
115 Serial.begin(9600);
116 matrix.begin();
117 matrix.setBrightness(30); // düşük parlaklık = düşük akım / low brightness = low current
118 randomSeed(analogRead(A3));
119 EEPROM.get(HISCORE_ADDR, hiScore);
120 if (hiScore == 0xFFFF) hiScore = 0;
121 resetGame();
122 draw();
123}
124
125void loop() {
126 if (!alive) { // oyun bitti: joystick butonuyla yeniden / game over: restart with the joystick button
127 if (digitalRead(JOY_SW) == LOW) { resetGame(); draw(); delay(300); }
128 return;
129 }
130 readJoystick(); // her turda oku, hiçbir hareketi kaçırma / read on every pass, never miss a move
131 if (millis() - lastStep >= stepMs) {
132 lastStep = millis();
133 step();
134 if (alive) draw();
135 }
136}El código tiene tres reglas clave. Prohibido dar marcha atrás: si una serpiente que va a la derecha gira de golpe a la izquierda, choca con su propio cuello; readJoystick() compara la nueva dirección con la dirección actual de la serpiente y no con la pendiente, así que ni dos movimientos rápidos pueden darle la vuelta. Excepción de la cola: en un paso sin comida la punta de la cola se libera, de modo que meter la cabeza en esa celda no es una colisión. Colocar la comida: placeFood() vuelve a intentarlo hasta que la comida no caiga sobre la serpiente. Cada comida acorta el paso 10 ms (de 300 a 120 ms); al terminar la partida se enciende en azul un LED por cada comida y el récord se guarda en la EEPROM.
También importa cómo están conectados los LED de tu matriz. En la mayoría de placas rígidas de 8x8 los LED van fila a fila en el mismo sentido (SERPENTINE = false). Los paneles flexibles de 16x16 suelen ir en zigzag: una fila va de izquierda a derecha y la siguiente de derecha a izquierda. Si la serpiente salta en diagonal o una fila de cada dos va al revés, pon SERPENTINE a true; si las direcciones salen invertidas, gira el joystick o intercambia los umbrales 300 y 700. Para 16x16 basta con poner W y H a 16; entonces los arrays de la serpiente ocupan 512 bytes, el búfer NeoPixel 768 bytes y se llena cerca del 75 % de la RAM del Uno.

Juego de memoria Simon: 4 LED y 4 pulsadores
Simon es el legendario juego de memoria de 1978: en cada ronda el aparato añade un color a la secuencia y tú repites la secuencia completa en el mismo orden. Componentes: cuatro LED de colores (verde, rojo, amarillo, azul), una resistencia de 220 Ω para cada uno, cuatro pulsadores y un buzzer pasivo. Conecta los LED a D9–D12, los pulsadores a D2–D5 y el buzzer a D8; este juego es un circuito aparte de la consola común. Puedes elegir la resistencia adecuada para tus LED con la calculadora de resistencia para LED. Los cuatro sonidos del Simon original son notas de un acorde de la mayor; el código usa las mismas notas (330, 440, 554 y 659 Hz), por eso suena bien incluso cuando pulsas rápido.
1#include <EEPROM.h>
2
3const byte LED_PINS[4] = {9, 10, 11, 12}; // yeşil, kırmızı, sarı, mavi / green, red, yellow, blue
4const byte BUTTON_PINS[4] = {2, 3, 4, 5}; // LED'lerle aynı sırada / same order as the LEDs
5const int TONES[4] = {330, 440, 554, 659}; // E4, A4, C#5, E5: A majör akoru / A major chord
6const byte BUZZER_PIN = 8;
7const int HISCORE_ADDR = 8; // EEPROM 8-9. bayt / EEPROM bytes 8-9
8const byte MAX_LEVEL = 50;
9
10byte sequence[MAX_LEVEL];
11byte level = 0;
12uint16_t hiScore;
13
14void playColor(byte c, int ms) {
15 digitalWrite(LED_PINS[c], HIGH);
16 tone(BUZZER_PIN, TONES[c]);
17 delay(ms);
18 digitalWrite(LED_PINS[c], LOW);
19 noTone(BUZZER_PIN);
20}
21
22// Basılan rengi döndürür; 3 sn içinde basılmazsa -1 / returns the pressed colour; -1 after 3 s
23int waitForButton() {
24 unsigned long start = millis();
25 while (millis() - start < 3000) {
26 for (byte i = 0; i < 4; i++) {
27 if (digitalRead(BUTTON_PINS[i]) == LOW) {
28 playColor(i, 200); // basılan rengi göster / show the pressed colour
29 while (digitalRead(BUTTON_PINS[i]) == LOW); // bırakılmasını bekle / wait for release
30 delay(30); // sıçramayı atla / skip the bounce
31 return i;
32 }
33 }
34 }
35 return -1;
36}
37
38// LED'ler döner; ilk basış oyunu başlatır ve rastgeleliği tohumlar
39// LEDs chase until the first press, which starts the game and seeds the randomness
40void waitForStart() {
41 byte i = 0;
42 while (true) {
43 digitalWrite(LED_PINS[i], HIGH);
44 for (byte t = 0; t < 20; t++) { // 200 ms boyunca butonlara bak / poll the buttons for 200 ms
45 for (byte b = 0; b < 4; b++) {
46 if (digitalRead(BUTTON_PINS[b]) == LOW) {
47 digitalWrite(LED_PINS[i], LOW);
48 randomSeed(micros()); // basış anı her seferinde farklı / the press time always differs
49 while (digitalRead(BUTTON_PINS[b]) == LOW);
50 delay(600);
51 return;
52 }
53 }
54 delay(10);
55 }
56 digitalWrite(LED_PINS[i], LOW);
57 i = (i + 1) % 4;
58 }
59}
60
61void endGame(uint16_t score) {
62 tone(BUZZER_PIN, 110, 700);
63 for (byte k = 0; k < 3; k++) { // tüm LED'ler yanıp söner / all LEDs flash
64 for (byte i = 0; i < 4; i++) digitalWrite(LED_PINS[i], HIGH);
65 delay(200);
66 for (byte i = 0; i < 4; i++) digitalWrite(LED_PINS[i], LOW);
67 delay(200);
68 }
69 if (score > hiScore) { hiScore = score; EEPROM.put(HISCORE_ADDR, hiScore); }
70 Serial.print(F("Seviye / Level: ")); Serial.print(score);
71 Serial.print(F(" Rekor / Best: ")); Serial.println(hiScore);
72 level = 0;
73 delay(1000);
74}
75
76void setup() {
77 for (byte i = 0; i < 4; i++) {
78 pinMode(LED_PINS[i], OUTPUT);
79 pinMode(BUTTON_PINS[i], INPUT_PULLUP); // basınca LOW / LOW when pressed
80 }
81 Serial.begin(9600);
82 EEPROM.get(HISCORE_ADDR, hiScore);
83 if (hiScore == 0xFFFF) hiScore = 0;
84}
85
86void loop() {
87 if (level == 0) waitForStart();
88 if (level == MAX_LEVEL) { endGame(MAX_LEVEL); return; } // hepsini bildin / you completed every level
89
90 sequence[level++] = random(4); // diziye yeni bir renk ekle / append a new colour
91 int showMs = max(180, 520 - level * 20); // seviye arttıkça hızlanır / faster at higher levels
92 for (byte i = 0; i < level; i++) { // diziyi baştan göster / replay the whole sequence
93 playColor(sequence[i], showMs);
94 delay(showMs / 2);
95 }
96 for (byte i = 0; i < level; i++) { // oyuncu aynı sırayı tekrarlar / the player repeats it
97 if (waitForButton() != sequence[i]) { // yanlış ya da geç / wrong or too late
98 endGame(level - 1);
99 return;
100 }
101 }
102 delay(700); // tur arası / pause between rounds
103}En Simon la aleatoriedad lo es todo: si la secuencia es igual en cada partida, el jugador se la aprende de memoria. El método randomSeed(analogRead(A0)), que lee un pin sin conectar, es débil en la práctica porque el pin suele leer valores parecidos. Este código usa un truco más robusto: el juego espera la primera pulsación del jugador y toma como semilla el valor de micros() en ese instante. Nadie puede repetir el momento de una pulsación con precisión de microsegundos, así que cada partida empieza distinta. El tiempo de exhibición se acorta 20 ms por nivel y se queda en 180 ms; si no pulsas en 3 segundos o pulsas el color equivocado, la partida termina y, si es récord, el nivel alcanzado se guarda en la EEPROM.
Runner en LCD: caracteres personalizados en una pantalla 16x2
En una LCD de caracteres 16x2 no puedes dibujar píxeles, pero con createChar() puedes definir hasta 8 caracteres personalizados de 5×8 píxeles. Para el runner bastan tres: dos posturas de carrera y un cactus. La fila inferior de la pantalla es el suelo; los cactus se desplazan de derecha a izquierda y, al pulsar el botón, el corredor sube a la fila superior y se queda unos pasos en el aire. Como el juego es de cuadrícula, la comprobación de colisión es una sola línea: si hay un cactus en la columna del corredor mientras está en el suelo, se acaba la partida. Para la librería y la conexión, consulta pantalla LCD I2C.
1#include <Wire.h>
2#include <EEPROM.h>
3#include <LiquidCrystal_I2C.h>
4
5LiquidCrystal_I2C lcd(0x27, 16, 2); // adres 0x3F de olabilir / the address may also be 0x3F
6
7const byte JUMP_PIN = 3, BUZZER_PIN = 8;
8const int HISCORE_ADDR = 4; // EEPROM 4-5. bayt / EEPROM bytes 4-5
9const byte PLAYER_COL = 1; // koşucunun sütunu / runner's column
10const byte JUMP_STEPS = 4; // zıplama süresi (adım) / jump length (steps)
11const byte MIN_GAP = 4; // kaktüsler arası en az boşluk / minimum gap between cacti
12
13// 5x8 özel karakterler / 5x8 custom characters
14byte runA[8] = {0b01110, 0b01110, 0b00100, 0b01110, 0b10101, 0b00100, 0b01010, 0b10001};
15byte runB[8] = {0b01110, 0b01110, 0b00100, 0b01110, 0b10101, 0b00100, 0b01010, 0b01010};
16byte cactus[8] = {0b00100, 0b00101, 0b10101, 0b10101, 0b10110, 0b01100, 0b00100, 0b00100};
17
18byte ground[16]; // 1 = o sütunda kaktüs var / 1 = cactus in that column
19byte gap = 0, jumpLeft = 0;
20uint16_t score = 0, hiScore = 0;
21unsigned int stepMs = 300;
22unsigned long lastStep = 0;
23bool alive = false;
24
25// Sıçramasız basış: yalnız basıldığı an true / bounce-free: true only at the moment of a press
26bool buttonPressed() {
27 static bool stable = HIGH, lastRead = HIGH;
28 static unsigned long changedAt = 0;
29 bool r = digitalRead(JUMP_PIN);
30 if (r != lastRead) { lastRead = r; changedAt = millis(); }
31 if (millis() - changedAt > 15 && r != stable) {
32 stable = r;
33 return stable == LOW;
34 }
35 return false;
36}
37
38void draw() {
39 byte row = jumpLeft > 0 ? 0 : 1; // havadaysa üst satır / top row while jumping
40 lcd.setCursor(0, 0);
41 for (byte c = 0; c < 11; c++) lcd.write((uint8_t)(row == 0 && c == PLAYER_COL ? 0 : ' '));
42 char buf[6];
43 sprintf(buf, "%5u", score); // sağa yaslı skor / right-aligned score
44 lcd.print(buf);
45 lcd.setCursor(0, 1);
46 for (byte c = 0; c < 16; c++) {
47 if (row == 1 && c == PLAYER_COL) lcd.write((uint8_t)(score & 1)); // runA / runB sırayla / alternately
48 else lcd.write((uint8_t)(ground[c] ? 2 : ' '));
49 }
50}
51
52void startGame() {
53 memset(ground, 0, sizeof(ground));
54 gap = 0; jumpLeft = 0; score = 0; stepMs = 300;
55 alive = true;
56 lastStep = millis();
57 draw();
58}
59
60void die() {
61 alive = false;
62 tone(BUZZER_PIN, 150, 400);
63 if (score > hiScore) { hiScore = score; EEPROM.put(HISCORE_ADDR, hiScore); }
64 lcd.clear();
65 lcd.print(F("GAME OVER ")); lcd.print(score);
66 lcd.setCursor(0, 1);
67 lcd.print(F("BEST ")); lcd.print(hiScore);
68}
69
70void setup() {
71 pinMode(JUMP_PIN, INPUT_PULLUP);
72 lcd.init();
73 lcd.backlight();
74 lcd.createChar(0, runA);
75 lcd.createChar(1, runB);
76 lcd.createChar(2, cactus);
77 EEPROM.get(HISCORE_ADDR, hiScore);
78 if (hiScore == 0xFFFF) hiScore = 0;
79 randomSeed(analogRead(A3));
80 lcd.setCursor(0, 0); lcd.print(F("LCD RUNNER"));
81 lcd.setCursor(0, 1); lcd.print(F("BEST ")); lcd.print(hiScore);
82}
83
84void loop() {
85 if (buttonPressed()) {
86 if (!alive) startGame(); // başlat / start
87 else if (jumpLeft == 0) { // yerdeyse zıpla / jump only from the ground
88 jumpLeft = JUMP_STEPS;
89 tone(BUZZER_PIN, 1200, 20);
90 draw(); // zıplamayı hemen göster / show the jump at once
91 }
92 }
93 if (!alive || millis() - lastStep < stepMs) return;
94 lastStep = millis();
95
96 for (byte c = 0; c < 15; c++) ground[c] = ground[c + 1]; // dünya sola kayar / the world scrolls left
97 if (gap >= MIN_GAP && random(100) < 35) { ground[15] = 1; gap = 0; }
98 else { ground[15] = 0; gap++; }
99
100 if (jumpLeft > 0) jumpLeft--; // havada geçen bir adım / one step in the air
101 if (jumpLeft == 0 && ground[PLAYER_COL]) { die(); return; } // yerdeyken kaktüse çarptı / hit a cactus on the ground
102
103 score++;
104 if (score % 25 == 0 && stepMs > 120) stepMs -= 15; // giderek hızlan / speed up gradually
105 draw();
106}Desplazar el mundo consiste en copiar cada elemento del array ground una posición a la izquierda; la columna de la derecha recibe un cactus nuevo con un 35 por ciento de probabilidad. La regla MIN_GAP = 4 deja al menos cuatro columnas vacías entre dos cactus, de modo que todos los obstáculos se pueden superar. Cada 25 puntos el paso se acorta 15 ms, de 300 a 120 ms. Reescribir las dos filas en cada paso en lugar de borrar la pantalla con lcd.clear() evita el parpadeo; el comando clear() por sí solo tarda unos 2 ms. Si solo ves cuadrados, gira el ajuste de contraste de la parte trasera del módulo; si no ves nada, cambia la dirección de 0x27 a 0x3F.
Test de reacción: mide tu tiempo de reacción en milisegundos
Es el juego más sencillo y el más adictivo: un LED se enciende en un momento aleatorio, pulsas el botón lo más rápido que puedas y el Arduino escribe el tiempo transcurrido en milisegundos en el monitor serie. Solo necesitas un LED en D7 con una resistencia de 220 Ω, el pulsador de D3 y un buzzer opcional. La espera es aleatoria, de 2 a 5 segundos; si pulsas mientras esperas recibes el aviso "demasiado pronto", así que no se puede hacer trampa adivinando. El tiempo se mide con micros(), se calcula tu media y tu mejor tiempo se guarda en la EEPROM.
1#include <EEPROM.h>
2
3const byte LED_PIN = 7, BUTTON_PIN = 3, BUZZER_PIN = 8;
4const int BEST_ADDR = 6; // EEPROM 6-7. bayt / EEPROM bytes 6-7
5const bool USE_SOUND = false; // true: LED ile birlikte bip / true: beep together with the LED
6
7uint16_t best; // en iyi süre (ms); 0xFFFF = kayıt yok / best time (ms); 0xFFFF = none
8unsigned long total = 0;
9unsigned int rounds = 0;
10
11bool pressed() { return digitalRead(BUTTON_PIN) == LOW; }
12
13void waitRelease() {
14 while (pressed());
15 delay(50); // bırakma sıçramasını atla / skip the release bounce
16}
17
18void setup() {
19 pinMode(LED_PIN, OUTPUT);
20 pinMode(BUTTON_PIN, INPUT_PULLUP);
21 Serial.begin(9600);
22 EEPROM.get(BEST_ADDR, best); // boş EEPROM 0xFFFF = kayıt yok / blank EEPROM 0xFFFF = no record
23 Serial.println(F("Baslamak icin butona bas / Press the button to start"));
24}
25
26void loop() {
27 while (!pressed()); // başlatma basışı / start press
28 randomSeed(micros());
29 waitRelease();
30 Serial.println(F("Hazir ol... / Get ready..."));
31
32 unsigned long waitMs = random(2000, 5001); // 2-5 sn rastgele bekleme / random 2-5 s wait
33 unsigned long start = millis();
34 while (millis() - start < waitMs) {
35 if (pressed()) { // erken basış / false start
36 tone(BUZZER_PIN, 150, 500);
37 Serial.println(F("Erken bastin! / Too early!"));
38 waitRelease();
39 return;
40 }
41 }
42
43 unsigned long t0 = micros();
44 digitalWrite(LED_PIN, HIGH); // işaret / signal
45 if (USE_SOUND) tone(BUZZER_PIN, 2000, 50);
46 while (!pressed() && micros() - t0 < 2000000UL); // en fazla 2 sn bekle / wait at most 2 s
47 unsigned long ms = (micros() - t0) / 1000;
48 digitalWrite(LED_PIN, LOW);
49
50 if (ms >= 2000) {
51 Serial.println(F("Cok yavas / Too slow"));
52 return;
53 }
54 total += ms; rounds++;
55 Serial.print(F("Sure / Time: ")); Serial.print(ms);
56 Serial.print(F(" ms Ortalama / Average: ")); Serial.print(total / rounds);
57 Serial.println(F(" ms"));
58 if (ms < best) { // daha kısa süre daha iyi / shorter is better
59 best = ms;
60 EEPROM.put(BEST_ADDR, best);
61 tone(BUZZER_PIN, 1500, 150);
62 Serial.println(F("Yeni rekor! / New record!"));
63 }
64 Serial.print(F("Rekor / Best: ")); Serial.print(best); Serial.println(F(" ms"));
65 waitRelease();
66 delay(1000);
67}El tiempo medio de reacción humano ante la luz ronda los 200–250 ms; un jugador bien concentrado puede bajar un poco de ahí. Si pones USE_SOUND a true, suena un pitido junto con el LED. Ante un sonido solemos reaccionar algo más rápido que ante la luz, así que comparar los récords de los dos modos es un experimento interesante. Si muestras el tiempo en un display de 7 segmentos o en una OLED en lugar del monitor serie, tendrás un aparato a pilas que funciona sin ordenador.
Juegos con ESP32
La mayor parte del código de esta guía funciona en el ESP32 con pequeños cambios: las librerías Adafruit SSD1306 y NeoPixel son compatibles con el ESP32, y tone() está disponible en las versiones 2.x y 3.x del núcleo Arduino para ESP32. Hay que cambiar tres cosas: los pines (columna ESP32 de la tabla común), los umbrales del joystick y la forma de guardar el récord. El ADC del ESP32 es de 12 bits y devuelve valores de 0 a 4095, así que usa unos 1200 y 2800 en lugar de 300 y 700. Conecta el joystick a los pines ADC1, GPIO 32–39, porque los pines ADC2 no se pueden leer con el Wi-Fi activo.
El ESP32 no tiene una EEPROM real; la librería EEPROM la emula en la flash y necesita llamadas a EEPROM.begin() y EEPROM.commit(). Es más limpio usar la librería Preferences: guarda los valores con un espacio de nombres (namespace) y una clave en la partición NVS de la flash y reparte el desgaste por sí misma. El juego "carrera de clics" que verás abajo cuenta cuántas veces puedes pulsar el botón en 5 segundos y guarda el récord con Preferences:
1#include <Wire.h>
2#include <Adafruit_GFX.h>
3#include <Adafruit_SSD1306.h>
4#include <Preferences.h>
5
6Adafruit_SSD1306 display(128, 64, &Wire, -1); // SDA = GPIO 21, SCL = GPIO 22
7Preferences prefs;
8
9const int BUTTON_PIN = 26; // aksiyon butonu / action button
10const int BUZZER_PIN = 27; // pasif buzzer / passive buzzer
11const unsigned long ROUND_MS = 5000; // 5 saniyelik tur / 5-second round
12
13uint32_t best = 0;
14
15// Sıçramasız basış: yalnız basıldığı an true / bounce-free: true only at the moment of a press
16bool buttonPressed() {
17 static bool stable = HIGH, lastRead = HIGH;
18 static unsigned long changedAt = 0;
19 bool r = digitalRead(BUTTON_PIN);
20 if (r != lastRead) { lastRead = r; changedAt = millis(); }
21 if (millis() - changedAt > 10 && r != stable) {
22 stable = r;
23 return stable == LOW;
24 }
25 return false;
26}
27
28void screen(const char *title, uint32_t big, uint32_t small) {
29 display.clearDisplay();
30 display.setTextSize(1);
31 display.setCursor(0, 0); display.print(title);
32 display.setCursor(0, 56); display.print("BEST "); display.print(best);
33 display.setTextSize(3);
34 display.setCursor(0, 20); display.print(big);
35 display.setTextSize(1);
36 display.setCursor(90, 56); display.print(small);
37 display.display();
38}
39
40void setup() {
41 pinMode(BUTTON_PIN, INPUT_PULLUP);
42 Serial.begin(115200);
43 if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
44 Serial.println("SSD1306 allocation failed");
45 for (;;) delay(1000);
46 }
47 display.setTextColor(SSD1306_WHITE);
48 prefs.begin("oyun", false); // "oyun" ad alanı, okuma-yazma / "oyun" namespace, read-write
49 best = prefs.getUInt("rekor", 0); // kayıt yoksa 0 / 0 when nothing is stored
50}
51
52void loop() {
53 screen("TAP RACE: PRESS", 0, 0);
54 while (!buttonPressed()) delay(1); // başlatma / start
55 for (int i = 3; i > 0; i--) { // 3-2-1 geri sayım / 3-2-1 countdown
56 screen("GET READY", i, 0);
57 tone(BUZZER_PIN, 800, 80);
58 delay(1000);
59 }
60 tone(BUZZER_PIN, 1600, 150);
61
62 uint32_t taps = 0;
63 unsigned long start = millis(), lastDraw = 0;
64 while (millis() - start < ROUND_MS) {
65 if (buttonPressed()) taps++;
66 if (millis() - lastDraw > 100) { // ekranı 10 kez/sn çiz / redraw 10 times per second
67 lastDraw = millis();
68 screen("TAP!", taps, (ROUND_MS - (millis() - start)) / 1000 + 1);
69 }
70 }
71
72 if (taps > best) { // yeni rekor flash'a / new record to flash
73 best = taps;
74 prefs.putUInt("rekor", best);
75 tone(BUZZER_PIN, 2000, 300);
76 }
77 screen("RESULT", taps, 0);
78 Serial.printf("Basis / Taps: %lu Rekor / Best: %lu\n", (unsigned long)taps, (unsigned long)best);
79 delay(3000);
80}prefs.getUInt("rekor", 0) devuelve el segundo parámetro si no hay nada guardado, así que nunca tendrás que lidiar con el problema del 0xFFFF de la EEPROM. Los nombres del espacio y de la clave pueden tener como máximo 15 caracteres. Aunque el procesador del ESP32 es muchas veces más rápido que el del Uno, la OLED usa el mismo bus I2C, así que la tasa de fotogramas apenas cambia; para juegos mucho más fluidos y en color puedes usar una pantalla TFT ILI9341 por SPI. El Bluetooth del ESP32, por su parte, es un buen punto de partida para convertir el móvil en un mando.
Juegos con Raspberry Pi Pico y MicroPython
Si programas la Raspberry Pi Pico en MicroPython, la lógica del juego es la misma y solo cambia la sintaxis: time.ticks_ms() y time.ticks_diff() en lugar de millis(), read_u16(), que devuelve de 0 a 65535, en lugar de analogRead(), y PWM en lugar de tone(). La Pico no tiene EEPROM, pero gracias al sistema de archivos de MicroPython es muy fácil escribir el récord en un pequeño archivo de texto en la flash. El código de abajo es la versión para Pico del test de reacción y guarda el récord en el archivo rekor.txt; si lo guardas en la placa como main.py desde Thonny, arrancará solo cada vez que la enciendas. Si eres nuevo en MicroPython, lee antes qué es la Raspberry Pi Pico.
1from machine import Pin, PWM
2import time, random
3
4led = Pin("LED", Pin.OUT) # kart üstündeki LED / onboard LED
5button = Pin(14, Pin.IN, Pin.PULL_UP) # GP14, basınca 0 / 0 when pressed
6buzzer = PWM(Pin(16)) # GP16, pasif buzzer / passive buzzer
7buzzer.duty_u16(0) # başlangıçta sessiz / silent at start
8FILE = "rekor.txt" # rekor flash'ta dosyada / record kept in a file on flash
9
10def beep(freq, ms):
11 buzzer.freq(freq)
12 buzzer.duty_u16(32768) # %50 görev oranı / 50% duty cycle
13 time.sleep_ms(ms)
14 buzzer.duty_u16(0)
15
16def load_best():
17 try:
18 with open(FILE) as f:
19 return int(f.read())
20 except (OSError, ValueError): # dosya yok ya da bozuk / file missing or corrupt
21 return 99999
22
23def save_best(ms):
24 with open(FILE, "w") as f:
25 f.write(str(ms))
26
27def wait_release():
28 while button.value() == 0:
29 time.sleep_ms(5)
30 time.sleep_ms(50) # bırakma sıçraması / release bounce
31
32best = load_best()
33print("Baslamak icin butona bas / Press the button to start")
34
35while True:
36 while button.value() == 1: # başlatma basışı / start press
37 time.sleep_ms(5)
38 wait_release()
39 print("Hazir ol... / Get ready...")
40
41 wait = random.randint(2000, 5000) # 2-5 sn rastgele bekleme / random 2-5 s wait
42 start = time.ticks_ms()
43 early = False
44 while time.ticks_diff(time.ticks_ms(), start) < wait:
45 if button.value() == 0:
46 early = True
47 break
48 if early:
49 print("Erken bastin! / Too early!")
50 beep(150, 400)
51 wait_release()
52 continue
53
54 t0 = time.ticks_us()
55 led.on() # işaret / signal
56 while button.value() == 1 and time.ticks_diff(time.ticks_us(), t0) < 2000000:
57 pass
58 ms = time.ticks_diff(time.ticks_us(), t0) // 1000
59 led.off()
60
61 if ms >= 2000:
62 print("Cok yavas / Too slow")
63 continue
64 print("Sure / Time:", ms, "ms")
65 if ms < best: # daha kısa süre daha iyi / shorter is better
66 best = ms
67 save_best(best)
68 print("Yeni rekor! / New record!")
69 beep(1500, 150)
70 print("Rekor / Best:", best, "ms")
71 wait_release()
72 time.sleep(1)Al medir tiempos es importante usar ticks_diff(): los contadores vuelven a empezar al llegar a cierto valor, y restar directamente dos valores da un resultado erróneo en ese momento; ticks_diff() calcula bien ese desbordamiento. Para juegos con OLED, el driver ssd1306 que instalas desde el gestor de paquetes de Thonny ofrece comandos como fill_rect(), text() y show(), parecidos a los de Adafruit GFX. El módulo neopixel que necesitas para una matriz NeoPixel ya viene incluido en MicroPython. Así puedes llevar también Pong y Snake a la Pico con la misma lógica.
Errores comunes y soluciones
| Síntoma | Causa | Solución |
|---|---|---|
| La pantalla se queda en blanco | Si el monitor serie muestra "SSD1306 allocation failed", falta RAM; si no, la dirección o el cableado son incorrectos | Pon los textos fijos dentro de F() y reduce los arrays grandes; sin ese mensaje, revisa los cables SDA/SCL y prueba la dirección 0x3D |
| El juego empieza igual cada vez | No hay randomSeed() o la semilla siempre es la misma | Usa como semilla el valor de micros() en la primera pulsación |
| Una pulsación provoca dos saltos | Rebote del pulsador | Usa la función buttonPressed(), que espera 15 ms de estabilidad |
| El récord muestra 65535 | Una EEPROM sin escribir lee 0xFF | Trata 0xFFFF como 0 |
| El personaje se desplaza solo con el joystick suelto | El centro no es 512 y la zona muerta es estrecha | Mantén los umbrales 300/700 o mide el centro al arrancar y calibra |
| La serpiente salta en diagonal | La matriz está conectada en zigzag | Pon SERPENTINE = true |
| El Arduino se reinicia al subir el brillo | La matriz LED consume demasiada corriente | Baja setBrightness() y usa una fuente externa de 5 V |
| El PWM del pin 3 u 11 falla cuando suena tone() | En el Uno, tone() usa el Timer2 | Pasa el componente que necesita PWM al pin 5, 6, 9 o 10 |
| El juego se vuelve lento con el tiempo | Serial.print o dibujo innecesario en cada fotograma | Quita la salida serie y mantén el fotograma por encima del tiempo de envío de la pantalla |
| La LCD solo muestra cuadrados | Contraste mal ajustado o dirección incorrecta | Gira el ajuste trasero y prueba la dirección 0x27 o 0x3F |
Ideas para mejorar tus juegos
- Pong para dos: borra las líneas de la CPU, controla la segunda pala con un potenciómetro en A2 y lleva los dos mandos a los extremos de la mesa con cables largos.
- Control por inclinación: usa un MPU6050 en lugar del joystick y dirige la serpiente o la pala inclinando la placa.
- Música y efectos: reproduce música de juego con un DFPlayer Mini; para efectos cortos basta un buzzer pasivo.
- Jugar con el móvil: convierte tu teléfono en un mando inalámbrico con un módulo Bluetooth HC-05.
- Mando para el ordenador: como el Arduino Leonardo puede funcionar como teclado USB, puedes construir un mando para juegos de PC con un joystick y pulsadores.
- Marcador: muestra los récords como texto deslizante en un módulo MAX7219 de 4 en 1.
- Juegos de azar: haz una ruleta con un anillo WS2812B o un dado electrónico con LED; la forma más divertida de practicar random().
Hacer juegos con Arduino es la manera más amena de aprender código y hardware a la vez. Cuando entiendas el bucle de juego, la detección de colisiones y el récord en la EEPROM, crear tu propio juego será solo cuestión de tener una idea; para inspirarte más, echa un vistazo a la lista de ideas de proyectos con Arduino. En la app BlueGrays encontrarás Pong, el T-Rex en OLED, Snake en matrices de 8x8 y 16x16, Simon, el runner en LCD y el test de reacción como proyectos paso a paso con esquemas de circuito y código listo para usar.
Preguntas frecuentes
¿Qué juegos se pueden hacer con Arduino?
Con un Arduino Uno puedes hacer Pong, el T-Rex runner, juegos tipo Flappy Bird, Snake, juegos sencillos de bloques, el juego de memoria Simon, un test de reacción y juegos de azar como un dado o una ruleta. Para juegos con gráficos basta una OLED de 128×64 y para juegos de cuadrícula en color, una matriz LED NeoPixel. Simon y el test de reacción se juegan incluso sin pantalla, solo con LED y pulsadores.
¿Cuál es la mejor pantalla para un juego con Arduino?
Para empezar, la mejor opción es la OLED SSD1306 de 0,96 pulgadas: es barata, se conecta con solo dos cables de datos y dibuja gráficos en movimiento a 128×64 píxeles. Si quieres una imagen más grande y en color, va bien una pantalla TFT ILI9341 o ST7789, y para juegos de estilo pixel art, una matriz LED de 8x8 o 16x16. Una LCD 16x2 con caracteres personalizados también sirve para juegos de carreras sencillos.
¿Cómo se guarda un récord en Arduino?
Si escribes el récord en la EEPROM, no se pierde aunque se corte la alimentación. Lo escribes con EEPROM.put(dirección, récord) y lo lees con EEPROM.get(dirección, récord); el Arduino Uno tiene 1024 bytes para ello. Como cada celda admite unas 100.000 escrituras, escribe solo cuando termine la partida y se haya batido el récord.
¿Se puede hacer un mando de videojuegos con Arduino?
Sí. Las placas con el procesador ATmega32U4, como el Arduino Leonardo, Micro o Pro Micro, pueden presentarse ante el ordenador como teclado, ratón o mando USB; leen el joystick y los pulsadores y envían pulsaciones de teclas. El Arduino Uno no puede hacerlo directamente, pero sí enviar los valores del joystick por el puerto serie a un programa del ordenador.
¿Por qué mi juego de Arduino va lento o se traba?
Las causas más habituales son los bucles hechos con delay(), las llamadas a Serial.print en cada fotograma y el tiempo de envío de la pantalla. En el Uno, enviar toda la imagen de la OLED tarda unos 25 ms, así que un fotograma de 30 ms es realista. Usa un tiempo de fotograma fijo con millis() en lugar de delay(), quita la salida serie y dibuja solo lo necesario.
¿Cuál es el juego de Arduino más fácil para principiantes?
El test de reacción es el más fácil: se hace con un LED, un pulsador y un programa corto, pero enseña conceptos básicos como la temporización, los números aleatorios y la EEPROM. Después, un buen camino de aprendizaje es pasar a Simon con cuatro LED y cuatro pulsadores, y luego a Pong y T-Rex con una pantalla OLED.
Proyectos para hacer después de leer esto
Ver todos los proyectos
Juego de Raquetas - SSD1306
Una pelota, dos palas y tú: juegas al tenis de mesa, uno de los primeros videojuegos de la historia, en una pequeña pantalla OLED. Con dos botones mueves tu pala de la derecha arriba y abajo, mientras

OLED T-Rex Run: Juego del dinosaurio con Arduino
El juego del dinosaurio que aparece cuando se va internet, ahora en tu mesa: el dinosaurio corre por la pequeña pantalla OLED, los cactus se le echan encima y tú saltas con un solo botón. Tu puntuació

Serpiente de píxeles Matriz 8x8
Monta con tus propias manos el clásico juego de la serpiente y juega. En una matriz de luces de colores de 8x8 guías una serpiente verde con un joystick y te comes manzanas rojas; con cada manzana la

Matriz Snake 16x16
El clásico juego de la serpiente, esta vez en una enorme pantalla LED de 256 luces. Guías a la serpiente verde con un joystick y recoges manzanas rojas; con cada manzana la serpiente crece, tu puntuac

Juego de memoria con Arduino (4 LED + 4 pulsadores + zumbador)
¿Te fías de tu memoria? Cuatro LED de colores se encienden en secuencia, cada uno con su propia nota, y tú repites el mismo orden pulsando los botones. Cada ronda acertada añade un color nuevo a la se

Juego Arduino LCD Runner
Juego endless runner de desplazamiento lateral que se ejecuta en una pantalla LCD de 16x2 caracteres. El héroe corre automáticamente, el jugador salta los obstáculos pulsando el botón. Aparecen obstáculos aleatorios en las filas superior e inferior. El zumbador pasivo reproduce la melodía de apertura, el sonido de salto, el sonido de celebración de 50 puntos y la melodía de fin de partida. La pantalla de inicio "BlueGrays - Arduino Projects" se muestra al arrancar.

Arduino Test de Reacción con Aviso Sonoro: Mide tus Reflejos en Milisegundos (LCD + Zumbador + 3 LED)
¿Tienes buenos reflejos? Pulsa el botón: el LED rojo dice preparados, el amarillo dice listos y empieza una espera aleatoria de 1,5 a 3,5 segundos. En cuanto se enciende el LED verde y suena el zumbador, vuelve a pulsar; tu tiempo de reacción aparece en milisegundos en la LCD I2C, con tu récord de la sesión y tu media debajo. Si pulsas antes del verde, la pantalla muestra TOO EARLY! y suena un tono de derrota. Mantén pulsado para un nuevo jugador con las mismas esperas.
Más artículos
App BlueGrays
Monta este circuito paso a paso en tu teléfono
Esquema, código y lista de piezas en una sola pantalla. Más de 200 proyectos de Arduino, ESP32 y Pico listos para montar.
Escanea con tu teléfono
Apunta la cámara al código y se abrirá la tienda de apps de tu teléfono.
o ábrela en la tienda

