Buscar
banner

Construa o BRINK GAMER: um console de jogos portátil ESP32

Publicado em 10 de Janeiro de 2026 às 17:18

BRINK GAMER é um console de jogos portátil "faça você mesmo" alimentado por um ESP32.

Este projeto combina eletrônica, programação e jogos em um dispositivo compacto e educativo.

O objetivo é demonstrar como o ESP32 pode ser usado para lidar com gráficos, botões e som, tornando-o ideal para projetos de jogos embarcados.

Materiais Necessários

  • Placa de desenvolvimento ESP32
  • Bateria de íon-lítio 18650 (3,7 V)
  • Módulo carregador de bateria 03962A
  • Conversor elevador de tensão (3,7 V para 5 V)
  • Display (LCD ST7567)
  • Push Button (para cima, para baixo, para a esquerda, para a direita, ação)
  • Interruptor Liga/Desliga
  • Fios de ligação
  • Placa perfurada (placa protótipo)
  • Placa de MDF (base tipo prancheta)
  • Cola instantânea
  • Tinta

Ferramentas Necessárias

  • Ferramentas:
  • Estilete (cortador)
  • Lixa

Etapas

Etapa 1: Por onde começar?

  • Antes de partir para a versão final do BRINK GAMER, o primeiro passo é montar todo o circuito em uma protoboard.
    Essa etapa é essencial para testar conexões, funcionamento do display, botões e alimentação, evitando erros quando o projeto for definitivo.

    Comece posicionando a placa ESP32 na protoboard e, em seguida, conecte os principais componentes que fazem parte do console.

    🔧 O que montar nesta etapa

    Conecte o ESP32 na protoboard

    Ligue o display gráfico ST7567 para testar imagem e comunicação

    Conecte os botões de controle (direções e ação)

    Monte o circuito de alimentação com bateria e carregador

    Utilize fios de ligação apenas para testes

    O objetivo aqui é validar o funcionamento eletrônico e lógico do projeto, antes de avançar para soldagem, caixa ou montagem final.
     
    Antes de partir para a versão final do BRINK GAMER, o primeiro passo é montar todo o circuito em uma protoboard.
Essa etapa é essencial para testar conexões, funcionamento do display, botões e alimentação, evitando erros quando o projeto for definitivo.

Comece posicionando a placa ESP32 na protoboard e, em seguida, conecte os principais componentes que fazem parte do console.

🔧 O que montar nesta etapa

Conecte o ESP32 na protoboard

Ligue o display gráfico ST7567 para testar imagem e comunicação

Conecte os botões de controle (direções e ação)

Monte o circuito de alimentação com bateria e carregador

Utilize fios de ligação apenas para testes

O objetivo aqui é validar o funcionamento eletrônico e lógico do projeto, antes de avançar para soldagem, caixa ou montagem final.
     

Etapa 2: Cortar e preparar a base de madeira

  • Com a parte eletrônica já testada na protoboard, é hora de pensar na estrutura física do BRINK GAMER.
    Nesta etapa, você irá escolher, medir, cortar e lixar as peças de madeira que formarão o corpo do console.

    Não existe um único padrão obrigatório: as dimensões podem ser adaptadas de acordo com suas preferências, levando em conta o tamanho do display, posição dos botões e conforto ao segurar o console.

    🪵 O que fazer nesta etapa

    Escolha o tipo de madeira que preferir (MDF, compensado, pinus, etc.)

    Defina o formato e tamanho do console

    Meça os espaços para:

    Display

    Botões

    Interruptor liga/desliga

    Acesso à carga da bateria

    Faça os cortes das peças

    Lixe bem todas as bordas para um melhor acabamento

    O ideal é montar tudo a seco, apenas encaixando as peças, para verificar se as medidas estão corretas antes da montagem final.
     
    Com a parte eletrônica já testada na protoboard, é hora de pensar na estrutura física do BRINK GAMER.
Nesta etapa, você irá escolher, medir, cortar e lixar as peças de madeira que formarão o corpo do console.

Não existe um único padrão obrigatório: as dimensões podem ser adaptadas de acordo com suas preferências, levando em conta o tamanho do display, posição dos botões e conforto ao segurar o console.

🪵 O que fazer nesta etapa

Escolha o tipo de madeira que preferir (MDF, compensado, pinus, etc.)

Defina o formato e tamanho do console

Meça os espaços para:

Display

Botões

Interruptor liga/desliga

Acesso à carga da bateria

Faça os cortes das peças

Lixe bem todas as bordas para um melhor acabamento

O ideal é montar tudo a seco, apenas encaixando as peças, para verificar se as medidas estão corretas antes da montagem final.
     

Etapa 3: Soldando os pinos de conexão na placa perfurada

  • Nesta etapa, o objetivo é preparar uma placa perfurada para facilitar a conexão e o teste do ESP32 e do display .

    Primeiro, solde pinos de conexão (fêmea ou macho, dependendo da sua configuração) na placa perfurada. Esses pinos permitirão que o ESP32 e o módulo de exibição sejam conectados e removidos facilmente durante os testes.

    Certifique-se de que os pinos estejam devidamente alinhados antes de soldar para garantir um encaixe perfeito e conexões confiáveis.

    Após a soldagem, coloque o ESP32 e o display nos conectores.

    Em seguida, conecte os fios do ESP32 ao display conforme mostrado na figura , certificando-se de que todas as conexões de sinal e energia estejam corretas.

    Após a fiação, realize testes básicos de conexão para confirmar se o visor está funcionando corretamente. Essa configuração ajuda a verificar a fiação e a funcionalidade antes de prosseguir para a montagem final.

    Essa abordagem facilita a depuração e evita danos aos componentes.
     
    Nesta etapa, o objetivo é preparar uma placa perfurada para facilitar a conexão e o teste do ESP32 e do display .

Primeiro, solde pinos de conexão (fêmea ou macho, dependendo da sua configuração) na placa perfurada. Esses pinos permitirão que o ESP32 e o módulo de exibição sejam conectados e removidos facilmente durante os testes.

Certifique-se de que os pinos estejam devidamente alinhados antes de soldar para garantir um encaixe perfeito e conexões confiáveis.

Após a soldagem, coloque o ESP32 e o display nos conectores.

Em seguida, conecte os fios do ESP32 ao display conforme mostrado na figura , certificando-se de que todas as conexões de sinal e energia estejam corretas.

Após a fiação, realize testes básicos de conexão para confirmar se o visor está funcionando corretamente. Essa configuração ajuda a verificar a fiação e a funcionalidade antes de prosseguir para a montagem final.

Essa abordagem facilita a depuração e evita danos aos componentes.
     

Etapa 4: Conexões e mapeamento de pinos

  • Nesta etapa, todas as conexões eletrônicas são explicadas com base na configuração de firmware utilizada no projeto.

    📟 Conexões de exibição (ST7567 – SPI)
    O visor gráfico comunica-se com o ESP32 usando SPI .

    Os seguintes pinos estão definidos no código:

    CS (Chip Select) → ESP32 GPIO 5
    DC (Dados/Comando) → GPIO 4 do ESP32
    RST (Reset) → ESP32 GPIO 15
    A comunicação SPI utiliza os pinos SPI de hardware do ESP32 internamente (MOSI e SCK), enquanto CS, DC e RST são definidos manualmente.

    Certifique-se de que o visor esteja alimentado de acordo com suas especificações (3,3 V ou 5 V, dependendo do módulo).

    🎮 Conexões de botões
    Todos os botões são configurados usando INPUT_PULLUP , o que significa que cada botão está conectado entre o pino do ESP32 e o GND .

    Botões direcionais:

    PARA CIMA → GPIO 25
    PARA BAIXO → GPIO 26
    ESQUERDA → GPIO 27
    DIREITA → GPIO 14
    Botões do menu:

    SELECIONAR → GPIO 32
    OK / INICIAR → GPIO 33
    Como os resistores de pull-up internos estão habilitados, não são necessários resistores externos.

    🔊 Conexão do Buzzer
    Sinal da campainha → GPIO 13
    Buzzer GND → GND
    A campainha é controlada usando a função tone() para gerar efeitos sonoros e música de inicialização.

    🔋 Sistema de energia
    A bateria 18650 (3,7 V) alimenta o sistema.
    A bateria está conectada ao módulo de carregamento 03962A.
    Um conversor elevador (3,7 V para 5 V) é usado para fornecer tensão estável.
    Um interruptor liga/desliga é colocado entre a bateria e o circuito.
    Essa configuração permite carregamento seguro e operação portátil.

    🧪 Verificações finais
    Antes de prosseguir para a etapa final:

    Verifique cuidadosamente todas as conexões.
    Certifique-se de que não haja curto-circuito.
    Teste de entrada de botões e inicialização da tela
    Assim que tudo estiver funcionando corretamente, você poderá prosseguir para a etapa final.

Etapa 5: Firmware e Código

  • #include <U8g2lib.h>
    #include <SPI.h>
    #include <Wire.h>

    // Arquivos.h
    #include "cobra.h"
    #include "pong.h"
    #include "shooter.h"
    #include "racer.h"
    #include "tetris.h"

    // ================== PINOS ==================
    #define PIN_CS 5
    #define PIN_DC 4
    #define PIN_RST 15

    #define PIN_BTN_UP 25
    #define PIN_BTN_DOWN 26
    #define PIN_BTN_LEFT 27
    #define PIN_BTN_RIGHT 14
    #define PIN_BTN_SEL 32
    #define PIN_BTN_OK 33
    #define PIN_BUZZER 13

    // ================== DISPLAY ==================
    U8G2_ST7567_JLX12864_F_4W_HW_SPI u8g2(
    U8G2_R0, PIN_CS, PIN_DC, PIN_RST
    );

    // ================== TELAS ==================
    enum Tela {
    TELA_START,
    TELA_MENU,
    TELA_COBRA,
    TELA_PONG,
    TELA_SHOOTER,
    TELA_RACER,
    TELA_TETRIS
    };

    Tela telaAtual = TELA_START;

    // ================== MENU ==================
    int jogoSelecionado = 0;
    const int TOTAL_JOGOS = 5;

    // ================== START CONTROLE ==================
    unsigned long startBlinkTimer = 0;
    bool startBlink = true;

    // 🎵 controle da música start
    unsigned long startMusicTimer = 0;
    int startMusicCount = 0;
    bool startMusicFinished = false;

    // ================== BUZZER ==================
    void beep(int f = 2000, int d = 60) {
    tone(PIN_BUZZER, f, d);
    }

    // ================== BOTÕES ==================
    void setupButtons() {
    pinMode(PIN_BTN_UP, INPUT_PULLUP);
    pinMode(PIN_BTN_DOWN, INPUT_PULLUP);
    pinMode(PIN_BTN_LEFT, INPUT_PULLUP);
    pinMode(PIN_BTN_RIGHT, INPUT_PULLUP);
    pinMode(PIN_BTN_SEL, INPUT_PULLUP);
    pinMode(PIN_BTN_OK, INPUT_PULLUP);
    }

    // ================== START MUSIC ==================
    void playStartSound() {
    tone(PIN_BUZZER, 1200, 120); delay(150);
    tone(PIN_BUZZER, 1600, 120); delay(150);
    tone(PIN_BUZZER, 2000, 200);
    }

    // ================== START SCREEN ==================
    void drawStartScreen() {

    if (millis() - startBlinkTimer > 400) {
    startBlinkTimer = millis();
    startBlink = !startBlink;
    }

    u8g2.clearBuffer();

    u8g2.setFont(u8g2_font_8x13B_tf);
    u8g2.drawStr(18, 16, "GAMER MODE");

    // ÍCONE CONTROLE
    u8g2.drawRFrame(28, 24, 72, 28, 6);
    u8g2.drawDisc(42, 38, 3);
    u8g2.drawDisc(86, 38, 3);
    u8g2.drawBox(60, 34, 6, 2);

    u8g2.setFont(u8g2_font_5x8_tf);
    if (startBlink)
    u8g2.drawStr(30, 62, "PRESS START");

    u8g2.sendBuffer();
    }

    // ================== ÍCONES MENU ==================
    void drawCobraIcon() {
    for (int i = 0; i < 5; i++)
    u8g2.drawBox(20 + i * 12, 32, 10, 10);
    u8g2.drawFrame(80, 32, 10, 10);
    }

    void drawPongIcon() {
    u8g2.drawBox(10, 26, 4, 24);
    u8g2.drawBox(114, 26, 4, 24);
    u8g2.drawDisc(62, 38, 3);
    }

    void drawShooterIcon() {
    u8g2.drawBox(56, 50, 16, 4);
    u8g2.drawBox(60, 46, 8, 4);
    u8g2.drawDisc(64, 40, 2);
    for (int i = 0; i < 4; i++)
    u8g2.drawFrame(20 + i * 20, 26, 12, 6);
    }

    void drawRacerIcon() {
    for (int i = 0; i < 64; i += 8) {
    u8g2.drawVLine(20, i, 4);
    u8g2.drawVLine(108, i, 4);
    }

    int px = 56, py = 50;
    u8g2.drawBox(px, py, 3, 3);
    u8g2.drawBox(px + 8, py, 3, 3);
    u8g2.drawBox(px + 4, py + 4, 3, 3);
    u8g2.drawBox(px, py + 8, 3, 3);
    u8g2.drawBox(px + 8, py + 8, 3, 3);
    }

    void drawTetrisIcon() {
    int x = 52, y = 26, s = 6;
    u8g2.drawBox(x, y, s, s);
    u8g2.drawBox(x + s + 2, y, s, s);
    u8g2.drawBox(x + s / 2 + 1, y + s + 2, s, s);
    u8g2.drawBox(x, y + 2 * s + 4, s, s);
    u8g2.drawBox(x + s + 2, y + 2 * s + 4, s, s);
    }

    // ================== MENU ==================
    void drawMenu() {
    u8g2.clearBuffer();
    u8g2.setFont(u8g2_font_6x12_tf);

    if (jogoSelecionado == 0) { u8g2.drawStr(30, 12, "COBRINHA"); drawCobraIcon(); }
    else if (jogoSelecionado == 1) { u8g2.drawStr(46, 12, "PONG"); drawPongIcon(); }
    else if (jogoSelecionado == 2) { u8g2.drawStr(26, 12, "SHOOTER"); drawShooterIcon(); }
    else if (jogoSelecionado == 3) { u8g2.drawStr(34, 12, "RACER"); drawRacerIcon(); }
    else if (jogoSelecionado == 4) { u8g2.drawStr(40, 12, "TETRIS"); drawTetrisIcon(); }

    u8g2.setFont(u8g2_font_5x8_tf);
    u8g2.drawStr(18, 62, "< SEL OK >");
    u8g2.sendBuffer();
    }

    void handleMenuButtons() {
    static unsigned long debounce = 0;
    if (millis() - debounce < 200) return;

    if (!digitalRead(PIN_BTN_SEL)) {
    jogoSelecionado = (jogoSelecionado + 1) % TOTAL_JOGOS;
    beep(1400);
    debounce = millis();
    }

    if (!digitalRead(PIN_BTN_OK)) {
    beep(2200, 100);

    if (jogoSelecionado == 0) { cobraReset(); telaAtual = TELA_COBRA; }
    else if (jogoSelecionado == 1) { pongReset(); telaAtual = TELA_PONG; }
    else if (jogoSelecionado == 2) { shooterReset(); telaAtual = TELA_SHOOTER; }
    else if (jogoSelecionado == 3) { racerReset(); telaAtual = TELA_RACER; }
    else if (jogoSelecionado == 4) { tetrisReset(); telaAtual = TELA_TETRIS; }

    debounce = millis();
    }
    }

    // ================== SETUP ==================
    void setup() {
    u8g2.begin();
    u8g2.setContrast(120);
    setupButtons();

    cobraInit(&u8g2, PIN_BUZZER);
    pongInit(&u8g2, PIN_BUZZER);
    shooterInit(&u8g2, PIN_BUZZER);
    racerInit(&u8g2, PIN_BUZZER);
    tetrisInit(&u8g2, PIN_BUZZER);
    }

    // ================== LOOP ==================
    void loop() {

    // ===== START =====
    if (telaAtual == TELA_START) {

    // 🎵 Música START 3x
    if (!startMusicFinished) {
    if (millis() - startMusicTimer > 700) {
    startMusicTimer = millis();
    playStartSound();
    startMusicCount++;

    if (startMusicCount >= 3) {
    startMusicFinished = true;
    }
    }
    }

    drawStartScreen();

    if (!digitalRead(PIN_BTN_OK)) {
    beep(2000, 120);
    delay(300);

    // reset música para próxima vez
    startMusicCount = 0;
    startMusicFinished = false;

    telaAtual = TELA_MENU;
    }
    return;
    }

    // ===== MENU =====
    if (telaAtual == TELA_MENU) {
    handleMenuButtons();
    drawMenu();
    }
    else if (telaAtual == TELA_COBRA) {
    cobraLoop();
    if (!digitalRead(PIN_BTN_SEL)) { beep(900); delay(300); telaAtual = TELA_MENU; }
    }
    else if (telaAtual == TELA_PONG) {
    pongLoop();
    if (!digitalRead(PIN_BTN_SEL)) { beep(900); delay(300); telaAtual = TELA_MENU; }
    }994955555555555
    else if (telaAtual == TELA_SHOOTER) {
    shooterLoop();
    if (!digitalRead(PIN_BTN_SEL)) { beep(900); delay(300); telaAtual = TELA_MENU; }
    }
    else if (telaAtual == TELA_RACER) {
    racerLoop();
    if (!digitalRead(PIN_BTN_SEL)) { beep(900); delay(300); telaAtual = TELA_MENU; }
    }
    else if (telaAtual == TELA_TETRIS) {
    tetrisLoop();
    if (!digitalRead(PIN_BTN_SEL)) { beep(900); delay(300); telaAtual = TELA_MENU; }
    }
    }

Etapa 6: Cobra.h

  • #ifndef COBRA_H
    #define COBRA_H

    // ================= CONFIG =================
    #define WIDTH 16
    #define HEIGHT 8
    #define BLOCK_SIZE 8

    enum Dir {UP, DOWN, LEFT, RIGHT, NONE};

    struct Point {
    int x;
    int y;
    };

    // ================= VARIÁVEIS =================
    U8G2 *display;
    int buzzerPin;

    Point snake[128];
    int snakeLength = 2;
    Point fruit;
    bool fruitExists = false;

    Dir direction = NONE;

    unsigned long lastMove = 0;
    int moveInterval = 300;
    int score = 0;
    bool gameStarted = false;

    // ================= BUZZER =================
    void cobraBeep(int freq=2000, int dur=80) {
    tone(buzzerPin, freq, dur);
    }

    // ================= INIT =================
    void cobraInit(U8G2 *u8, int buzzer) {
    display = u8;
    buzzerPin = buzzer;
    randomSeed(analogRead(0) + micros());
    }

    // ================= RESET =================
    void cobraReset() {
    snakeLength = 2;
    score = 0;
    direction = NONE;
    gameStarted = false;
    moveInterval = 300;

    snake[0] = {1, 0};
    snake[1] = {0, 0};

    fruitExists = false;
    }

    // ================= FRUTA =================
    void spawnFruit() {
    while (true) {
    fruit.x = random(0, WIDTH);
    fruit.y = random(0, HEIGHT);

    bool ok = true;
    for (int i = 0; i < snakeLength; i++) {
    if (snake[i].x == fruit.x && snake[i].y == fruit.y) {
    ok = false;
    break;
    }
    }

    if (ok) {
    fruitExists = true;
    return;
    }
    }
    }

    // ================= COLISÃO =================
    bool checkCollision(int x, int y) {
    if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return true;

    for (int i = 1; i < snakeLength; i++)
    if (snake[i].x == x && snake[i].y == y) return true;

    return false;
    }

    // ================= BOTÕES =================
    void cobraButtons() {
    static unsigned long debounce = 0;
    if (millis() - debounce < 150) return;

    if (digitalRead(25) == LOW && direction != DOWN) { direction = UP; gameStarted = true; debounce = millis(); }
    if (digitalRead(26) == LOW && direction != UP) { direction = DOWN; gameStarted = true; debounce = millis(); }
    if (digitalRead(27) == LOW && direction != RIGHT){ direction = LEFT; gameStarted = true; debounce = millis(); }
    if (digitalRead(14) == LOW && direction != LEFT) { direction = RIGHT; gameStarted = true; debounce = millis(); }
    }

    // ================= MOVIMENTO =================
    void moveSnake() {
    if (!gameStarted || direction == NONE) return;

    Point head = snake[0];

    if (direction == UP) head.y--;
    if (direction == DOWN) head.y++;
    if (direction == LEFT) head.x--;
    if (direction == RIGHT) head.x++;

    if (checkCollision(head.x, head.y)) {
    cobraBeep(400, 300);
    cobraReset();
    return;
    }

    for (int i = snakeLength; i > 0; i--)
    snake[i] = snake[i - 1];

    snake[0] = head;

    if (!fruitExists) spawnFruit();

    if (head.x == fruit.x && head.y == fruit.y) {
    snakeLength++;
    score++;
    fruitExists = false;
    cobraBeep(2500, 100);
    }
    }

    // ================= DRAW =================
    void drawSnake() {
    display->clearBuffer();
    display->setFont(u8g2_font_5x8_tf);

    char s[16];
    sprintf(s, "Score:%d", score);
    display->drawStr(0, 8, s);

    if (!gameStarted)
    display->drawStr(40, 8, "START");

    if (fruitExists)
    display->drawBox(fruit.x * BLOCK_SIZE, fruit.y * BLOCK_SIZE + 10, BLOCK_SIZE, BLOCK_SIZE);

    for (int i = 0; i < snakeLength; i++) {
    display->drawBox(
    snake[i].x * BLOCK_SIZE,
    snake[i].y * BLOCK_SIZE + 10,
    BLOCK_SIZE,
    BLOCK_SIZE
    );
    }

    display->sendBuffer();
    }

    // ================= LOOP =================
    void cobraLoop() {
    cobraButtons();

    if (millis() - lastMove > moveInterval) {
    lastMove = millis();
    moveSnake();
    drawSnake();
    }
    }

    #endif

Etapa 7: Tetris.h

  • #ifndef TETRIS_H
    #define TETRIS_H

    #include <U8g2lib.h>

    // ================= PROTÓTIPOS =================
    void tetrisInit(U8G2 *u8g2, int buzzerPin);
    void tetrisReset();
    void tetrisLoop();

    // ================= DISPLAY / BUZZER =================
    static U8G2 *tetrisDisplay;
    static int tetrisBuzzer;

    // ================= PINOS =================
    #define TETRIS_BTN_LEFT 27
    #define TETRIS_BTN_RIGHT 14
    #define TETRIS_BTN_ROTATE 25
    #define TETRIS_BTN_START 33

    // ================= DIMENSÕES =================
    #define TETRIS_CELL 4
    #define TETRIS_COLS 22
    #define TETRIS_ROWS 16
    #define TETRIS_OFFSET_X 20

    static byte tetrisGrid[TETRIS_ROWS][TETRIS_COLS];

    // ================= ESTADO =================
    enum TetrisState { TETRIS_START, TETRIS_PLAY, TETRIS_GAMEOVER };
    static TetrisState tetrisState = TETRIS_START;

    // ================= PEÇA =================
    struct TetrisPiece {
    int x, y;
    byte shape[4][4];
    };
    static TetrisPiece tetrisPiece;

    // ================= CONTROLE =================
    static unsigned long tetrisLastFall = 0;
    static unsigned long tetrisBlinkTimer = 0;
    static unsigned long tetrisMusicTimer = 0;
    static bool tetrisBlinkOn = true;
    static bool tetrisEsperandoSoltar = true;
    static bool tetrisRotSolto = true;
    static int tetrisPontos = 0;
    static int tetrisMusicIndex = 0;

    // ================= PEÇAS =================
    static const byte tetrisPieces[7][4][4] = {
    {{0,1,1,0},{0,1,1,0},{0,0,0,0},{0,0,0,0}},
    {{0,0,0,0},{1,1,1,1},{0,0,0,0},{0,0,0,0}},
    {{0,1,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}},
    {{1,0,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}},
    {{0,0,1,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}},
    {{0,1,1,0},{1,1,0,0},{0,0,0,0},{0,0,0,0}},
    {{1,1,0,0},{0,1,1,0},{0,0,0,0},{0,0,0,0}}
    };

    // ================= MÚSICA =================
    static const int tetrisStartMusic[][2] = {
    {900,80},{1200,80},{1500,120}
    };
    static const int tetrisStartMusicLen = 3;

    static const int tetrisGameOverMusic[][2] = {
    {800,120},{500,200}
    };
    static const int tetrisGameOverMusicLen = 2;

    // ================= SOM =================
    static void tetrisBeep(int f,int d){ tone(tetrisBuzzer,f,d); }

    // ================= ÍCONE =================
    void tetrisDrawIcon(int x,int y){
    int s=6;
    tetrisDisplay->drawBox(x,y,s,s);
    tetrisDisplay->drawBox(x+s+2,y,s,s);
    tetrisDisplay->drawBox(x+s/2+1,y+s+2,s,s);
    tetrisDisplay->drawBox(x,y+2*s+4,s,s);
    tetrisDisplay->drawBox(x+s+2,y+2*s+4,s,s);
    }

    // ================= DESENHO =================
    void tetrisDrawCell(int gx,int gy){
    tetrisDisplay->drawBox(
    TETRIS_OFFSET_X + gx*TETRIS_CELL,
    gy*TETRIS_CELL,
    TETRIS_CELL-1,
    TETRIS_CELL-1
    );
    }

    void tetrisDrawGrid(){
    for(int y=0;y<TETRIS_ROWS;y++)
    for(int x=0;x<TETRIS_COLS;x++)
    if(tetrisGrid[y][x]) tetrisDrawCell(x,y);
    }

    void tetrisDrawPiece(){
    for(int r=0;r<4;r++)
    for(int c=0;c<4;c++)
    if(tetrisPiece.shape[r][c]){
    int gx=tetrisPiece.x+c;
    int gy=tetrisPiece.y+r;
    if(gy>=0) tetrisDrawCell(gx,gy);
    }
    }

    void tetrisDrawBorders(){
    int l=TETRIS_OFFSET_X-1;
    int r=TETRIS_OFFSET_X+TETRIS_COLS*TETRIS_CELL;
    tetrisDisplay->drawVLine(l,0,64);
    tetrisDisplay->drawVLine(r,0,64);
    }

    // ================= COLISÃO =================
    bool tetrisCollision(int nx,int ny){
    for(int r=0;r<4;r++)
    for(int c=0;c<4;c++)
    if(tetrisPiece.shape[r][c]){
    int gx=nx+c, gy=ny+r;
    if(gx<0||gx>=TETRIS_COLS||gy>=TETRIS_ROWS) return true;
    if(gy>=0 && tetrisGrid[gy][gx]) return true;
    }
    return false;
    }

    // ================= ROTAÇÃO =================
    void tetrisRotatePiece(){
    byte tmp[4][4];
    for(int r=0;r<4;r++)
    for(int c=0;c<4;c++)
    tmp[c][3-r]=tetrisPiece.shape[r][c];

    for(int r=0;r<4;r++)
    for(int c=0;c<4;c++)
    if(tmp[r][c]){
    int gx=tetrisPiece.x+c, gy=tetrisPiece.y+r;
    if(gx<0||gx>=TETRIS_COLS||gy>=TETRIS_ROWS) return;
    if(gy>=0 && tetrisGrid[gy][gx]) return;
    }

    memcpy(tetrisPiece.shape,tmp,16);
    tetrisBeep(900,40);
    }

    // ================= FIXAR =================
    void tetrisFixPiece(){
    for(int r=0;r<4;r++)
    for(int c=0;c<4;c++)
    if(tetrisPiece.shape[r][c]){
    int gy=tetrisPiece.y+r;
    if(gy>=0) tetrisGrid[gy][tetrisPiece.x+c]=1;
    }
    }

    // ================= LIMPAR LINHAS =================
    void tetrisClearLines(){
    for(int y=TETRIS_ROWS-1;y>=0;y--){
    bool full=true;
    for(int x=0;x<TETRIS_COLS;x++) if(!tetrisGrid[y][x]) full=false;
    if(full){
    for(int yy=y;yy>0;yy--) memcpy(tetrisGrid[yy],tetrisGrid[yy-1],TETRIS_COLS);
    memset(tetrisGrid[0],0,TETRIS_COLS);
    tetrisPontos++;
    tetrisBeep(1200,80);
    y++;
    }
    }
    }

    // ================= NOVA PEÇA =================
    void tetrisNewPiece(){
    memcpy(tetrisPiece.shape,tetrisPieces[random(0,7)],16);
    tetrisPiece.x=(TETRIS_COLS/2)-2;
    tetrisPiece.y=-1;
    if(tetrisCollision(tetrisPiece.x,tetrisPiece.y))
    tetrisState=TETRIS_GAMEOVER;
    }

    // ================= INIT =================
    void tetrisInit(U8G2 *u8g2,int buzzerPin){
    tetrisDisplay=u8g2;
    tetrisBuzzer=buzzerPin;

    pinMode(TETRIS_BTN_LEFT,INPUT_PULLUP);
    pinMode(TETRIS_BTN_RIGHT,INPUT_PULLUP);
    pinMode(TETRIS_BTN_ROTATE,INPUT_PULLUP);
    pinMode(TETRIS_BTN_START,INPUT_PULLUP);

    randomSeed(millis());
    tetrisReset();
    }

    // ================= RESET =================
    void tetrisReset(){
    memset(tetrisGrid,0,sizeof(tetrisGrid));
    tetrisPontos=0;
    tetrisBlinkTimer=millis();
    tetrisMusicTimer=millis();
    tetrisMusicIndex=0;
    tetrisBlinkOn=true;
    tetrisEsperandoSoltar=true;
    tetrisState=TETRIS_START;
    tetrisNewPiece();
    }

    // ================= LOOP =================
    void tetrisLoop(){

    // ===== START =====
    if(tetrisState==TETRIS_START){

    if(millis()-tetrisBlinkTimer>400){
    tetrisBlinkTimer=millis();
    tetrisBlinkOn=!tetrisBlinkOn;
    }

    if(millis()-tetrisMusicTimer>300){
    tetrisMusicTimer=millis();
    tetrisBeep(
    tetrisStartMusic[tetrisMusicIndex][0],
    tetrisStartMusic[tetrisMusicIndex][1]
    );
    tetrisMusicIndex=(tetrisMusicIndex+1)%tetrisStartMusicLen;
    }

    if(tetrisEsperandoSoltar){
    if(digitalRead(TETRIS_BTN_START)==HIGH)
    tetrisEsperandoSoltar=false;
    } else if(digitalRead(TETRIS_BTN_START)==LOW){
    noTone(tetrisBuzzer);
    tetrisBeep(1000,150);
    tetrisState=TETRIS_PLAY;
    delay(200);
    }

    tetrisDisplay->clearBuffer();
    tetrisDisplay->setFont(u8g2_font_6x12_tf);
    tetrisDisplay->drawStr(42,12,"TETRIS");
    tetrisDrawIcon(54,18);
    if(tetrisBlinkOn)
    tetrisDisplay->drawStr(18,56,"PRESSIONE START");
    tetrisDisplay->sendBuffer();
    return;
    }

    // ===== PLAY =====
    if(tetrisState==TETRIS_PLAY){

    if(!digitalRead(TETRIS_BTN_LEFT)&&!tetrisCollision(tetrisPiece.x-1,tetrisPiece.y)){
    tetrisPiece.x--; delay(70);
    }
    if(!digitalRead(TETRIS_BTN_RIGHT)&&!tetrisCollision(tetrisPiece.x+1,tetrisPiece.y)){
    tetrisPiece.x++; delay(70);
    }

    if(tetrisRotSolto && !digitalRead(TETRIS_BTN_ROTATE)){
    tetrisRotatePiece(); tetrisRotSolto=false;
    }
    if(digitalRead(TETRIS_BTN_ROTATE)) tetrisRotSolto=true;

    if(millis()-tetrisLastFall>500){
    tetrisLastFall=millis();
    if(!tetrisCollision(tetrisPiece.x,tetrisPiece.y+1))
    tetrisPiece.y++;
    else{
    tetrisFixPiece();
    tetrisClearLines();
    tetrisNewPiece();
    }
    }

    tetrisDisplay->clearBuffer();
    tetrisDrawBorders();
    tetrisDisplay->setFont(u8g2_font_5x8_tf);
    tetrisDisplay->setCursor(0,8);
    tetrisDisplay->print("PTS:");
    tetrisDisplay->print(tetrisPontos);
    tetrisDrawGrid();
    tetrisDrawPiece();
    tetrisDisplay->sendBuffer();
    return;
    }

    // ===== GAME OVER =====
    if(tetrisState==TETRIS_GAMEOVER){

    static bool tocou=false;
    if(!tocou){
    for(int i=0;i<tetrisGameOverMusicLen;i++){
    tetrisBeep(tetrisGameOverMusic[i][0],tetrisGameOverMusic[i][1]);
    delay(220);
    }
    tocou=true;
    }

    if(millis()-tetrisBlinkTimer>400){
    tetrisBlinkTimer=millis();
    tetrisBlinkOn=!tetrisBlinkOn;
    }

    tetrisDisplay->clearBuffer();
    tetrisDisplay->setFont(u8g2_font_6x12_tf);
    tetrisDisplay->drawStr(32,22,"GAME OVER");
    if(tetrisBlinkOn)
    tetrisDisplay->drawStr(18,52,"PRESSIONE START");
    tetrisDisplay->sendBuffer();

    if(!digitalRead(TETRIS_BTN_START)){
    tocou=false;
    tetrisReset();
    delay(300);
    }
    }
    }

    #endif

Etapa 8: Pong.h

  • #ifndef PONG_H
    #define PONG_H

    #include <U8g2lib.h>

    // ================= PROTÓTIPOS =================
    void pongInit(U8G2 *u8g2, int buzzerPin);
    void pongReset();
    void pongLoop();

    // ================= DISPLAY / BUZZER =================
    static U8G2 *pongDisplay;
    static int pongBuzzer;

    // ================= PINOS =================
    #define PIN_BTN_LEFT 27
    #define PIN_BTN_RIGHT 14
    #define PIN_BTN_OK 33

    // ================= ESTADOS =================
    enum PongEstado {
    PONG_TELA_INICIO,
    PONG_JOGANDO,
    PONG_GAME_OVER
    };

    static PongEstado pongEstado = PONG_TELA_INICIO;

    // ================= VARIÁVEIS =================
    static int paddleX;
    static const int paddleW = 24;
    static const int paddleY = 58;

    static int ballX, ballY;
    static int ballDX, ballDY;

    static int pontos = 0;

    // ================= TEMPO =================
    static unsigned long lastBallMove = 0;
    static unsigned long lastPaddleMove = 0;
    static unsigned long blinkTimer = 0;
    static unsigned long musicTimer = 0;

    // ================= CONTROLE =================
    static bool blinkOn = true;
    static bool aguardandoSoltarBotao = true;
    static int musicIndex = 0;

    // ================= BEEP =================
    static void pongBeep(int f, int d) {
    tone(pongBuzzer, f, d);
    }

    static void pongStopSound() {
    noTone(pongBuzzer);
    }

    // ================= MUSIQUINHA =================
    static const int introMusic[][2] = {
    {1200, 120},
    {1500, 120},
    {1800, 120},
    {1500, 120}
    };
    static const int introMusicLen = 4;

    // ================= DESENHO CONTROLE =================
    static void drawGamerIcon(int x, int y) {
    // Corpo
    pongDisplay->drawRFrame(x, y, 40, 18, 4);

    // Direcional
    pongDisplay->drawBox(x + 6, y + 7, 6, 2);
    pongDisplay->drawBox(x + 8, y + 5, 2, 6);

    // Botões
    pongDisplay->drawDisc(x + 30, y + 7, 2);
    pongDisplay->drawDisc(x + 34, y + 10, 2);
    }

    // ================= INIT =================
    void pongInit(U8G2 *u8g2, int buzzerPin) {
    pongDisplay = u8g2;
    pongBuzzer = buzzerPin;
    pongReset();
    }

    // ================= RESET =================
    void pongReset() {
    paddleX = 52;
    ballX = 64;
    ballY = 40;
    ballDX = 1;
    ballDY = -1;

    pontos = 0;
    pongEstado = PONG_TELA_INICIO;

    aguardandoSoltarBotao = true;
    blinkTimer = millis();
    musicTimer = millis();
    musicIndex = 0;
    blinkOn = true;
    }

    // ================= LOOP =================
    void pongLoop() {

    // ======= TELA START =======
    if (pongEstado == PONG_TELA_INICIO) {

    if (millis() - blinkTimer > 400) {
    blinkTimer = millis();
    blinkOn = !blinkOn;
    }

    if (millis() - musicTimer > 200) {
    musicTimer = millis();
    pongBeep(introMusic[musicIndex][0], introMusic[musicIndex][1]);
    musicIndex++;
    if (musicIndex >= introMusicLen) musicIndex = 0;
    }

    if (aguardandoSoltarBotao) {
    if (digitalRead(PIN_BTN_OK) == HIGH)
    aguardandoSoltarBotao = false;
    } else {
    if (digitalRead(PIN_BTN_OK) == LOW) {
    pongStopSound();
    pongBeep(2500, 120);
    pongEstado = PONG_JOGANDO;
    delay(200);
    }
    }

    pongDisplay->clearBuffer();
    pongDisplay->setFont(u8g2_font_6x12_tf);

    pongDisplay->drawStr(42, 12, "PONG");

    drawGamerIcon(44, 18);

    if (blinkOn)
    pongDisplay->drawStr(18, 55, "PRESSIONE START");

    pongDisplay->sendBuffer();
    return;
    }

    // ======= GAME OVER =======
    if (pongEstado == PONG_GAME_OVER) {

    if (digitalRead(PIN_BTN_OK) == LOW) {
    pongReset();
    delay(300);
    }

    pongDisplay->clearBuffer();
    pongDisplay->setFont(u8g2_font_6x12_tf);

    pongDisplay->drawStr(28, 20, "GAME OVER");

    char txt[20];
    sprintf(txt, "PONTOS: %d", pontos);
    pongDisplay->drawStr(24, 38, txt);

    pongDisplay->drawStr(10, 58, "OK = REINICIAR");
    pongDisplay->sendBuffer();
    return;
    }

    // ======= JOGANDO =======
    if (millis() - lastPaddleMove > 20) {
    lastPaddleMove = millis();

    if (digitalRead(PIN_BTN_LEFT) == LOW && paddleX > 0)
    paddleX -= 1;

    if (digitalRead(PIN_BTN_RIGHT) == LOW && paddleX < 128 - paddleW)
    paddleX += 1;
    }

    if (millis() - lastBallMove > 35) {
    lastBallMove = millis();

    ballX += ballDX;
    ballY += ballDY;

    if (ballX <= 0 || ballX >= 125) {
    ballDX = -ballDX;
    pongBeep(1200, 20);
    }

    if (ballY <= 0) {
    ballDY = -ballDY;
    pongBeep(1200, 20);
    }

    if (ballY >= paddleY - 2 &&
    ballX >= paddleX &&
    ballX <= paddleX + paddleW) {
    ballDY = -ballDY;
    pontos++;
    pongBeep(2000, 30);
    }

    if (ballY > 63) {
    pongBeep(400, 300);
    pongEstado = PONG_GAME_OVER;
    delay(200);
    }
    }

    pongDisplay->clearBuffer();

    pongDisplay->drawBox(paddleX, paddleY, paddleW, 4);
    pongDisplay->drawBox(ballX, ballY, 3, 3);

    pongDisplay->setFont(u8g2_font_5x8_tf);
    char s[15];
    sprintf(s, "PONTOS:%d", pontos);
    pongDisplay->drawStr(2, 8, s);

    pongDisplay->sendBuffer();
    }

    #endif

Etapa 9: Shooter.h

  • #ifndef SHOOTER_H
    #define SHOOTER_H

    #include <U8g2lib.h>

    // ================= PROTÓTIPOS =================
    void shooterInit(U8G2 *u8g2, int buzzerPin);
    void shooterReset();
    void shooterLoop();

    // ================= DISPLAY / BUZZER =================
    static U8G2 *shooterDisplay;
    static int shooterBuzzer;

    // ================= PINOS =================
    #define PIN_BTN_LEFT 27
    #define PIN_BTN_RIGHT 14
    #define PIN_BTN_UP 25 // ATIRAR / START
    #define PIN_BTN_OK 33

    // ================= ESTADO DO JOGO =================
    enum ShooterState { SHOOTER_START, SHOOTER_PLAY, SHOOTER_GAMEOVER };
    static ShooterState shooterState = SHOOTER_START;

    // ================= NAVE =================
    static int naveX;
    static const int naveY = 54;
    static const int naveW = 20;
    static const int naveH = 6;

    // ================= TIROS =================
    #define MAX_TIROS 12
    struct Tiro { bool ativo; int x, y; };
    static Tiro tiros[MAX_TIROS];

    // ================= INIMIGOS =================
    #define MAX_INIMIGOS 5
    static int inimigoX[MAX_INIMIGOS];
    static int inimigoY[MAX_INIMIGOS];

    // ================= CONTROLE =================
    static int shooterPontos = 0;
    static unsigned long shooterLastMove = 0;
    static unsigned long shooterLastEnemy = 0;
    static unsigned long shooterLastShot = 0;
    static unsigned long shooterBlinkTimer = 0;
    static bool shooterBlinkOn = true;
    static bool shooterAguardandoSoltarBotao = true;
    static unsigned long shooterMusicTimer = 0;
    static int shooterMusicIndex = 0;

    // ================= MUSIQUINHA =================
    // Start: notas suaves e ascendentes
    static const int shooterIntroMusic[][2] = {
    {600,120}, {800,120}, {1000,120}, {1200,120}, {1400,150}
    };
    // Game Over: notas descendentes suaves
    static const int shooterGameOverMusic[][2] = {
    {800,150}, {700,150}, {600,150}, {500,200}, {400,250}
    };
    static const int shooterIntroMusicLen = 5;
    static const int shooterGameOverMusicLen = 5;

    // ================= SOM =================
    static void sBeep(int f,int d){tone(shooterBuzzer,f,d);}
    static void shooterStopSound(){noTone(shooterBuzzer);}

    // ================= DESENHOS =================
    static void drawShooterGamerIcon(int x,int y){
    shooterDisplay->drawRFrame(x,y,40,18,4);
    shooterDisplay->drawBox(x+6,y+7,6,2);
    shooterDisplay->drawBox(x+8,y+5,2,6);
    shooterDisplay->drawDisc(x+30,y+7,2);
    shooterDisplay->drawDisc(x+34,y+10,2);
    }

    static void drawNave(int x,int y){
    shooterDisplay->drawBox(x,y,naveW,naveH);
    shooterDisplay->drawBox(x+naveW/2-2,y-6,4,6);
    }

    // ===== Tiros grandes e lentos =====
    static void drawTiro(int x,int y){
    shooterDisplay->drawDisc(x,y,6);
    }

    static void drawInimigo(int x,int y){
    shooterDisplay->drawFrame(x,y,12,8);
    shooterDisplay->drawBox(x+2,y+2,8,3);
    }

    // ================= INIT =================
    void shooterInit(U8G2 *u8g2,int buzzerPin){
    shooterDisplay = u8g2;
    shooterBuzzer = buzzerPin;
    pinMode(PIN_BTN_LEFT,INPUT_PULLUP);
    pinMode(PIN_BTN_RIGHT,INPUT_PULLUP);
    pinMode(PIN_BTN_UP,INPUT_PULLUP);
    pinMode(PIN_BTN_OK,INPUT_PULLUP);
    shooterReset();
    }

    // ================= RESET =================
    void shooterReset(){
    shooterState = SHOOTER_START;
    naveX = (128-naveW)/2;
    shooterPontos = 0;
    for(int i=0;i<MAX_INIMIGOS;i++){
    inimigoX[i]=random(4,118);
    inimigoY[i]=random(-80,-10);
    }
    for(int i=0;i<MAX_TIROS;i++) tiros[i].ativo=false;
    shooterBlinkTimer=millis();
    shooterBlinkOn=true;
    shooterAguardandoSoltarBotao=true;
    shooterMusicTimer=millis();
    shooterMusicIndex=0;
    shooterLastShot=0;
    }

    // ================= LOOP =================
    void shooterLoop(){

    // ===== START =====
    if(shooterState==SHOOTER_START){
    if(millis()-shooterBlinkTimer>400){shooterBlinkTimer=millis();shooterBlinkOn=!shooterBlinkOn;}
    if(millis()-shooterMusicTimer>300){
    shooterMusicTimer=millis();
    sBeep(shooterIntroMusic[shooterMusicIndex][0],shooterIntroMusic[shooterMusicIndex][1]);
    shooterMusicIndex=(shooterMusicIndex+1)%shooterIntroMusicLen;
    }
    if(shooterAguardandoSoltarBotao){
    if(digitalRead(PIN_BTN_OK)==HIGH) shooterAguardandoSoltarBotao=false;
    } else if(digitalRead(PIN_BTN_OK)==LOW){
    shooterStopSound();
    sBeep(1000,150);
    shooterState=SHOOTER_PLAY;
    delay(200);
    }
    shooterDisplay->clearBuffer();
    shooterDisplay->setFont(u8g2_font_6x12_tf);
    shooterDisplay->drawStr(28,12,"SHOOTER");
    drawShooterGamerIcon(44,18);
    if(shooterBlinkOn) shooterDisplay->drawStr(18,55,"PRESSIONE START");
    shooterDisplay->sendBuffer();
    return;
    }

    // ===== PLAY =====
    if(shooterState==SHOOTER_PLAY){

    // MOVIMENTO NAVE
    if(millis()-shooterLastMove>15){shooterLastMove=millis();
    if(!digitalRead(PIN_BTN_LEFT) && naveX>0) naveX-=2;
    if(!digitalRead(PIN_BTN_RIGHT) && naveX<128-naveW) naveX+=2;
    }

    // DISPARO: ainda mais lento
    if(!digitalRead(PIN_BTN_UP) && millis()-shooterLastShot>400){
    for(int i=0;i<MAX_TIROS;i++){
    if(!tiros[i].ativo){
    tiros[i].ativo = true;
    tiros[i].x = naveX + naveW/2;
    tiros[i].y = naveY - 4;
    sBeep(2000,40);
    shooterLastShot = millis();
    break;
    }
    }
    }

    // MOVIMENTO TIROS: mais lento
    for(int i=0;i<MAX_TIROS;i++){
    if(tiros[i].ativo){
    tiros[i].y -= 1;
    if(tiros[i].y < 0) tiros[i].ativo = false;
    }
    }

    // INIMIGOS
    if(millis()-shooterLastEnemy>350){shooterLastEnemy=millis();
    for(int i=0;i<MAX_INIMIGOS;i++){
    inimigoY[i]+=2;
    if(inimigoY[i]>=naveY){sBeep(300,500); shooterState=SHOOTER_GAMEOVER;}
    }
    }

    // COLISÃO
    for(int i=0;i<MAX_INIMIGOS;i++){
    for(int j=0;j<MAX_TIROS;j++){
    if(tiros[j].ativo &&
    tiros[j].x>=inimigoX[i] &&
    tiros[j].x<=inimigoX[i]+12 &&
    tiros[j].y>=inimigoY[i] &&
    tiros[j].y<=inimigoY[i]+8){
    tiros[j].ativo=false;
    shooterPontos++;
    sBeep(1500,80);
    inimigoX[i]=random(4,118);
    inimigoY[i]=random(-80,-10);
    }
    }
    }

    // DESENHO
    shooterDisplay->clearBuffer();
    drawNave(naveX,naveY);
    for(int i=0;i<MAX_TIROS;i++) if(tiros[i].ativo) drawTiro(tiros[i].x,tiros[i].y);
    for(int i=0;i<MAX_INIMIGOS;i++) drawInimigo(inimigoX[i],inimigoY[i]);
    shooterDisplay->setFont(u8g2_font_5x8_tf);
    char txt[16]; sprintf(txt,"PONTOS:%d",shooterPontos);
    shooterDisplay->drawStr(2,8,txt);
    shooterDisplay->sendBuffer();
    return;
    }

    // ===== GAME OVER =====
    if(shooterState==SHOOTER_GAMEOVER){
    if(millis()-shooterBlinkTimer>400){shooterBlinkTimer=millis(); shooterBlinkOn=!shooterBlinkOn;}
    if(millis()-shooterMusicTimer>400){ // música Game Over
    shooterMusicTimer=millis();
    sBeep(shooterGameOverMusic[shooterMusicIndex][0], shooterGameOverMusic[shooterMusicIndex][1]);
    shooterMusicIndex=(shooterMusicIndex+1)%shooterGameOverMusicLen;
    }
    shooterDisplay->clearBuffer();
    shooterDisplay->setFont(u8g2_font_6x12_tf);
    shooterDisplay->drawStr(26,20,"GAME OVER");
    char txt[20]; sprintf(txt,"PONTOS:%d",shooterPontos);
    shooterDisplay->drawStr(28,38,txt);
    if(shooterBlinkOn) shooterDisplay->drawStr(12,58,"OK = REINICIAR");
    shooterDisplay->sendBuffer();
    if(!digitalRead(PIN_BTN_UP)){ shooterReset(); delay(300);}
    }

    }

    #endif

Etapa 10: Racer.h

  • #ifndef RACER_H
    #define RACER_H

    #include <U8g2lib.h>

    // ================= PROTÓTIPOS =================
    void racerInit(U8G2 *u8g2, int buzzerPin);
    void racerReset();
    void racerLoop();

    // ================= DISPLAY / BUZZER =================
    static U8G2 *racerDisplay;
    static int racerBuzzer;

    // ================= PINOS =================
    #define RACER_PIN_LEFT 27
    #define RACER_PIN_RIGHT 14
    #define RACER_PIN_OK 33 // START / REINICIAR

    // ================= ESTADO DO JOGO =================
    enum RacerState { RACER_START, RACER_PLAY, RACER_GAMEOVER };
    static RacerState racerState = RACER_START;

    // ================= JOGADOR =================
    static int racerPlayerX;
    static const int racerPlayerY = 50;
    static const int racerPlayerW = 11;
    static const int racerPlayerH = 11;

    // ================= INIMIGOS =================
    #define RACER_MAX_ENEMIES 4
    struct RacerEnemy { bool ativo; int x, y; int speed; };
    static RacerEnemy racerEnemies[RACER_MAX_ENEMIES];

    // ================= CONTROLE =================
    static int racerPontos = 0;
    static unsigned long racerLastMove = 0;
    static unsigned long racerLastEnemyFall = 0;
    static unsigned long racerBlinkTimer = 0;
    static bool racerBlinkOn = true;
    static bool racerEsperandoBotao = true;
    static unsigned long racerMusicTimer = 0;
    static int racerMusicIndex = 0;
    static unsigned long racerEngineTimer = 0;

    // ================= MUSIQUINHA =================
    static const int racerStartMusic[][2] = {{600,120},{800,120},{1000,120},{1200,150}};
    static const int racerGameOverMusic[][2] = {{800,150},{700,150},{600,150},{500,200}};
    static const int racerStartMusicLen = 4;
    static const int racerGameOverMusicLen = 4;

    // ================= SOM =================
    static void racerBeep(int f,int d){tone(racerBuzzer,f,d);}
    static void racerStopSound(){noTone(racerBuzzer);}
    static void racerEngineSound() {
    static bool toggle = false;
    int freq = toggle ? 700 : 900;
    toggle = !toggle;
    tone(racerBuzzer, freq, 50);
    }

    // ================= DESENHOS =================
    static void racerDrawPlayer(int x,int y){
    racerDisplay->drawBox(x, y, 3, 3);
    racerDisplay->drawBox(x + 8, y, 3, 3);
    racerDisplay->drawBox(x + 4, y + 4, 3, 3);
    racerDisplay->drawBox(x, y + 8, 3, 3);
    racerDisplay->drawBox(x + 8, y + 8, 3, 3);
    }

    static void racerDrawEnemy(int x,int y){
    racerDrawPlayer(x, y);
    }

    static void racerDrawTrack(){
    for(int i=0;i<64;i+=8){
    racerDisplay->drawVLine(20,i,4);
    racerDisplay->drawVLine(108,i,4);
    }
    }

    // ================= INIT =================
    void racerInit(U8G2 *u8g2,int buzzerPin){
    racerDisplay = u8g2;
    racerBuzzer = buzzerPin;
    pinMode(RACER_PIN_LEFT,INPUT_PULLUP);
    pinMode(RACER_PIN_RIGHT,INPUT_PULLUP);
    pinMode(RACER_PIN_OK,INPUT_PULLUP);
    racerReset();
    }

    // ================= RESET =================
    void racerReset(){
    racerState = RACER_START;
    racerPlayerX = 64;
    racerPontos = 0;
    for(int i=0;i<RACER_MAX_ENEMIES;i++){
    racerEnemies[i].ativo = false;
    racerEnemies[i].x = 0;
    racerEnemies[i].y = -30 * i; // espaçamento inicial
    racerEnemies[i].speed = random(2,5);
    }
    racerBlinkTimer = millis();
    racerBlinkOn = true;
    racerEsperandoBotao = true;
    racerMusicTimer = millis();
    racerMusicIndex = 0;
    racerLastMove = 0;
    racerLastEnemyFall = 0;
    racerEngineTimer = 0;
    }

    // ================= LOOP =================
    void racerLoop(){

    // ===== START =====
    if(racerState==RACER_START){
    if(millis()-racerBlinkTimer>400){racerBlinkTimer=millis(); racerBlinkOn=!racerBlinkOn;}
    if(millis()-racerMusicTimer>300){
    racerMusicTimer=millis();
    racerBeep(racerStartMusic[racerMusicIndex][0], racerStartMusic[racerMusicIndex][1]);
    racerMusicIndex=(racerMusicIndex+1)%racerStartMusicLen;
    }
    // aguarda soltar o botão
    if(racerEsperandoBotao){
    if(digitalRead(RACER_PIN_OK)==HIGH) racerEsperandoBotao=false;
    } else if(digitalRead(RACER_PIN_OK)==LOW){
    racerStopSound();
    racerBeep(1000,150);
    racerState=RACER_PLAY;
    delay(200);
    }

    // Desenho da tela de START
    racerDisplay->clearBuffer();
    racerDisplay->setFont(u8g2_font_6x12_tf);
    racerDisplay->drawStr(30,12,"RACER GAME");

    // ================= ÍCONE / CARRO MAIOR =================
    int iconeX = 54;
    int iconeY = 20;
    racerDisplay->drawBox(iconeX, iconeY, 6, 6); // topo esquerda
    racerDisplay->drawBox(iconeX + 12, iconeY, 6, 6); // topo direita
    racerDisplay->drawBox(iconeX + 6, iconeY + 6, 6, 6);// meio
    racerDisplay->drawBox(iconeX, iconeY + 12, 6, 6); // base esquerda
    racerDisplay->drawBox(iconeX + 12, iconeY + 12, 6, 6);// base direita

    // Mensagem piscando
    if(racerBlinkOn) racerDisplay->drawStr(18,55,"PRESSIONE START");
    racerDisplay->sendBuffer();
    return;
    }


    // ===== PLAY =====
    if(racerState==RACER_PLAY){

    // MOVIMENTO JOGADOR
    if(millis()-racerLastMove>15){ racerLastMove=millis();
    if(!digitalRead(RACER_PIN_LEFT) && racerPlayerX>24) racerPlayerX-=2;
    if(!digitalRead(RACER_PIN_RIGHT) && racerPlayerX<108-racerPlayerW) racerPlayerX+=2;
    }

    // INIMIGOS DESCENDO
    if(millis()-racerLastEnemyFall>100){ racerLastEnemyFall=millis();
    for(int i=0;i<RACER_MAX_ENEMIES;i++){
    if(racerEnemies[i].ativo){
    racerEnemies[i].y += racerEnemies[i].speed;
    if(racerEnemies[i].y >= 64){
    racerEnemies[i].ativo=false;
    racerPontos++;
    }
    // COLISÃO
    if(racerEnemies[i].y + 11 >= racerPlayerY && racerEnemies[i].y <= racerPlayerY + racerPlayerH &&
    racerEnemies[i].x + 11 >= racerPlayerX && racerEnemies[i].x <= racerPlayerX + racerPlayerW){
    racerState=RACER_GAMEOVER;
    racerBeep(300,500);
    }
    } else {
    // novo inimigo só aparece se houver espaço vertical livre
    bool podeSpawn = true;
    for(int j=0;j<RACER_MAX_ENEMIES;j++){
    if(racerEnemies[j].ativo && abs(racerEnemies[j].y)<20) podeSpawn=false;
    }
    if(podeSpawn){
    racerEnemies[i].ativo = true;
    racerEnemies[i].x = random(28, 94);
    racerEnemies[i].y = random(-50,-10);
    racerEnemies[i].speed = random(2,5);
    }
    }
    }
    }

    // SOM MOTOR
    if(millis() - racerEngineTimer > 60){
    racerEngineTimer = millis();
    racerEngineSound();
    }

    // DESENHO
    racerDisplay->clearBuffer();
    racerDrawTrack();
    racerDrawPlayer(racerPlayerX,racerPlayerY);
    for(int i=0;i<RACER_MAX_ENEMIES;i++) if(racerEnemies[i].ativo) racerDrawEnemy(racerEnemies[i].x,racerEnemies[i].y);
    racerDisplay->setFont(u8g2_font_5x8_tf);
    char txt[16]; sprintf(txt,"PONTOS:%d",racerPontos);
    racerDisplay->drawStr(2,8,txt);
    racerDisplay->sendBuffer();
    return;
    }

    // ===== GAME OVER =====
    if(racerState==RACER_GAMEOVER){
    if(millis()-racerBlinkTimer>400){racerBlinkTimer=millis(); racerBlinkOn=!racerBlinkOn;}
    if(millis()-racerMusicTimer>400){
    racerMusicTimer=millis();
    racerBeep(racerGameOverMusic[racerMusicIndex][0], racerGameOverMusic[racerMusicIndex][1]);
    racerMusicIndex=(racerMusicIndex+1)%racerGameOverMusicLen;
    }
    racerDisplay->clearBuffer();
    racerDisplay->setFont(u8g2_font_6x12_tf);
    racerDisplay->drawStr(26,20,"GAME OVER");
    char txt[20]; sprintf(txt,"PONTOS:%d",racerPontos);
    racerDisplay->drawStr(28,38,txt);
    if(racerBlinkOn) racerDisplay->drawStr(12,58,"OK = REINICIAR");
    racerDisplay->sendBuffer();
    if(!digitalRead(RACER_PIN_OK)) { racerReset(); delay(300);}
    }

    }

    #endif

Etapa 11: Spacetrasher.h

  • #ifndef SPACETRASH_H
    #define SPACETRASH_H

    #include <U8g2lib.h>
    #include <Arduino.h>

    // ================== CONFIGURAÇÕES ==================
    #define ST_OBJ_CNT 30
    #define ST_FP 4
    #define ST_AREA_HEIGHT 56
    #define ST_AREA_WIDTH 128

    // ================== ESTRUTURAS ==================
    typedef struct {
    uint8_t ot; // tipo do objeto
    int8_t tmp; // valor temporário
    int16_t x, y; // posição
    int8_t x0, y0, x1, y1; // bounding box
    } st_obj;

    // ================== VARIÁVEIS GLOBAIS ==================
    static U8G2 *st_u8g2 = nullptr;
    static uint8_t st_buzzerPin = 0;
    static uint8_t st_player_pos = 28;
    static uint16_t st_player_points = 0;
    static uint16_t st_player_points_delayed = 0;
    static uint16_t st_highscore = 0;
    static uint8_t st_state = 0;
    static uint8_t st_difficulty = 1;
    static uint16_t st_to_diff_cnt = 0;
    static st_obj st_objects[ST_OBJ_CNT];
    static uint8_t st_fire_player = 0;
    static uint8_t st_is_fire_last_value = 1;

    // Estados
    #define ST_STATE_PREPARE 0
    #define ST_STATE_IPREPARE 1
    #define ST_STATE_GAME 2
    #define ST_STATE_END 3
    #define ST_STATE_IEND 4

    // Tipos de objetos
    #define ST_OT_TRASH1 4
    #define ST_OT_PLAYER 5
    #define ST_OT_MISSLE 3
    #define ST_OT_BIG_TRASH 2
    #define ST_OT_TRASH2 9

    // Bitmaps
    static const uint8_t st_bitmap_player1[] = { 0x060, 0x0f8, 0x07e, 0x0f8, 0x060 };
    static const uint8_t st_bitmap_trash_5x5_1[] = { 0x070, 0x0f0, 0x0f8, 0x078, 0x030 };
    static const uint8_t st_bitmap_trash_5x5_2[] = { 0x030, 0x0f8, 0x0f8, 0x0f0, 0x070 };
    static const uint8_t st_bitmap_trash_7x7[] = { 0x038, 0x07c, 0x0fc, 0x0fe, 0x0fe, 0x07e, 0x078 };

    // ================== FUNÇÕES INTERNAS ==================
    static void st_beep(int f = 2000, int d = 60) {
    if(st_buzzerPin) tone(st_buzzerPin, f, d);
    }

    static void st_ClrObjs() {
    for(int i = 0; i < ST_OBJ_CNT; i++) {
    st_objects[i].ot = 0;
    }
    }

    static int8_t st_NewObj() {
    for(int i = 0; i < ST_OBJ_CNT; i++) {
    if(st_objects[i].ot == 0) return i;
    }
    return -1;
    }

    static void st_SetXY(st_obj *o, uint8_t x, uint8_t y) {
    o->x = ((int16_t)x) << ST_FP;
    o->y = ((int16_t)y) << ST_FP;
    }

    static void st_InitTrash(uint8_t x, uint8_t y) {
    int8_t objnr = st_NewObj();
    if(objnr < 0) return;
    st_obj *o = &st_objects[objnr];
    o->ot = (rand() & 1) ? ST_OT_TRASH1 : ST_OT_TRASH2;
    o->tmp = (rand() & 1) ? 1 : -1;
    st_SetXY(o, x, y);
    o->x0 = -3; o->x1 = 1;
    o->y0 = -2; o->y1 = 2;
    // 25% de chance de ser lixo grande em dificuldade alta
    if(st_difficulty >= 5 && (rand() & 3) == 0) {
    o->ot = ST_OT_BIG_TRASH;
    o->y0--; o->y1++;
    o->x0--; o->x1++;
    }
    }

    static void st_NewPlayerMissle(uint8_t x, uint8_t y) {
    int8_t objnr = st_NewObj();
    if(objnr < 0) return;
    st_obj *o = &st_objects[objnr];
    o->ot = ST_OT_MISSLE;
    st_SetXY(o, x, y);
    o->x0 = -4; o->x1 = 1;
    o->y0 = 0; o->y1 = 0;
    }

    static void st_NewPlayer() {
    int8_t objnr = st_NewObj();
    if(objnr < 0) return;
    st_obj *o = &st_objects[objnr];
    o->x = 6 << ST_FP;
    o->y = (ST_AREA_HEIGHT / 2) << ST_FP;
    o->x0 = -6; o->x1 = 0;
    o->y0 = -2; o->y1 = 2;
    o->ot = ST_OT_PLAYER;
    }

    static void st_FireStep(uint8_t is_fire) {
    if(st_fire_player < 20) {
    st_fire_player++;
    } else {
    if(st_is_fire_last_value && !is_fire) {
    st_fire_player = 0;
    }
    }
    st_is_fire_last_value = is_fire;
    }

    static void st_InitDeltaTrash() {
    // Contar lixo atual
    uint8_t trash_cnt = 0;
    uint8_t max_x = 0;
    for(int i = 0; i < ST_OBJ_CNT; i++) {
    uint8_t ot = st_objects[i].ot;
    if(ot == ST_OT_TRASH1 || ot == ST_OT_TRASH2 || ot == ST_OT_BIG_TRASH) {
    trash_cnt++;
    uint8_t x = st_objects[i].x >> ST_FP;
    if(x > max_x) max_x = x;
    }
    }
    // Criar novo lixo se necessário
    if(trash_cnt < 20 && max_x < (ST_AREA_WIDTH - 20)) {
    st_InitTrash(ST_AREA_WIDTH - 1, rand() % ST_AREA_HEIGHT);
    }
    }

    static void st_DrawBitmap(uint8_t x, uint8_t y, const uint8_t *bm, uint8_t w, uint8_t h) {
    uint8_t screen_y = st_u8g2->getHeight() - 1 - y;
    if(screen_y < h) return; // Ajuste para não desenhar fora da tela
    st_u8g2->drawBitmap(x, screen_y - h + 1, (w + 7) / 8, h, bm);
    }

    static void st_DrawObjects() {
    for(int i = 0; i < ST_OBJ_CNT; i++) {
    if(st_objects[i].ot == 0) continue;
    uint8_t x = st_objects[i].x >> ST_FP;
    uint8_t y = st_objects[i].y >> ST_FP;
    switch(st_objects[i].ot) {
    case ST_OT_PLAYER:
    st_DrawBitmap(x, y, st_bitmap_player1, 7, 5);
    break;
    case ST_OT_TRASH1:
    st_DrawBitmap(x, y, st_bitmap_trash_5x5_1, 5, 5);
    break;
    case ST_OT_TRASH2:
    st_DrawBitmap(x, y, st_bitmap_trash_5x5_2, 5, 5);
    break;
    case ST_OT_BIG_TRASH:
    st_DrawBitmap(x, y, st_bitmap_trash_7x7, 7, 7);
    break;
    case ST_OT_MISSLE:
    st_u8g2->drawBox(x, st_u8g2->getHeight() - 1 - y, 3, 1);
    break;
    }
    }
    }

    static void st_DrawHUD() {
    char buf[16];
    // Pontuação
    st_u8g2->setFont(u8g2_font_5x7_tf);
    sprintf(buf, "%d", st_player_points_delayed);
    st_u8g2->drawStr(ST_AREA_WIDTH - strlen(buf) * 5 - 2, 6, buf);
    // Dificuldade
    sprintf(buf, "Lvl:%d", st_difficulty);
    st_u8g2->drawStr(2, 6, buf);
    // Barra de progresso da dificuldade
    uint8_t progress = map(st_to_diff_cnt, 0, 1500, 0, ST_AREA_WIDTH - 40);
    st_u8g2->drawFrame(40, 0, ST_AREA_WIDTH - 40, 3);
    st_u8g2->drawBox(40, 0, progress, 3);
    }

    static void st_DrawStartScreen() {
    st_u8g2->setFont(u8g2_font_6x10_tf);
    st_u8g2->drawStr(30, 20, "SPACE TRASH");
    st_u8g2->setFont(u8g2_font_5x8_tf);
    st_u8g2->drawStr(25, 40, "PRESS FIRE");
    // Desenha nave do jogador
    st_DrawBitmap(60, 50, st_bitmap_player1, 7, 5);
    }

    static void st_DrawGameOver() {
    st_u8g2->setFont(u8g2_font_6x10_tf);
    st_u8g2->drawStr(30, 20, "GAME OVER");
    st_u8g2->setFont(u8g2_font_5x8_tf);
    char buf[32];
    sprintf(buf, "SCORE: %d", st_player_points);
    st_u8g2->drawStr(30, 35, buf);
    if(st_player_points > st_highscore) {
    st_highscore = st_player_points;
    st_u8g2->drawStr(25, 45, "NEW HIGHSCORE!");
    } else {
    sprintf(buf, "HIGH: %d", st_highscore);
    st_u8g2->drawStr(35, 45, buf);
    }
    st_u8g2->drawStr(20, 55, "FIRE TO RESTART");
    }

    // ================== FUNÇÕES PÚBLICAS ==================
    void spacetrashInit(U8G2 *display, uint8_t buzzerPin) {
    st_u8g2 = display;
    st_buzzerPin = buzzerPin;
    }

    void spacetrashReset() {
    st_player_points = 0;
    st_player_points_delayed = 0;
    st_difficulty = 1;
    st_to_diff_cnt = 0;
    st_ClrObjs();
    st_NewPlayer();
    st_state = ST_STATE_PREPARE;
    st_beep(1500, 100);
    }

    void spacetrashLoop() {
    if(!st_u8g2) return;
    // Limpar buffer
    st_u8g2->clearBuffer();
    // Desenhar de acordo com o estado
    switch(st_state) {
    case ST_STATE_PREPARE:
    st_DrawStartScreen();
    break;
    case ST_STATE_GAME:
    // Desenhar objetos
    st_DrawObjects();
    // Desenhar HUD
    st_DrawHUD();
    // Atualizar lógica do jogo
    for(int i = 0; i < ST_OBJ_CNT; i++) {
    if(st_objects[i].ot) {
    // Mover para esquerda
    st_objects[i].x -= (1 << ST_FP) / 8 + st_difficulty;
    // Remover se saiu da tela
    if((st_objects[i].x >> ST_FP) < -10) {
    st_objects[i].ot = 0;
    }
    }
    }
    // Gerar novo lixo
    if(rand() % (30 - st_difficulty) == 0) {
    st_InitDeltaTrash();
    }
    // Atualizar pontuação
    if(st_player_points_delayed < st_player_points) {
    st_player_points_delayed++;
    }
    // Aumentar dificuldade
    st_to_diff_cnt++;
    if(st_to_diff_cnt >= 1500) {
    st_to_diff_cnt = 0;
    st_difficulty++;
    st_player_points += 25;
    st_beep(1200, 50);
    }
    // Verificar colisões (simplificado)
    for(int i = 0; i < ST_OBJ_CNT; i++) {
    if(st_objects[i].ot >= ST_OT_TRASH1 && st_objects[i].ot <= ST_OT_BIG_TRASH) {
    uint8_t trash_x = st_objects[i].x >> ST_FP;
    uint8_t trash_y = st_objects[i].y >> ST_FP;
    // Colisão com jogador
    if(trash_x < 10 && trash_x > 0 &&
    abs(trash_y - st_player_pos) < 5) {
    st_state = ST_STATE_END;
    st_beep(300, 200);
    }
    // Colisão com mísseis
    for(int j = 0; j < ST_OBJ_CNT; j++) {
    if(st_objects[j].ot == ST_OT_MISSLE) {
    uint8_t missle_x = st_objects[j].x >> ST_FP;
    uint8_t missle_y = st_objects[j].y >> ST_FP;
    if(abs(missle_x - trash_x) < 5 && abs(missle_y - trash_y) < 5) {
    st_objects[i].ot = 0;
    st_objects[j].ot = 0;
    st_player_points += st_difficulty * 5;
    st_beep(1000, 30);
    }
    }
    }
    }
    }
    break;
    case ST_STATE_END:
    st_DrawGameOver();
    break;
    }
    // Enviar buffer para o display
    st_u8g2->sendBuffer();
    }

    // Função para ser chamada no loop principal com os botões
    void spacetrashHandleInput(uint8_t up, uint8_t down, uint8_t fire) {
    static uint8_t lastFire = 1;
    switch(st_state) {
    case ST_STATE_PREPARE:
    if(!fire && lastFire) {
    st_state = ST_STATE_GAME;
    spacetrashReset();
    st_beep(2000, 100);
    }
    break;
    case ST_STATE_GAME:
    // Movimento
    if(up && st_player_pos > 5) st_player_pos--;
    if(down && st_player_pos < ST_AREA_HEIGHT - 5) st_player_pos++;
    // Atualizar posição do jogador
    for(int i = 0; i < ST_OBJ_CNT; i++) {
    if(st_objects[i].ot == ST_OT_PLAYER) {
    st_objects[i].y = st_player_pos << ST_FP;
    break;
    }
    }
    // Tiro
    st_FireStep(fire);
    if(st_fire_player == 0) {
    for(int i = 0; i < ST_OBJ_CNT; i++) {
    if(st_objects[i].ot == ST_OT_PLAYER) {
    uint8_t x = (st_objects[i].x >> ST_FP) + 6;
    uint8_t y = st_objects[i].y >> ST_FP;
    st_NewPlayerMissle(x, y);
    st_beep(4000, 20);
    break;
    }
    }
    }
    break;
    case ST_STATE_END:
    if(!fire && lastFire) {
    st_state = ST_STATE_PREPARE;
    st_beep(1800, 100);
    }
    break;
    }
    lastFire = fire;
    }

    #endif

Comentários

0
Faça login para adicionar um comentário.
  1. Nenhum comentário ainda. Seja o primeiro a comentar!

Gostou do Projeto?