Jogos com Arduino: como fazer Pong, T-Rex, Snake e Simon
Projetos de jogos com Arduino passo a passo: Pong e T-Rex num OLED, Snake numa matriz de LED, Simon, uma corrida no LCD e um teste de reação, mais código de recorde na EEPROM.
Fazer jogos com Arduino é o jeito mais divertido de aprender eletrônica e programação: com um display OLED, alguns botões e um joystick, você mesmo monta clássicos como Pong, a corrida do T-Rex, Snake e o jogo da memória Simon, e depois desafia os amigos a bater o seu recorde. Enquanto escreve um jogo, você também aprende quase sem perceber os temas mais importantes da programação: controle de tempo, máquinas de estados, sistemas de coordenadas, cálculo de colisões, números aleatórios e memória permanente. Todos esses jogos rodam num único Arduino Uno; os 2 KB de RAM dele bastam, desde que você use a memória com cuidado.
Neste guia, montamos seis jogos do começo ao fim: Pong e a corrida do T-Rex num display OLED, Snake numa matriz de LED NeoPixel 8x8 e 16x16, Simon com quatro LEDs e quatro botões, um jogo de corrida num LCD 16x2 e um teste de reação com precisão de milissegundos. Cada jogo vem com lista de componentes, ligações e código que compila. Primeiro aprendemos o esqueleto que todos compartilham, ou seja, o loop do jogo, a detecção de colisão, a pontuação e a gravação do recorde na EEPROM, e depois o reaproveitamos em cada jogo. No fim, você também encontra versões para ESP32 e Raspberry Pi Pico.

Qual jogo precisa de quais componentes?
Quando você faz um jogo com Arduino, a primeira decisão é o display, porque é ele que define que tipo de jogo dá para fazer. Só com alguns LEDs você já monta o Simon ou um teste de reação, enquanto um OLED de 128×64 pixels torna possíveis o Pong e o T-Rex, com gráficos em movimento e física. A tabela abaixo resume os jogos deste guia, do mais fácil ao mais difícil:
| Jogo | Display | Entrada | Outros componentes | Dificuldade |
|---|---|---|---|---|
| Teste de reação | 1 LED + Monitor Serial | 1 botão | Resistor de 220 Ω, buzzer opcional | Fácil |
| Jogo da memória Simon | 4 LEDs coloridos | 4 botões | 4 resistores de 220 Ω, buzzer passivo | Fácil–médio |
| Corrida no LCD | LCD I2C 16x2 | 1 botão | Buzzer passivo | Médio |
| Pong | OLED SSD1306 de 0,96" | Joystick ou potenciômetro de 10 kΩ | Buzzer passivo | Médio |
| Corrida do T-Rex | OLED SSD1306 de 0,96" | 1 botão ou o botão do joystick | Buzzer passivo | Médio |
| Snake (8x8) | Matriz NeoPixel 8x8 | Joystick | Resistor de 330 Ω, capacitor de 1000 µF | Médio–avançado |
| Snake (16x16) | Matriz NeoPixel 16x16 | Joystick | Fonte externa de 5 V e 3–4 A | Avançado |






Ligações em comum: um circuito que vira console de jogos
Escolhemos pinos em comum para que todos os jogos, exceto o Simon, rodem no mesmo circuito. O OLED e o LCD I2C compartilham os mesmos dois fios (SDA e SCL); o joystick vai nas entradas analógicas, o botão de ação no D3 e o buzzer no D8. Depois de montar esse “console de jogos”, você troca de jogo só carregando outro código. Você vê o mapa completo de pinos da sua placa na ferramenta Pinagem.
| Componente / pino | Arduino Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| OLED ou LCD I2C: SDA | A4 | GPIO 21 | GP4 |
| OLED ou LCD I2C: SCL | A5 | GPIO 22 | GP5 |
| Joystick VRx (eixo X) | A0 | GPIO 34 | GP26 |
| Joystick VRy (eixo Y) | A1 | GPIO 35 | GP27 |
| Joystick SW (botão) | D2 | GPIO 25 | GP15 |
| Botão de ação (outro terminal no GND) | D3 | GPIO 26 | GP14 |
| Buzzer passivo (+) | D8 | GPIO 27 | GP16 |
| NeoPixel DIN (através de 330 Ω) | D6 | GPIO 13 | GP22 |
Como funciona o loop de um jogo?
Todo videogame se baseia num loop de quatro passos que se repete dezenas de vezes por segundo: ler a entrada (botões, joystick), atualizar o mundo (posição, velocidade, pontuação), verificar colisões e desenhar a tela. Cada passagem pelo loop se chama quadro (frame). Trinta quadros por segundo (30 FPS) bastam para parecer fluido, o que significa que cada quadro dura cerca de 33 ms. A função loop() do Arduino já é um loop, mas roda muito rápido e de forma irregular, então você precisa dividi-la em quadros fixos com millis().
Evitar o delay() aqui é essencial. Se você escreve delay(33), o Arduino não faz nada durante esses 33 ms, pode perder um toque rápido no botão, e a velocidade do jogo muda sempre que o desenho leva mais ou menos tempo. Com millis(), o loop() lê a entrada a cada passagem, mas só avança o jogo quando chega a hora de um novo quadro. A diferença entre os dois está explicada em detalhes em millis ou delay no Arduino. O esqueleto abaixo é a base de todos os jogos deste guia:
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}A segunda ideia-chave do código é a máquina de estados. A cada momento, o jogo está num de três estados: MENU, PLAYING ou GAME_OVER. Cada estado reage de forma diferente ao mesmo botão: no menu, um toque inicia o jogo; durante a partida, faz o personagem pular; e depois do fim de jogo, começa uma nova rodada. Dividir o código assim elimina blocos if aninhados e facilita acrescentar um novo estado, como PAUSE. Tratamos o tema com mais profundidade em máquina de estados no Arduino.
Leitura de botões e de um joystick
Ligar os botões com INPUT_PULLUP é o método mais limpo: não precisa de resistor externo, o pino lê HIGH em repouso e LOW quando pressionado. Porém, um botão mecânico trepida por alguns milissegundos ao ser pressionado ou solto, e o Arduino entende isso como vários toques seguidos. Num jogo, isso significa pular duas vezes com um toque só, ou o Simon contar uma cor errada. A função buttonPressed() do esqueleto só aceita um novo estado depois que o pino fica igual por 15 ms e retorna true uma única vez, no momento do toque. Para o circuito e outros métodos, veja como ligar um botão ao Arduino.
Um joystick é feito de dois potenciômetros e um botão. No Uno, cada eixo dá um valor de 0 a 1023 e, com a alavanca solta, o centro fica em torno de 512, mas isso varia de módulo para módulo entre aproximadamente 490 e 530. Por isso, jogos em grade usam limites em vez de valores exatos: abaixo de 300 significa esquerda ou cima, acima de 700 significa direita ou baixo. A faixa larga no meio (a zona morta) impede que o personagem se mova sozinho quando você solta a alavanca. Já no Pong, mapeamos o valor analógico direto para a raquete: map() converte a faixa 0–1023 no curso de 0–48 pixels da raquete. A calibração e a tabela de direções estão no guia do módulo joystick.
Como funciona a detecção de colisão?
A detecção de colisão pergunta se dois objetos estão no mesmo lugar ao mesmo tempo. Em jogos com tela, o método mais comum é o teste AABB (caixa delimitadora alinhada aos eixos): você trata cada objeto como um retângulo. Dois retângulos se sobrepõem só se as quatro condições forem verdadeiras ao mesmo tempo: a borda esquerda de A está à esquerda da borda direita de B, a borda direita de A está à direita da borda esquerda de B, e as mesmas duas condições valem no eixo vertical. No código, é uma linha só: ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by. Em jogos em grade (Snake, a corrida no LCD), é ainda mais simples: se duas células têm as mesmas coordenadas, há colisão.
| Jogo | O que colide com o quê? | Método |
|---|---|---|
| Pong | Bola com as paredes e as raquetes | AABB + inversão da velocidade |
| T-Rex | Dinossauro com os cactos | AABB (com uma caixa um pouco menor) |
| Snake | Cabeça com as paredes, o corpo e a comida | Igualdade de células da grade |
| Corrida no LCD | Corredor com um cacto | Mesma coluna e mesma linha |
| Simon | Cor pressionada com a cor da sequência | Comparação de elementos do array |
| Teste de reação | Momento do toque com o momento do sinal | Comparação de tempos (largada queimada) |
Pontuação e gravação do recorde na EEPROM
A pontuação fica numa variável na RAM e some no instante em que você desconecta o Arduino. Para tornar um recorde permanente, você usa a EEPROM: o ATmega328P do Arduino Uno tem 1024 bytes de EEPROM, que mantêm o conteúdo sem alimentação. Dois bytes do tipo uint16_t bastam para um recorde (0–65535). O único ponto fraco da EEPROM é a resistência à gravação: cada célula pode ser gravada cerca de 100.000 vezes. Parece muito, mas se você gravar a pontuação a cada quadro, 30 vezes por segundo, a célula se desgasta em cerca de 55 minutos. O certo é gravar só quando o jogo termina e o recorde foi batido; mesmo com 10 recordes novos por dia, a célula vai durar 27 anos.
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() {}O código tem dois pontos sutis. Primeiro, todo byte de uma EEPROM que nunca foi gravada vale 0xFF, então na primeiríssima inicialização o recorde aparece como 65535, e a verificação de 0xFFFF o transforma em 0. No teste de reação é o contrário: 0xFFFF significa “ainda não há recorde”, porque ali um tempo menor é melhor. Segundo, o EEPROM.put() lê cada byte antes de gravar e só grava os bytes que mudaram, o que aumenta ainda mais a vida útil. Para os outros comandos da EEPROM, veja como usar a EEPROM do Arduino. Para que você possa carregar os jogos deste guia no mesmo Uno, um depois do outro, cada jogo tem seu próprio endereço:
| Jogo | Endereço na EEPROM | Valor gravado |
|---|---|---|
| Corrida do T-Rex | 0–1 | Maior pontuação |
| Snake | 2–3 | Maior quantidade de comida |
| Corrida no LCD | 4–5 | Maior pontuação |
| Teste de reação | 6–7 | Menor tempo (ms) |
| Simon | 8–9 | Maior nível alcançado |
Pong: display OLED e joystick
Pong é o jogo de tênis de mesa lançado em 1972 que deu início à era dos videogames. Na nossa versão, você controla a raquete da esquerda com o eixo Y do joystick (ou com um potenciômetro no A1), e o Arduino joga com a raquete da direita. Componentes: um Arduino Uno, um OLED I2C SSD1306 de 0,96 polegada, um joystick e um buzzer passivo. Ao instalar a biblioteca Adafruit SSD1306 pelo Gerenciador de Bibliotecas, instale também a dependência dela, a Adafruit GFX. As ligações do display e os comandos de desenho estão explicados em detalhes em display 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}O que torna o Pong divertido é a função hitPaddle(). Se a bola bate no meio da raquete, volta quase reta; se bate na borda, ricocheteia num ângulo fechado e pressiona o adversário. Fazemos isso escalando a distância da bola até o centro da raquete para um valor entre -1 e +1 e transformando-o em velocidade vertical. A bola acelera 5% a cada rebatida e é limitada por MAX_SPEED. A raquete do Arduino pode se mover no máximo 1,2 pixel por quadro, então, se você der um ângulo fechado à bola, ela não consegue acompanhar e você marca ponto. Para deixar o jogo mais difícil, aumente esse valor para 1.5; para dois jogadores, apague as linhas da CPU e controle a segunda raquete com um potenciômetro no A2.
Por que um tempo de quadro de 30 ms? O Uno envia o buffer de quadro de 1024 bytes ao OLED pelo I2C de 400 kHz em cerca de 25 ms, então o display não consegue mostrar mais de 35–40 quadros por segundo. Manter o tempo de quadro um pouco acima desse tempo de transferência deixa a velocidade do jogo estável. E não se engane com o relatório do compilador “Variáveis globais usam 28%”: o buffer do SSD1306 é alocado em tempo de execução, então o uso real de RAM sobe para cerca de 78%. Se a memória acabar, você verá o erro SSD1306 allocation failed; reunimos as soluções em memória insuficiente no Arduino.
Corrida do T-Rex: física do pulo e recorde
Vamos levar para o display OLED o jogo do dinossauro que o Chrome mostra quando você está sem internet. O dinossauro corre sem sair do lugar, os cactos passam da direita para a esquerda e você pula sobre eles apertando o botão. Para o pulo parecer realista, usamos uma física simples: apertar o botão define a velocidade vertical em -4,2 pixels por quadro, a cada quadro a gravidade soma 0,3 a essa velocidade, e a posição muda conforme a velocidade. Assim, o dinossauro sobe desacelerando, fica um instante parado no alto e cai cada vez mais rápido; ele sobe cerca de 27 pixels e fica no ar por 27 quadros, cerca de 0,8 segundo. Os componentes são os mesmos do Pong; para pular, você usa o botão no D3 ou o botão do 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}| Parâmetro | Valor | Efeito |
|---|---|---|
| JUMP_V | -4.2 | Força do pulo; com -5, o dinossauro sobe cerca de 40 pixels, perto do topo da tela |
| GRAVITY | 0.3 | Um valor maior deixa o pulo mais curto e mais brusco |
| speed | 2.0 → 5.0 | Velocidade dos cactos; aumenta 0,25 a cada 200 quadros |
| random(70, 140) | pixels | Distância entre dois cactos; valores menores deixam o jogo mais difícil |
| Hitbox | 10×12 pixels | Menor que o dinossauro desenhado de 14×14, para que quase-colisões não contem como morte |
O recorde é gravado na EEPROM no instante em que o jogo termina, e só se tiver sido batido; ele aparece no canto superior direito como HI. Se você segurar o botão D3 ao ligar a placa, o recorde é zerado, o que é prático quando começa um novo torneio com os amigos. A altura e a largura dos cactos vêm de random(). Para que cada partida comece diferente, a linha randomSeed(analogRead(A3)) usa o ruído de um pino analógico desconectado como semente.
Snake numa matriz de LED (8x8 e 16x16)
O jogo Snake, que ficou famoso nos antigos celulares Nokia, fica melhor numa matriz de LED, porque cada LED é uma célula do jogo. Usamos uma matriz colorida WS2812B NeoPixel: a versão 8x8 tem 64 pixels e a 16x16, 256, todos controlados por um único fio de dados. O corpo da cobra fica guardado em dois arrays (snakeX e snakeY), e o elemento 0 é a cabeça. A cada passo, o corpo se desloca uma célula e a nova posição é gravada na cabeça; quando a cobra come, a cauda não é removida, e assim ela cresce uma célula. Para compilar o código, instale a biblioteca Adafruit NeoPixel; os detalhes das ligações estão no guia 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}Há três regras críticas no código. Sem marcha a ré: se uma cobra que vai para a direita vira de repente para a esquerda, ela bate no próprio pescoço; readJoystick() compara a nova direção com a direção atual da cobra, e não com a que está pendente, então nem dois movimentos rápidos conseguem fazê-la dar meia-volta. Exceção da cauda: num passo sem comida, a ponta da cauda sai do lugar, então mover a cabeça para essa célula não é colisão. Posicionar a comida: placeFood() continua tentando até que a comida não caia sobre a cobra. Cada comida encurta o tempo do passo em 10 ms (de 300 ms até 120 ms); quando o jogo termina, um LED acende em azul para cada comida que você pegou, e o recorde vai para a EEPROM.
Também importa como os LEDs da sua matriz estão ligados. Na maioria das placas rígidas 8x8, os LEDs seguem linha por linha no mesmo sentido (SERPENTINE = false). Os painéis flexíveis 16x16 costumam ser ligados em zigue-zague: uma linha vai da esquerda para a direita e a seguinte, da direita para a esquerda. Se a cobra pular na diagonal ou andar para trás em linhas alternadas, defina SERPENTINE como true; se as direções saírem invertidas, gire o joystick ou troque os limites 300 e 700. Para 16x16, basta definir W e H como 16; os arrays da cobra passam a ocupar 512 bytes, o buffer do NeoPixel 768 bytes, e cerca de 75% da RAM do Uno é usada.

Jogo da memória Simon: 4 LEDs e 4 botões
Simon (no Brasil, conhecido como Genius) é o lendário jogo da memória lançado em 1978: a cada rodada, o aparelho acrescenta uma cor à sequência e você repete a sequência inteira na mesma ordem. Componentes: quatro LEDs coloridos (verde, vermelho, amarelo, azul), um resistor de 220 Ω para cada um, quatro botões e um buzzer passivo. Ligue os LEDs no D9–D12, os botões no D2–D5 e o buzzer no D8; este jogo é um circuito separado do console comum. Você pode escolher o resistor certo para os seus LEDs com a calculadora Resistor para LED. Os quatro sons do Simon original são notas de um acorde de lá maior; o código usa as mesmas notas (330, 440, 554 e 659 Hz), então soa agradável mesmo quando você aperta 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}No Simon, a aleatoriedade é tudo: se a sequência for igual em toda partida, o jogador simplesmente a decora. O método randomSeed(analogRead(A0)), que lê um pino desconectado, é fraco na prática, porque o pino muitas vezes lê valores parecidos. Este código usa um truque mais robusto: o jogo espera o primeiro toque do jogador e usa o valor de micros() nesse momento como semente. Ninguém consegue repetir o momento de um toque com precisão de microssegundos, então cada partida começa diferente. O tempo de exibição fica 20 ms mais curto a cada nível e se estabiliza em 180 ms; se você não apertar em 3 segundos ou apertar a cor errada, o jogo termina, e o nível alcançado é gravado na EEPROM se for recorde.
Jogo de corrida no LCD: caracteres personalizados num display 16x2
Não dá para desenhar pixels num LCD de caracteres 16x2, mas com createChar() você pode definir até 8 caracteres personalizados de 5×8 pixels cada. Três bastam para o jogo de corrida: duas poses de corrida e um cacto. A linha de baixo do display é o chão; os cactos passam da direita para a esquerda e, quando você aperta o botão, o corredor sobe para a linha de cima e fica no ar por alguns passos. Como este jogo é em grade, a verificação de colisão é uma linha só: se houver um cacto na coluna do corredor enquanto ele está no chão, o jogo acaba. Para a biblioteca e as ligações, veja display 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}Rolar o mundo significa copiar cada elemento do array do chão uma posição para a esquerda; a coluna mais à direita recebe um novo cacto com 35% de chance. A regra MIN_GAP = 4 deixa pelo menos quatro colunas vazias entre dois cactos, para que todo obstáculo possa ser superado. A cada 25 pontos, o tempo do passo cai 15 ms, de 300 ms até 120 ms. Reescrever as duas linhas a cada passo, em vez de apagar o display com lcd.clear(), evita a cintilação; só o comando clear() leva cerca de 2 ms. Se você vê só blocos na tela, gire o trimpot de contraste atrás do módulo; se não vê nada, mude o endereço de 0x27 para 0x3F.
Teste de reação: meça seu tempo de reação em milissegundos
Este é o jogo mais simples e o mais viciante: um LED acende num momento aleatório, você aperta o botão o mais rápido que puder, e o Arduino mostra o tempo decorrido em milissegundos no Monitor Serial. Você só precisa de um LED no D7 com um resistor de 220 Ω, o botão no D3 e um buzzer opcional. O tempo de espera é aleatório, entre 2 e 5 segundos; se você apertar durante a espera, recebe um aviso de “cedo demais”, então não dá para trapacear chutando. O tempo é medido com micros(), sua média é calculada e seu melhor tempo é gravado na 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}O tempo médio de reação humana à luz é de cerca de 200–250 ms; um jogador bem concentrado consegue ficar um pouco abaixo disso. Se você definir USE_SOUND como true, um bipe toca junto com o LED. As reações ao som costumam ser um pouco mais rápidas do que à luz, então comparar os recordes dos dois modos rende um experimento interessante. Mostre o tempo num display de 7 segmentos ou num OLED em vez do Monitor Serial e você terá um aparelho a pilha que funciona sem computador.
Como fazer jogos com o ESP32
A maior parte do código deste guia roda no ESP32 com pequenas mudanças: as bibliotecas Adafruit SSD1306 e NeoPixel são compatíveis com o ESP32, e tone() está disponível nas versões 2.x e 3.x do core Arduino do ESP32. Três coisas precisam mudar: os pinos (a coluna ESP32 da tabela comum), os limites do joystick e a forma de gravar o recorde. O ADC do ESP32 é de 12 bits e retorna valores de 0 a 4095, então use cerca de 1200 e 2800 em vez de 300 e 700. Ligue o joystick nos pinos do ADC1, GPIO 32–39, porque os pinos do ADC2 não podem ser lidos com o Wi-Fi ligado.
O ESP32 não tem uma EEPROM de verdade; a biblioteca EEPROM a emula na flash e exige as chamadas EEPROM.begin() e EEPROM.commit(). O caminho mais limpo é a biblioteca Preferences: ela grava valores sob um namespace e um nome de chave na partição NVS da flash e distribui o desgaste sozinha. O jogo “corrida de toques” abaixo conta quantas vezes você consegue apertar o botão em 5 segundos e guarda o recorde com 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) retorna o segundo parâmetro se não houver nada gravado, então você nunca precisa lidar com o problema do 0xFFFF da EEPROM. Os nomes de namespace e de chave podem ter no máximo 15 caracteres. Embora o processador do ESP32 seja muitas vezes mais rápido que o do Uno, o OLED usa o mesmo barramento I2C, então a taxa de quadros quase não muda; para jogos bem mais fluidos e coloridos, você pode usar um display TFT ILI9341 com SPI. Já o Bluetooth do ESP32 é um bom ponto de partida para transformar o celular num controle de jogo.
Jogos com o Raspberry Pi Pico e MicroPython
Se você programa o Raspberry Pi Pico em MicroPython, a lógica do jogo é a mesma e só a sintaxe muda: time.ticks_ms() e time.ticks_diff() em vez de millis(), read_u16(), que retorna 0–65535, em vez de analogRead(), e PWM em vez de tone(). O Pico não tem EEPROM, mas graças ao sistema de arquivos do MicroPython é muito fácil gravar o recorde num pequeno arquivo de texto na flash. O código abaixo é a versão para Pico do teste de reação e guarda o recorde no arquivo rekor.txt; salve-o na placa como main.py no Thonny e ele inicia sozinho sempre que a placa é ligada. Se você é novo no MicroPython, leia primeiro O que é o 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)Usar ticks_diff() na medição de tempo é importante: os contadores dão a volta depois de um certo valor, e simplesmente subtrair dois valores dá um resultado errado nesse momento; ticks_diff() trata essa volta corretamente. Para jogos com OLED, o driver ssd1306 que você instala pelo gerenciador de pacotes do Thonny oferece os comandos fill_rect(), text() e show(), parecidos com os da Adafruit GFX. O módulo neopixel, necessário para uma matriz NeoPixel, já vem com o MicroPython. Assim, você pode levar o Pong e o Snake para o Pico com a mesma lógica.
Erros comuns e como corrigir
| Sintoma | Causa | Solução |
|---|---|---|
| Tela em branco | Se o Monitor Serial mostra “SSD1306 allocation failed”, a RAM acabou; se não, o endereço ou as ligações estão errados | Coloque os textos fixos dentro de F() e reduza os arrays grandes; sem a mensagem, verifique os fios SDA/SCL e tente o endereço 0x3D |
| O jogo começa sempre igual | Falta randomSeed() ou a semente é sempre a mesma | Use como semente o valor de micros() no primeiro toque do jogador |
| Um toque faz pular duas vezes | Repique do botão | Use a função buttonPressed(), que espera 15 ms de estabilidade |
| O recorde mostra 65535 | Uma EEPROM nunca gravada lê 0xFF | Trate 0xFFFF como 0 |
| O personagem se move sozinho com o joystick solto | O centro não é 512 e a zona morta é estreita demais | Mantenha os limites 300/700 ou meça o centro na inicialização e calibre |
| A cobra pula na diagonal | A matriz está ligada em zigue-zague | Defina SERPENTINE = true |
| O Arduino reinicia quando o brilho aumenta | A matriz de LED consome corrente demais | Reduza o setBrightness() e use uma fonte externa de 5 V |
| O PWM no pino 3 ou 11 falha quando tone() toca | No Uno, tone() usa o Timer2 | Passe o componente que precisa de PWM para o pino 5, 6, 9 ou 10 |
| O jogo fica lento com o tempo | Serial.print ou desenhos desnecessários a cada quadro | Remova a saída serial e mantenha o tempo de quadro acima do tempo de transferência do display |
| O LCD só mostra blocos | Ajuste de contraste ou endereço errado | Gire o trimpot atrás do módulo e tente o endereço 0x27 ou 0x3F |
Ideias para levar seus jogos mais longe
- Pong para dois jogadores: apague as linhas da CPU, controle a segunda raquete com um potenciômetro no A2 e leve os dois controles para pontas opostas da mesa com fios compridos.
- Controle por inclinação: use um MPU6050 em vez do joystick e guie a cobra ou a raquete inclinando a placa.
- Música e efeitos sonoros: toque a música do jogo com um DFPlayer Mini; para efeitos curtos, um buzzer passivo basta.
- Jogue pelo celular: transforme o celular num controle sem fio com um módulo Bluetooth HC-05.
- Um gamepad para o computador: como o Arduino Leonardo pode agir como teclado USB, você pode montar um controle para jogos de PC com um joystick e botões.
- Placar: mostre os recordes como texto rolante num módulo MAX7219 4 em 1.
- Jogos de sorte: monte uma roleta com um anel WS2812B ou um dado eletrônico com LEDs; o jeito mais divertido de praticar random().
Fazer jogos com Arduino é o jeito mais gostoso de aprender código e hardware ao mesmo tempo. Depois de entender o loop do jogo, a detecção de colisão e o recorde na EEPROM, criar o seu próprio jogo é só questão de ter uma ideia; para mais inspiração, veja a lista de ideias de projetos Arduino. No app BlueGrays, você encontra Pong, o T-Rex no OLED, o Snake em matriz 8x8 e 16x16, o Simon, a corrida no LCD e o teste de reação como projetos passo a passo, com esquemas do circuito e código pronto para usar.
Perguntas frequentes
Que jogos dá para fazer com um Arduino?
Com um Arduino Uno, você pode fazer Pong, a corrida do T-Rex, jogos no estilo Flappy Bird, Snake, jogos simples de blocos, o jogo da memória Simon, um teste de reação e jogos de sorte, como dado ou roleta. Um OLED de 128×64 basta para jogos com gráficos, e uma matriz de LED NeoPixel para jogos coloridos em grade. Jogos como o Simon e o teste de reação são jogados só com LEDs e botões, sem nenhum display.
Qual é o melhor display para um jogo com Arduino?
Para iniciantes, a melhor escolha é o OLED SSD1306 de 0,96 polegada: é barato, liga com apenas dois fios de dados e desenha gráficos em movimento em 128×64 pixels. Se você quer uma imagem colorida maior, um display TFT ILI9341 ou ST7789 serve, e para jogos de pixel art, uma matriz de LED 8x8 ou 16x16. Um LCD 16x2 com caracteres personalizados também basta para jogos de corrida simples.
Como gravar um recorde no Arduino?
Se você gravar o recorde na EEPROM, ele não se perde quando a alimentação é desligada. Você grava com EEPROM.put(endereço, pontuação) e lê com EEPROM.get(endereço, pontuação); o Arduino Uno tem 1024 bytes para isso. Como cada célula pode ser gravada cerca de 100.000 vezes, grave só quando o jogo terminar e o recorde tiver sido batido.
Dá para fazer um gamepad com Arduino?
Sim. Placas com o processador ATmega32U4, como o Arduino Leonardo, o Micro ou o Pro Micro, podem aparecer para o computador como teclado, mouse ou controle de jogo USB; elas leem o joystick e os botões e enviam as teclas pressionadas. O Arduino Uno não consegue fazer isso diretamente, mas pode enviar os valores do joystick pela porta serial para um programa no computador.
Por que meu jogo com Arduino está lento ou travando?
As causas mais comuns são loops feitos com delay(), chamadas de Serial.print a cada quadro e o tempo de transferência do display. No Uno, enviar a tela inteira do OLED leva cerca de 25 ms, então um tempo de quadro de 30 ms é realista. Use um tempo de quadro fixo com millis() em vez de delay(), remova a saída serial e desenhe só o necessário.
Qual é o jogo com Arduino mais fácil para iniciantes?
O teste de reação é o mais fácil: precisa de um LED, um botão e um sketch curto, e mesmo assim ensina ideias centrais como controle de tempo, números aleatórios e EEPROM. Depois, passar para o Simon com quatro LEDs e quatro botões e, em seguida, para o Pong e o T-Rex num display OLED é um bom caminho de aprendizado.
Projetos para montar depois desta leitura
Ver todos os projetos
Jogo de bola e raquetes - SSD1306
Uma bola, duas raquetes e você: jogue tênis de mesa, um dos primeiros videogames, em um pequeno display OLED. Dois botões movem sua raquete à direita para cima e para baixo, enquanto o computador cont

Corrida do T-Rex no OLED: jogo do dinossauro com Arduino
O jogo do dinossauro que aparece quando a internet cai, agora na sua mesa: o dinossauro corre pelo pequeno display OLED, os cactos vêm na sua direção e você pula com um único botão. Sua pontuação aume

Cobrinha em pixels: matriz 8x8
Monte o clássico jogo da cobrinha com as próprias mãos e jogue. Em uma matriz de luzes coloridas 8x8, você guia uma cobra verde com um joystick e come maçãs vermelhas; a cada maçã, a cobra cresce, ace

Cobrinha na matriz 16x16
O clássico jogo da cobrinha, desta vez em um enorme display LED com 256 luzes. Você guia a cobra verde com um joystick e coleta maçãs vermelhas; a cada maçã, a cobra cresce, sua pontuação aumenta em u

Jogo da memória com Arduino (4 LEDs + 4 botões + buzzer)
Você confia na sua memória? Quatro LEDs coloridos acendem em sequência, cada um tocando sua própria nota, e você repete a mesma ordem apertando os botões. A cada rodada correta, uma nova cor é adicion

Jogo de corrida no LCD com Arduino
Jogo de corrida infinita com rolagem lateral em um display LCD de 16x2 caracteres. O herói corre automaticamente, e o jogador pressiona o botão para pular os obstáculos. Obstáculos aleatórios aparecem nas linhas superior e inferior. O buzzer passivo toca a melodia de abertura, o som de pulo, o som de comemoração a cada 50 pontos e a melodia de fim de jogo. A tela de abertura "BlueGrays - Arduino Projects" aparece ao iniciar. A pontuação é atualizada em tempo real no canto superior direito.

Teste de reação com Arduino e aviso sonoro: meça seus reflexos em milissegundos (LCD + buzzer + 3 LEDs)
Seus reflexos são rápidos? Aperte o botão: o LED vermelho diz preparar, o amarelo diz apontar e começa uma espera aleatória de 1,5 a 3,5 segundos. Assim que o LED verde acende e o buzzer apita, aperte de novo; seu tempo de reação aparece em milissegundos no LCD I2C, com seu recorde da sessão e sua média embaixo. Apertou antes do verde? A tela mostra TOO EARLY! e toca um som de derrota. Segure o botão para um novo jogador com a mesma ordem de esperas.
Mais artigos
App BlueGrays
Monte este circuito passo a passo pelo celular
Esquema do circuito, código e lista de componentes em uma só tela. Mais de 200 projetos com Arduino, ESP32 e Pico prontos para montar.
Escaneie com seu celular
Aponte a câmera para o código para abrir a loja de aplicativos do seu celular.
ou abrir a loja

