projeto
Mini Console com Arduino: Jogo da Cobra e Pedra-Papel-Tesoura num LCD
Uma mini consola de jogos portátil, construída à volta de um Arduino Uno, um ecrã LCD 16x2 com módulo I2C e um joystick analógico. A consola tem dois jogos completos — Jogo da Cobra e Pedra, Papel, Tesoura — navegáveis através de um menu principal, com som em quase todas as ações e gráficos personalizados desenhados pixel a pixel, apesar do ecrã ser um LCD de texto simples.
Componentes usados
- Arduino Uno
- Ecrã LCD 16x2 com módulo I2C (endereço
0x27) - Módulo joystick analógico (2 eixos + botão)
- Buzzer piezoelétrico
- Protoboard e jumpers
Visão geral: como a consola está organizada
O programa funciona com três "estados" principais, controlados por variáveis booleanas (inMenu,
inSnake, inRPS): o menu principal, o jogo da cobra, e o jogo de pedra-papel-tesoura.
O loop() principal do Arduino verifica em qual destes estados a consola está e chama a função
correspondente — handleMenu(), snakeLoop() ou rpsLoop(). É uma
máquina de estados simples, mas suficiente para organizar toda a lógica sem misturar os jogos entre si.
Jogo 1 — Jogo da Cobra
A cobra move-se automaticamente numa direção, controlada pelo joystick, e cresce cada vez que come uma "maçã" que aparece em posições aleatórias no ecrã. O jogo termina quando a cobra colide consigo própria.
O truque da resolução dupla no LCD
Um LCD de texto 16x2 mostra apenas caracteres, não pixels individuais — só tem 16 colunas por 2 linhas de
"casas" de texto. Mas cada caractere pode ser personalizado através de
lcd.createChar(), definindo um bitmap de 5x8 pixels para essa posição. O projeto usa isto a
seu favor: cria dois caracteres customizados, um que preenche só a metade superior da
célula (topHalf) e outro que preenche só a metade inferior (botHalf).
Isto permite tratar cada célula de texto como duas posições verticais independentes, duplicando a resolução
vertical efetiva de 2 linhas para 4 "linhas lógicas" (LOGICAL_ROWS = LCD_ROWS * 2) — dando à
cobra espaço suficiente para se mover com alguma fluidez, em vez de ficar limitada a saltar entre só duas
posições. A mesma técnica é aplicada à maçã (appleTop/appleBot), para que a
comida respeite a mesma grelha duplicada.
Como o corpo da cobra é gerido
A posição de cada segmento da cobra é guardada em dois arrays paralelos, snakeX[] e
snakeY[], com a posição da cabeça sempre no índice 0. A cada movimento (controlado por um
temporizador, moveInterval, que diminui ao longo do jogo para aumentar a dificuldade):
- Calcula-se a nova posição da cabeça, somando a direção atual (
dirX,dirY). - Se a cabeça sair de um lado do ecrã, reaparece do lado oposto (efeito "wraparound").
- Verifica-se se a nova posição da cabeça coincide com algum segmento existente do corpo — se sim, é
gameOver. - Apaga-se visualmente a última posição da cauda.
- Cada segmento do corpo "empurra-se" para a posição do segmento anterior (o corpo persegue a cabeça).
- Se a nova posição da cabeça coincidir com a maçã, a cobra cresce (
snakeLen++), a pontuação sobe, o intervalo de movimento diminui (fica mais rápido) e uma nova maçã é colocada numa posição aleatória que não colida com o corpo da cobra.
Leitura do joystick no jogo da cobra
O joystick é lido nos eixos X e Y em modo analógico (0 a 1023), com o centro de repouso perto de 512.
Para evitar mudanças de direção acidentais por pequenas variações do sensor, existe uma "zona morta"
(dead = 250) — só valores claramente afastados do centro contam como intenção real de
movimento. Um temporizador (lastInput) também impede leituras repetidas demasiado depressa,
tornando o controlo mais previsível. Há ainda uma regra de segurança: a cobra nunca pode inverter
diretamente a direção (por exemplo, ir de "direita" para "esquerda" instantaneamente), o que evitaria
que ela colidisse consigo própria de forma injusta.
Jogo 2 — Pedra, Papel, Tesoura
Um jogo simples contra o computador. O jogador navega entre as três opções ("PEDRA",
"PAPEL", "TESOURA") movendo o joystick para a esquerda ou direita no eixo X, e
confirma a escolha com o botão do joystick. Nesse momento, o Arduino gera uma escolha aleatória
(random(0, 3)) para representar a jogada do computador, e o resultado é calculado comparando
os índices das duas escolhas com a lógica clássica do jogo (pedra vence tesoura, tesoura vence papel, papel
vence pedra). O resultado — "EMPATE!", "VOCE GANHOU!" ou "VOCE PERDEU!" — é mostrado no ecrã, com música
de derrota a tocar apenas quando o jogador perde.
Sistema de som
Todo o som é gerado através da função nativa tone() do Arduino, associada a um único buzzer
piezoelétrico. O código define notas musicais como constantes de frequência (C4, D4,
E4, etc., em Hertz) e usa pequenas melodias de 2 a 3 notas para cada evento:
- Navegação no menu — uma nota curta e aguda a cada movimento.
- Início de jogo — uma melodia ascendente de três notas (Dó-Mi-Sol).
- Comer a maçã — duas notas curtas e satisfatórias.
- Perder — duas notas descendentes, tocadas apenas uma vez por jogo (controlado pela
variável
playedLoseMusic, para não repetir o som em cada frame enquanto o ecrã de "GAME OVER" está visível). - Seleção no Pedra-Papel-Tesoura — uma nota curta ao mudar de opção.
Menu principal
O menu é navegado no eixo Y do joystick — para cima ou para baixo alterna entre as duas opções ("Cobra"
e "Pedra/Papel/Tesoura"), com um ">" a indicar a opção selecionada no momento. Ao premir o botão do
joystick, a consola muda de estado (desliga inMenu, liga inSnake ou
inRPS) e inicializa o jogo escolhido — no caso da cobra, reiniciando posições e pontuação
(resetSnake()); no caso do Pedra-Papel-Tesoura, apenas desenhando o ecrã inicial
(drawRPS()).
Código completo (Arduino IDE)
Este é o código completo, pronto a copiar diretamente para o Arduino IDE:
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 16, 2);
// ------------------- JOYSTICK -------------------
const int JOY_X = A0;
const int JOY_Y = A1;
const int JOY_BTN = 7;
// ------------------- BUZZER -------------------
const int BUZZER = 11; // Pino do buzzer
// Notas musicais básicas
#define C4 262
#define D4 294
#define E4 330
#define F4 349
#define G4 392
#define A4 440
#define B4 494
#define C5 523
#define G3 196
// ------------------- MENU -------------------
int menuIndex = 0;
bool inMenu = true;
bool inSnake = false;
bool inRPS = false; // Pedra Papel Tesoura
// ------------------- JOGO DA COBRA -------------------
const int LCD_COLS = 16;
const int LCD_ROWS = 2;
const int LOGICAL_ROWS = LCD_ROWS * 2;
const int MAX_SNAKE = 64;
int snakeX[MAX_SNAKE];
int snakeY[MAX_SNAKE];
int snakeLen = 3;
int dirX = 1, dirY = 0;
unsigned long lastMove = 0;
unsigned long moveInterval = 200;
unsigned long lastInput = 0;
int foodX, foodY;
int score = 0;
bool gameOver = false;
bool playedLoseMusic = false; // controla para tocar música de perda apenas 1 vez
// ------------------- JOGO RPS -------------------
const char* choices[3] = {"PEDRA", "PAPEL", "TESOURA"};
int playerChoice = 0;
int computerChoice = 0;
bool rpsResultShown = false;
// ------------------- CUSTOM CHARS -------------------
byte topHalf[8] = {
B11111, B11111, B11111, B11111,
B00000, B00000, B00000, B00000
};
byte botHalf[8] = {
B00000, B00000, B00000, B00000,
B11111, B11111, B11111, B11111
};
// Metade da célula para a maçã (igual ao bloco da cobra)
byte appleTop[8] = {
B11111, B11111, B11111, B11111,
B00000, B00000, B00000, B00000
};
byte appleBot[8] = {
B00000, B00000, B00000, B00000,
B11111, B11111, B11111, B11111
};
// ----------------------------------------------------
// ==================== MÚSICAS =====================
void playMenuMove() {
tone(BUZZER, C5, 80); delay(80); noTone(BUZZER);
}
void playStartMusic() {
int melody[] = {C4, E4, G4};
int duration[] = {200, 200, 200};
for (int i = 0; i < 3; i++) {
tone(BUZZER, melody[i], duration[i]);
delay(duration[i]);
noTone(BUZZER);
}
}
void playEatApple() {
int melody[] = {G4, C5};
int duration[] = {100, 100};
for (int i = 0; i < 2; i++) {
tone(BUZZER, melody[i], duration[i]);
delay(duration[i]);
noTone(BUZZER);
}
}
void playLoseMusic() {
if (playedLoseMusic) return;
playedLoseMusic = true;
int melody[] = {C4, G3};
int duration[] = {300, 300};
for (int i = 0; i < 2; i++) {
tone(BUZZER, melody[i], duration[i]);
delay(duration[i]);
noTone(BUZZER);
}
}
void playRPSSelect() {
tone(BUZZER, E4, 80); delay(80); noTone(BUZZER);
}
// =====================================================
void setup() {
pinMode(JOY_BTN, INPUT_PULLUP);
pinMode(BUZZER, OUTPUT);
lcd.init();
lcd.backlight();
lcd.createChar(0, topHalf);
lcd.createChar(1, botHalf);
lcd.createChar(2, appleTop);
lcd.createChar(3, appleBot);
drawMenu();
playStartMusic();
}
// ====================================================
// ==================== LOOP ==========================
void loop() {
if (inMenu) handleMenu();
else if (inSnake) snakeLoop();
else if (inRPS) rpsLoop();
}
// ====================================================
// ==================== MENU ==========================
void drawMenu() {
lcd.clear();
lcd.setCursor(3, 0);
lcd.print(menuIndex == 0 ? "> Cobra" : " Cobra");
lcd.setCursor(3, 1);
lcd.print(menuIndex == 1 ? "> Pedra/Papel/Tes" : " Pedra/Papel/Tes");
}
void handleMenu() {
int y = analogRead(JOY_Y);
if (y < 300) { menuIndex = (menuIndex + 1) % 2; drawMenu(); playMenuMove(); delay(200);}
else if (y > 700) { menuIndex = (menuIndex == 0) ? 1 : 0; drawMenu(); playMenuMove(); delay(200);}
if (digitalRead(JOY_BTN) == LOW) {
delay(200);
playedLoseMusic = false;
playStartMusic();
if (menuIndex == 0) {
inMenu = false; inSnake = true;
resetSnake();
drawAll(true);
} else {
inMenu = false; inRPS = true;
drawRPS();
}
}
}
// ====================================================
// ==================== JOGO DA COBRA =================
void snakeLoop() {
if (gameOver) {
lcd.clear();
lcd.setCursor(4, 0);
lcd.print("GAME OVER");
lcd.setCursor(3, 1);
lcd.print("Pontos: ");
lcd.print(score);
playLoseMusic();
delay(500);
if (digitalRead(JOY_BTN) == LOW) {
inSnake = false;
inMenu = true;
drawMenu();
playedLoseMusic = false;
playStartMusic();
}
return;
}
readJoystickSnake();
unsigned long now = millis();
if (now - lastMove >= moveInterval) {
lastMove = now;
moveSnake();
}
}
void readJoystickSnake() {
unsigned long now = millis();
if (now - lastInput < 200) return;
int x = analogRead(JOY_X);
int y = analogRead(JOY_Y);
int center = 512;
int dead = 250;
if (x < center - dead && dirX != 1) { dirX = -1; dirY = 0; lastInput = now; }
else if (x > center + dead && dirX != -1) { dirX = 1; dirY = 0; lastInput = now; }
else if (y < center - dead && dirY != 1) { dirX = 0; dirY = -1; lastInput = now; }
else if (y > center + dead && dirY != -1) { dirX = 0; dirY = 1; lastInput = now; }
}
void moveSnake() {
int newX = snakeX[0] + dirX;
int newY = snakeY[0] + dirY;
if (newX < 0) newX = LCD_COLS - 1;
if (newX >= LCD_COLS) newX = 0;
if (newY < 0) newY = LOGICAL_ROWS - 1;
if (newY >= LOGICAL_ROWS) newY = 0;
for (int i = 0; i < snakeLen; i++) {
if (snakeX[i] == newX && snakeY[i] == newY) { gameOver = true; return; }
}
int tailX = snakeX[snakeLen - 1];
int tailY = snakeY[snakeLen - 1];
drawCell(tailX, tailY, ' ');
for (int i = snakeLen - 1; i > 0; i--) {
snakeX[i] = snakeX[i - 1];
snakeY[i] = snakeY[i - 1];
}
snakeX[0] = newX;
snakeY[0] = newY;
if (newX == foodX && newY == foodY) {
if (snakeLen < MAX_SNAKE) snakeLen++;
score++;
if (moveInterval > 60) moveInterval -= 8;
placeFood();
playEatApple();
}
drawCell(snakeX[0], snakeY[0], 'S');
drawCell(foodX, foodY, 'A');
}
void placeFood() {
bool ok = false;
while (!ok) {
ok = true;
foodX = random(0, LCD_COLS);
foodY = random(0, LOGICAL_ROWS);
for (int i = 0; i < snakeLen; i++) {
if (snakeX[i] == foodX && snakeY[i] == foodY) { ok = false; break; }
}
}
}
void drawAll(bool fullRedraw) {
if (fullRedraw) lcd.clear();
for (int i = 0; i < snakeLen; i++) drawCell(snakeX[i], snakeY[i], 'S');
drawCell(foodX, foodY, 'A');
}
void drawCell(int x, int y, char type) {
int lcdRow = y / 2;
int half = y % 2;
lcd.setCursor(x, lcdRow);
if (type == 'S') lcd.write(half == 0 ? byte(0) : byte(1));
else if (type == 'A') lcd.write(half == 0 ? byte(2) : byte(3));
else lcd.print(' ');
}
void resetSnake() {
snakeLen = 3;
int startX = LCD_COLS / 2;
int startY = LOGICAL_ROWS / 2;
for (int i = 0; i < snakeLen; i++) {
snakeX[i] = startX - i;
snakeY[i] = startY;
}
dirX = 1; dirY = 0;
moveInterval = 200;
score = 0;
gameOver = false;
playedLoseMusic = false;
placeFood();
}
// ====================================================
// ==================== PEDRA PAPEL TESOURA ============
void drawRPS() {
lcd.clear();
lcd.setCursor(3, 0);
lcd.print("Sua escolha:");
lcd.setCursor(3, 1);
lcd.print(choices[playerChoice]);
}
void rpsLoop() {
int x = analogRead(JOY_X);
if (x < 300) { playerChoice = (playerChoice + 2) % 3; drawRPS(); playRPSSelect(); delay(200);}
else if (x > 700) { playerChoice = (playerChoice + 1) % 3; drawRPS(); playRPSSelect(); delay(200);}
if (digitalRead(JOY_BTN) == LOW) {
delay(300);
if (!rpsResultShown) {
computerChoice = random(0, 3);
showRPSResult();
rpsResultShown = true;
} else {
inRPS = false;
inMenu = true;
rpsResultShown = false;
drawMenu();
playStartMusic();
}
}
}
void showRPSResult() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Voce: ");
lcd.print(choices[playerChoice]);
lcd.setCursor(0, 1);
lcd.print("CPU: ");
lcd.print(choices[computerChoice]);
delay(700);
lcd.clear();
lcd.setCursor(4, 0);
if (playerChoice == computerChoice) lcd.print("EMPATE!");
else if ((playerChoice == 0 && computerChoice == 2) ||
(playerChoice == 1 && computerChoice == 0) ||
(playerChoice == 2 && computerChoice == 1))
lcd.print("VOCE GANHOU!");
else {
lcd.print("VOCE PERDEU!");
playLoseMusic();
}
delay(1000);
lcd.setCursor(0, 1);
lcd.print("Btn p/ menu...");
}