Você está no 3DFinder
Buscamos em Thingiverse, MakerWorld e Printables ao mesmo tempo para te dar o melhor de cada uma.
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 Pin | Connected To |
|---|---|
| D22 | OLED SCL |
| D21 | OLED SDA |
| GND | OLED GND |
| 3V3 | OLED VDD |
| D32 | Joystick UP |
| D33 | Joystick DWN |
| D25 | Joystick LET |
| D26 | Joystick RHT |
| D27 | Joystick MID |
| D14 | Joystick SET |
| D12 | Joystick RST |
| GND | Joystick 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();
}