Ir para conteúdo
3DFinder
Entrar

Você está no 3DFinder

Buscamos em Thingiverse, MakerWorld e Printables ao mesmo tempo para te dar o melhor de cada uma.

Buscar mais como este
Modelo 3D Mini-Arcade por Dominik no MakerWorld

Descrição

Mini Arcade – The Ultimate Pocket Arcade

 

 

Play Tetris, Snake & Pong anywhere you go

 

What is the Mini-Arcade?

The Mini-Arcade is a tiny retro-inspired arcade console you can build yourself!
It runs on an ESP32 microcontroller, powered via USB-C, and features a 2.42” OLED display and a 5-way navigation switch with two additional buttons.

Once plugged in, you can play three classic games:

  • Snake – pass through the edges and only lose when hitting yourself
  • Tetris – includes all 6 standard blocks, rotate and drop to complete rows
  • Pong – fast-paced and unpredictable ball movement that gets harder over time

 

Wiring Instructions

ESP32 PinConnected To
D22OLED SCL
D21OLED SDA
GNDOLED GND
3V3OLED VDD
D32Joystick UP
D33Joystick DWN
D25Joystick LET
D26Joystick RHT
D27Joystick MID
D14Joystick SET
D12Joystick RST
GNDJoystick COM 

 

Assembly Notes

The whole setup is designed to be mounted on a single flat baseplate and printed using AMS.
You'll need just a few screws and a bit of superglue to hold everything together.

You don’t need to glue the bottom lid – it can simply be press-fitted and holds by itself. If you’d like to add a battery, that’s also possible. Just connect it to the GND and VIN pins. Make sure the battery only provides 5V. You can find suitable ones on Bambu Lab, too.

 

Some parts might not be displayed correctly here – just leave it in the original language and everything will show up properly.

 

 

Shopping List:

The items you need are all listed below and can be purchased on Amazon – just enter the item number on Amazon and you'll find it.

 

 

 

How the Games Work

  • Snake:
    The snake can go through one side and appear on the opposite side. You only lose if you hit yourself. Random food dots spawn as you play.
  • Tetris:
    All 6 classic Tetris blocks are included. Press the button to rotate, use left/right to move, and down to speed up the fall. Completed rows disappear automatically.
  • Pong:
    Control the paddle with up/down. The ball bounces at random angles and increases speed over time. Great reflexes are needed!

 

 

 

Want More Games?

Got an idea for a new mini game?
💬 Let me know in the comments or message me directly – I'd love to add more!

 

 

 

The Code

To get the Mini Arcade running on your ESP32 using the Arduino IDE, you’ll first need to set up the correct board and libraries. Start by opening the Arduino IDE and going to File > Preferences. In the field labeled "Additional Board Manager URLs", paste the following link:
[https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json](https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json)
Then click OK.

Next, open the Boards Manager by going to Tools > Board > Boards Manager. Search for ESP32, and install the package developed by Espressif Systems. Once installed, go to Tools > Board again and select your specific ESP32 model, such as ESP32 Dev Module or the one you're using for your Mini Arcade.

Now, you’ll need to install a few required libraries. Open the Library Manager by going to Tools > Manage Libraries. Search for and install the following libraries:

  • Adafruit GFX Library (by Adafruit)
  • Adafruit SSD1306 (also by Adafruit)

These libraries allow your ESP32 to communicate with the OLED display and draw graphics like text, lines, and shapes. The Wire.h library, which is needed for I2C communication, is already included in the ESP32 board package and doesn’t need to be installed separately.

 

👇 See the full code below:

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Buttons
#define BTN_UP   32
#define BTN_DWN  33
#define BTN_LFT  25
#define BTN_RHT  26
#define BTN_MID  27
#define BTN_SET  14
#define BTN_RST  12

// Menü
const char* menuItems[] = {"Snake", "Tetris", "Pong"};
const int menuLength = sizeof(menuItems) / sizeof(menuItems[0]);
int currentSelection = 0;
bool inGame = false;
int activeGame = -1;

// Spielbereich (für alle Spiele gleich)
#define SEG_SIZE 6
#define FIELD_X 2
#define FIELD_Y 2
#define FIELD_W 60
#define FIELD_H 120
#define GRID_WIDTH 10
#define GRID_HEIGHT 20

// Snake
#define MAX_SNAKE_LENGTH 100
struct Point { int x, y; };
Point snake[MAX_SNAKE_LENGTH];
int snakeLength = 5;
int dx = 0, dy = -1;
Point food;
unsigned long lastMove = 0;
unsigned long moveInterval = 300;

// Pong
int pongBallX = FIELD_X + FIELD_W / 2;
int pongBallY = FIELD_Y + FIELD_H / 2;
int pongBallDX = 1;
int pongBallDY = 1;
float pongSpeed = 1.0;
int pongPaddleY = FIELD_Y + FIELD_H / 2 - 10;

// Tetris (Variablen folgen in Teil 5/6)

void setup() {
  pinMode(BTN_UP, INPUT_PULLUP); pinMode(BTN_DWN, INPUT_PULLUP);
  pinMode(BTN_LFT, INPUT_PULLUP); pinMode(BTN_RHT, INPUT_PULLUP);
  pinMode(BTN_MID, INPUT_PULLUP); pinMode(BTN_SET, INPUT_PULLUP);
  pinMode(BTN_RST, INPUT_PULLUP);

  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.setRotation(3);
  display.clearDisplay();
  display.display();
  drawMenu();
}






// Teil 2




void loop() {
  if (inGame) {
    switch (activeGame) {
      case 0: // Snake
        handleSnakeInput();
        if (millis() - lastMove > moveInterval) {
          updateSnake();
          lastMove = millis();
        }
        drawSnakeGame();
        break;

      case 1: // Tetris
        loopTetris();  // Funktion kommt in Teil 6
        break;

      case 2: // Pong
        handlePongInput();
        updatePong();
        drawPongGame();
        break;
    }
  } else {
    handleMenuInput();
    drawMenu();
  }
}

// Menü-Logik
void handleMenuInput() {
  if (!digitalRead(BTN_UP))  { currentSelection = (currentSelection - 1 + menuLength) % menuLength; delay(150); }
  if (!digitalRead(BTN_DWN)) { currentSelection = (currentSelection + 1) % menuLength; delay(150); }

  if (!digitalRead(BTN_MID)) {
    if (currentSelection == 0) { startSnakeGame(); }
    else if (currentSelection == 1) { startTetrisGame(); }
    else if (currentSelection == 2) { startPongGame(); }
  }

  if (!digitalRead(BTN_RST)) currentSelection = 0;
}

// Menü anzeigen
void drawMenu() {
  display.clearDisplay();
  display.drawRect(FIELD_X - 1, FIELD_Y - 1, FIELD_W + 2, FIELD_H + 2, SSD1306_WHITE);

  display.fillRect(FIELD_X + 3, FIELD_Y + 4, 54, 20, SSD1306_WHITE);
  display.setTextColor(SSD1306_BLACK);
  display.setTextSize(1);
  display.setCursor(FIELD_X + 17, FIELD_Y + 6);  display.print("Mini");
  display.setCursor(FIELD_X + 13, FIELD_Y + 14); display.print("Arcade");

  display.setTextColor(SSD1306_WHITE);
  display.setCursor(FIELD_X + 3, FIELD_Y + 32);  display.print("DodoDomme");

  for (int i = 0; i < menuLength; i++) {
    int y = FIELD_Y + 48 + i * 12;
    display.setCursor((i == currentSelection) ? FIELD_X + 3 : FIELD_X + 12, y);
    if (i == currentSelection) display.print(">");
    display.print(menuItems[i]);
  }

  display.display();
}





// Teil 3



// Snake starten
void startSnakeGame() {
  inGame = true;
  activeGame = 0;
  snakeLength = 5;
  dx = 0; dy = -1;
  for (int i = 0; i < snakeLength; i++) {
    snake[i] = {GRID_WIDTH / 2, GRID_HEIGHT / 2 + i};
  }
  placeFood();
  lastMove = millis();
}

void handleSnakeInput() {
  if (!digitalRead(BTN_UP) && dy != 1)    { dx = 0; dy = -1; }
  if (!digitalRead(BTN_DWN) && dy != -1)  { dx = 0; dy = 1; }
  if (!digitalRead(BTN_LFT) && dx != 1)   { dx = -1; dy = 0; }
  if (!digitalRead(BTN_RHT) && dx != -1)  { dx = 1; dy = 0; }
  if (!digitalRead(BTN_RST)) { inGame = false; delay(200); }
}

void updateSnake() {
  Point head = snake[0];
  Point newHead = {
    (head.x + dx + GRID_WIDTH) % GRID_WIDTH,
    (head.y + dy + GRID_HEIGHT) % GRID_HEIGHT
  };

  for (int i = 0; i < snakeLength; i++) {
    if (snake[i].x == newHead.x && snake[i].y == newHead.y) {
      inGame = false;
      return;
    }
  }

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

  if (newHead.x == food.x && newHead.y == food.y) {
    snakeLength++;
    placeFood();
  } else if (snakeLength > MAX_SNAKE_LENGTH) {
    snakeLength = MAX_SNAKE_LENGTH;
  }
}

void placeFood() {
  bool valid;
  do {
    valid = true;
    food.x = random(GRID_WIDTH);
    food.y = random(GRID_HEIGHT);
    for (int i = 0; i < snakeLength; i++) {
      if (snake[i].x == food.x && snake[i].y == food.y) {
        valid = false;
        break;
      }
    }
  } while (!valid);
}

void drawSnakeGame() {
  display.clearDisplay();
  display.drawRect(FIELD_X - 1, FIELD_Y - 1, FIELD_W + 2, FIELD_H + 2, SSD1306_WHITE);

  for (int i = 0; i < snakeLength; i++) {
    int px = FIELD_X + snake[i].x * SEG_SIZE;
    int py = FIELD_Y + snake[i].y * SEG_SIZE;
    display.fillRect(px, py, SEG_SIZE, SEG_SIZE, SSD1306_WHITE);
  }

  int fx = FIELD_X + food.x * SEG_SIZE;
  int fy = FIELD_Y + food.y * SEG_SIZE;
  display.fillRect(fx, fy, SEG_SIZE, SEG_SIZE, SSD1306_WHITE);

  display.display();
}




// Teil 4




// ====================== TEIL 4 ======================
// Pong starten
void startPongGame() {
  inGame = true;
  activeGame = 2;
  pongBallX = FIELD_X + FIELD_W / 2;
  pongBallY = FIELD_Y + FIELD_H / 2;

  // Startrichtung leicht schräg
  pongBallDX = -1;
  pongBallDY = random(-1, 2); // -1, 0 oder 1

  if (pongBallDY == 0) pongBallDY = 1; // damit er nicht komplett waagrecht startet

  pongPaddleY = FIELD_Y + FIELD_H / 2 - 10;
  pongSpeed = 0.5;  // langsamer Start (~0.5 mm/s)
  lastMove = millis();
}

void handlePongInput() {
  if (!digitalRead(BTN_UP))   { pongPaddleY -= 2; }
  if (!digitalRead(BTN_DWN))  { pongPaddleY += 2; }
  if (!digitalRead(BTN_RST))  { inGame = false; delay(200); }

  // Begrenzung Paddle
  if (pongPaddleY < FIELD_Y) pongPaddleY = FIELD_Y;
  if (pongPaddleY + 20 > FIELD_Y + FIELD_H) pongPaddleY = FIELD_Y + FIELD_H - 20;
}

void updatePong() {
  pongBallX += pongBallDX * pongSpeed;
  pongBallY += pongBallDY * pongSpeed;

  // Oben oder unten abprallen
  if (pongBallY <= FIELD_Y) {
    pongBallY = FIELD_Y;
    pongBallDY = -pongBallDY;
  }
  if (pongBallY >= FIELD_Y + FIELD_H - 2) {
    pongBallY = FIELD_Y + FIELD_H - 2;
    pongBallDY = -pongBallDY;
  }

  // Paddle getroffen?
  if (pongBallX <= FIELD_X + 3 &&
      pongBallY >= pongPaddleY &&
      pongBallY <= pongPaddleY + 20) {
    pongBallX = FIELD_X + 3;
    pongBallDX = -pongBallDX;

    // Kleine Streuung für neue Richtung
    int bounceY = random(-1, 2);
    if (bounceY != 0) pongBallDY = bounceY;

    // sanfte Beschleunigung
    pongSpeed += 0.2;
    if (pongSpeed > 3.5) pongSpeed = 3.5;
  }

  // Rechts raus → nur abprallen
  if (pongBallX >= FIELD_X + FIELD_W - 2) {
    pongBallX = FIELD_X + FIELD_W - 2;
    pongBallDX = -pongBallDX;
  }

  // Links raus → Spiel neu starten
  if (pongBallX < FIELD_X) {
    startPongGame();
  }
}

void drawPongGame() {
  display.clearDisplay();
  display.drawRect(FIELD_X - 1, FIELD_Y - 1, FIELD_W + 2, FIELD_H + 2, SSD1306_WHITE);

  // Ball
  display.fillRect(pongBallX, pongBallY, 2, 2, SSD1306_WHITE);

  // Paddle
  display.fillRect(FIELD_X + 2, pongPaddleY, 2, 20, SSD1306_WHITE);

  display.display();
}







// Teil 5


// ===================== TEIL 5: TETRIS mit allen 7 Formen & Linien-Clearing =====================

#define TETRIS_COLS 10
#define TETRIS_ROWS 20
#define TETRIS_BLOCK 6
byte tetrisField[TETRIS_ROWS][TETRIS_COLS];

int tetrisX = 4, tetrisY = 0;
byte tetrisShape[4][4];
unsigned long lastTetrisMove = 0;
int tetrisInterval = 500;
bool tetrisGameOver = false;

// 7 klassische Tetris-Blöcke (I, O, T, L, J, S, Z)
const byte SHAPES[7][4][4] = {
  {{0,0,0,0},{1,1,1,1},{0,0,0,0},{0,0,0,0}}, // I
  {{0,1,1,0},{0,1,1,0},{0,0,0,0},{0,0,0,0}}, // O
  {{0,1,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}}, // T
  {{0,0,1,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}}, // L
  {{1,0,0,0},{1,1,1,0},{0,0,0,0},{0,0,0,0}}, // J
  {{0,1,1,0},{1,1,0,0},{0,0,0,0},{0,0,0,0}}, // S
  {{1,1,0,0},{0,1,1,0},{0,0,0,0},{0,0,0,0}}  // Z
};

void copyRandomShape() {
  int index = random(0, 7);
  memcpy(tetrisShape, SHAPES[index], sizeof(tetrisShape));
}

void startTetrisGame() {
  inGame = true;
  activeGame = 1;
  memset(tetrisField, 0, sizeof(tetrisField));
  copyRandomShape();
  tetrisX = 3; tetrisY = 0;
  tetrisGameOver = false;
  lastTetrisMove = millis();
}

void loopTetris() {
  handleTetrisInput();
  if (millis() - lastTetrisMove > tetrisInterval) {
    moveTetrisDown();
    lastTetrisMove = millis();
  }
  drawTetrisGame();
}

void handleTetrisInput() {
  if (!digitalRead(BTN_LFT)) { if (canMove(-1, 0)) tetrisX--; delay(150); }
  if (!digitalRead(BTN_RHT)) { if (canMove(1, 0)) tetrisX++; delay(150); }
  if (!digitalRead(BTN_DWN)) { moveTetrisDown(); delay(150); }
  if (!digitalRead(BTN_MID)) { rotateTetrisShape(); delay(150); }
  if (!digitalRead(BTN_RST)) { inGame = false; delay(200); }
}







bool canMove(int dx, int dy) {
  for (int y = 0; y < 4; y++) {
    for (int x = 0; x < 4; x++) {
      if (tetrisShape[y][x]) {
        int nx = tetrisX + x + dx;
        int ny = tetrisY + y + dy;
        if (nx < 0 || nx >= TETRIS_COLS || ny >= TETRIS_ROWS || (ny >= 0 && tetrisField[ny][nx])) {
          return false;
        }
      }
    }
  }
  return true;
}

void rotateTetrisShape() {
  byte temp[4][4];
  for (int y = 0; y < 4; y++) {
    for (int x = 0; x < 4; x++) {
      temp[x][3 - y] = tetrisShape[y][x];
    }
  }

  // Prüfen ob Rotation möglich ist
  byte backup[4][4];
  memcpy(backup, tetrisShape, sizeof(tetrisShape));
  memcpy(tetrisShape, temp, sizeof(tetrisShape));
  if (!canMove(0, 0)) {
    memcpy(tetrisShape, backup, sizeof(tetrisShape));
  }
}

void moveTetrisDown() {
  if (canMove(0, 1)) {
    tetrisY++;
  } else {
    // Block fixieren
    for (int y = 0; y < 4; y++) {
      for (int x = 0; x < 4; x++) {
        if (tetrisShape[y][x]) {
          int fx = tetrisX + x;
          int fy = tetrisY + y;
          if (fy < 0) {
            tetrisGameOver = true;
            inGame = false;
            return;
          }
          tetrisField[fy][fx] = 1;
        }
      }
    }

    clearFullLines();  // Volle Linien entfernen

    copyRandomShape();  // Neue Form
    tetrisX = 3;
    tetrisY = 0;
  }
}

void clearFullLines() {
  for (int y = TETRIS_ROWS - 1; y >= 0; y--) {
    bool full = true;
    for (int x = 0; x < TETRIS_COLS; x++) {
      if (!tetrisField[y][x]) {
        full = false;
        break;
      }
    }

    if (full) {
      for (int row = y; row > 0; row--) {
        for (int col = 0; col < TETRIS_COLS; col++) {
          tetrisField[row][col] = tetrisField[row - 1][col];
        }
      }
      for (int col = 0; col < TETRIS_COLS; col++) {
        tetrisField[0][col] = 0;
      }
      y++;  // nochmal prüfen, weil alles eine Zeile runtergerutscht ist
    }
  }
}

void drawTetrisGame() {
  display.clearDisplay();
  display.drawRect(FIELD_X - 1, FIELD_Y - 1, FIELD_W + 2, FIELD_H + 2, SSD1306_WHITE);

  // Feld zeichnen
  for (int y = 0; y < TETRIS_ROWS; y++) {
    for (int x = 0; x < TETRIS_COLS; x++) {
      if (tetrisField[y][x]) {
        int px = FIELD_X + x * TETRIS_BLOCK;
        int py = FIELD_Y + y * TETRIS_BLOCK;
        display.fillRect(px, py, TETRIS_BLOCK, TETRIS_BLOCK, SSD1306_WHITE);
      }
    }
  }

  // Aktuelle Form zeichnen
  for (int y = 0; y < 4; y++) {
    for (int x = 0; x < 4; x++) {
      if (tetrisShape[y][x]) {
        int px = FIELD_X + (tetrisX + x) * TETRIS_BLOCK;
        int py = FIELD_Y + (tetrisY + y) * TETRIS_BLOCK;
        if (py >= FIELD_Y) {
          display.fillRect(px, py, TETRIS_BLOCK, TETRIS_BLOCK, SSD1306_WHITE);
        }
      }
    }
  }

  display.display();
}

 

MakerWorld

Mini-Arcade

Publicado em 17 de jun de 2025

1,042
Curtidas
221
Downloads
1,986
Coleções
56
Impressões
Categoria Other Toys & Games
Tags
arcade MINI gift haus deko deco Game
Licença Standard Digital File License
Ver no MakerWorld (abre em nova aba)