Community project

Gacha Pet Legendaris

ESP32
Photo of Gacha Pet Legendaris
Generated with AI

D.P Utama

Published October 11, 2026

Gacha Pet Legendaris is a virtual pet game built around an ESP32 microcontroller and a small OLED display. Players collect legendary creatures like dragons, phoenixes, and unicorns through a gacha system, then care for their pets by managing hunger, happiness, and energy levels. The game features multiple modes including pet care, gacha pulls, adventures, and status tracking, all controlled with three push buttons.

This guide provides a complete wiring diagram, parts list, and step-by-step assembly instructions for building a portable handheld device powered by a rechargeable LiPo battery. The included firmware handles all game logic, display rendering, button input with debouncing, and stat decay over time. Once assembled and programmed, the device is ready to play immediately—no additional setup required.

Wiring diagram

Wiring diagram for Gacha Pet Legendaris

Gather all the parts

QtyComponent
1

SSD1306 OLED

0.96 inch 128x64 OLED display with I2C interface

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

LiPo 3.7V 1000mAh Battery

Single-cell LiPo pack, nominal 3.7 V, 1000 mAh. Default rechargeable choice for portable ESP32 / Pico projects. Pair with a TP4056 charger for safe USB recharging.

1

TP4056 Li-Ion/LiPo charger module with protection

TP4056 single-cell Li-Ion/LiPo linear charger module, 5V USB input, 1A charge current (programmable). Common variants ship with DW01 protection. Pair with battery_lipo_storage for the cell.

1

3.3v Buck Boost

TI TPS63030/TPS63031 high-efficiency single-inductor buck-boost converter family with 1A switches. Used to hold a regulated rail when battery voltage crosses above and below the target output.

Assemble it in 5 steps

1. Siapkan papan dan layar

Letakkan ESP32 DevKit v1 dan layar OLED di atas meja dengan tulisan pin terlihat. Hubungkan VCC OLED ke pin 3V3 ESP32 (power), GND OLED ke GND ESP32 (ground), SDA OLED ke GPIO21 (data), dan SCL OLED ke GPIO22 (data).

  • Gunakan kabel pendek agar gambar pada layar stabil.
  • Pastikan VCC dan GND tidak tertukar — power yang terbalik dapat merusak layar.

2. Pasang tiga tombol permainan

Setiap tombol memiliki dua sambungan: satu kaki ke GND ESP32 (ground) dan kaki seberangnya ke pin sinyal. Tombol atas: SIGNAL ke GPIO32 (memilih ke atas). Tombol bawah: SIGNAL ke GPIO33 (memilih ke bawah). Tombol tengah: SIGNAL ke GPIO27 (memilih atau menjalankan menu).

  • Jika memakai tombol taktil berkaki empat, gunakan dua kaki yang saling berseberangan; dua kaki pada sisi yang sama sudah tersambung di dalam tombol.
  • Jangan menghubungkan satu tombol langsung dari 3V3 ke pin sinyal, karena program memakai sambungan tombol ke GND.

3. Hubungkan baterai ke pengisi daya

Hubungkan kabel positif baterai LiPo ke B+ pada modul TP4056 (power), lalu kabel negatif baterai ke B- (ground). Sambungkan port USB 5V khusus pengisian ke IN+ dan IN- pada TP4056 sesuai tanda positif dan negatifnya.

  • Gunakan modul TP4056 enam-pad yang memiliki B+, B-, OUT+, dan OUT- agar baterai memiliki perlindungan dasar.
  • Jangan pernah menukar kabel baterai — baterai lithium dapat panas, rusak, atau berbahaya bila polaritasnya salah.

4. Atur listrik 3,3 volt untuk ESP32

Hubungkan OUT+ TP4056 ke VIN regulator 3,3V (power), dan OUT- TP4056 ke GND regulator (ground). Hubungkan VOUT regulator ke pin 3V3 ESP32 (power), lalu GND regulator ke GND ESP32 (ground).

  • Atur dan ukur keluaran regulator menjadi 3,3V sebelum menghubungkannya ke ESP32 jika modul regulator Anda dapat diatur.
  • Jangan sambungkan keluaran regulator yang belum dipastikan 3,3V ke pin 3V3 ESP32 — tegangan terlalu tinggi dapat merusak papan.

5. Nyalakan dan mainkan

Setelah semua sambungan diperiksa, nyalakan rangkaian dari baterai. Layar menampilkan naga awal dan empat menu singkat: Rawat, Gacha, Petualangan, dan Status. Tekan tombol atas atau bawah untuk berpindah, lalu tombol tengah untuk memilih.

  • Rawat memberi makan dengan 5 koin, gacha memakai 30 koin untuk mencoba mendapatkan hewan legendaris, dan petualangan memberi koin jika hewan tidak lapar atau lelah.

Review all connections

1. Connections between "oled_1" and "ESP32"

Functionoled_1ESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22

2. Connections between "button_up" and "ESP32"

Functionbutton_upESP32
groundGNDGND
digitalSIGNALGPIO 32

3. Connections between "button_down" and "ESP32"

Functionbutton_downESP32
groundGNDGND
digitalSIGNALGPIO 33

4. Connections between "button_select" and "ESP32"

Functionbutton_selectESP32
groundGNDGND
digitalSIGNALGPIO 27

5. Connections between "lipo_1" and "ESP32"

Functionlipo_1ESP32
power+V → TP4056 Li-Ion/LiPo charger module with protection B+EXT
groundGND → TP4056 Li-Ion/LiPo charger module with protection B-EXT

6. Connections between "charger_1" and "ESP32"

Functioncharger_1ESP32
powerIN+ → 5 V USB charging cable positiveEXT
groundIN- → 5 V USB charging cable groundEXT
powerOUT+ → 3.3v Buck Boost VINEXT
groundOUT- → 3.3v Buck Boost GNDEXT

7. Connections between "regulator_1" and "ESP32"

Functionregulator_1ESP32
powerVOUT3V3
groundGNDGND

Deploy the firmware

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

#define OLED_SDA 21
#define OLED_SCL 22
#define BUTTON_UP 32
#define BUTTON_DOWN 33
#define BUTTON_SELECT 27

constexpr int SCREEN_WIDTH = 128;
constexpr int SCREEN_HEIGHT = 64;
constexpr uint8_t OLED_ADDRESS = 0x3C;
constexpr unsigned long DEBOUNCE_MS = 180;
constexpr unsigned long DECAY_INTERVAL_MS = 60000;

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

enum PetType { DRAGON, PHOENIX, UNICORN };
const char *petNames[] = {"Naga", "Phoenix", "Unicorn"};
const char *menuItems[] = {"Rawat", "Gacha", "Petualangan", "Status"};

PetType pet = DRAGON;
uint8_t petLevel = 1;
int hunger = 65;
int happiness = 65;
int energy = 75;
int coins = 100;
uint8_t menuIndex = 0;
bool messageMode = false;
String messageLine1;
String messageLine2;
unsigned long messageUntil = 0;
unsigned long lastInput = 0;
unsigned long lastDecay = 0;

bool pressed(uint8_t pin) {
  if (digitalRead(pin) == LOW && millis() - lastInput >= DEBOUNCE_MS) {
    lastInput = millis();
    return true;
  }
  return false;
}

void clampStats() {
  hunger = constrain(hunger, 0, 100);
  happiness = constrain(happiness, 0, 100);
  energy = constrain(energy, 0, 100);
}

void showMessage(const String &line1, const String &line2) {
  messageMode = true;
  messageLine1 = line1;
  messageLine2 = line2;
  messageUntil = millis() + 1800;
}

void drawBar(int y, const char *label, int value) {
  display.setCursor(0, y);
  display.print(label);
  display.drawRect(31, y, 70, 7, SSD1306_WHITE);
  int width = map(value, 0, 100, 0, 68);
  if (width > 0) display.fillRect(32, y + 1, width, 5, SSD1306_WHITE);
  display.setCursor(105, y);
  display.print(value);
}

void drawPetIcon() {
  display.setTextSize(2);
  display.setCursor(94, 0);
  if (pet == DRAGON) display.print("D");
  if (pet == PHOENIX) display.print("P");
  if (pet == UNICORN) display.print("U");
  display.setTextSize(1);
}

void render() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);

  if (messageMode) {
    display.setTextSize(1);
    display.setCursor(4, 15);
    display.println(messageLine1);
    display.setCursor(4, 32);
    display.println(messageLine2);
    display.setCursor(4, 51);
    display.print("Koin: ");
    display.print(coins);
    display.display();
    return;
  }

  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print(petNames[pet]);
  display.print(" Lv.");
  display.print(petLevel);
  drawPetIcon();
  drawBar(13, "Lapar", hunger);
  drawBar(23, "Senang", happiness);
  drawBar(33, "Energi", energy);

  display.setCursor(0, 45);
  display.print("Koin: ");
  display.print(coins);
  for (uint8_t i = 0; i < 4; ++i) {
    display.setCursor(i * 31, 55);
    if (i == menuIndex) display.print(">"); else display.print(" ");
    display.print(menuItems[i][0]);
  }
  display.display();
}

void feedPet() {
  if (coins < 5) {
    showMessage("Koin tidak cukup", "Butuh 5 koin");
    return;
  }
  coins -= 5;
  hunger += 24;
  happiness += 8;
  clampStats();
  showMessage("Peliharaan makan!", "+lapar +senang");
}

void doGacha() {
  if (coins < 30) {
    showMessage("Koin tidak cukup", "Gacha butuh 30");
    return;
  }
  coins -= 30;
  int roll = random(100);
  if (roll < 10) {
    pet = PHOENIX;
    petLevel++;
    showMessage("LEGENDARIS!", "Phoenix menetas");
  } else if (roll < 25) {
    pet = UNICORN;
    petLevel++;
    showMessage("Sangat langka!", "Unicorn menetas");
  } else if (roll < 50) {
    pet = DRAGON;
    petLevel++;
    showMessage("Langka!", "Naga menetas");
  } else {
    coins += 12;
    showMessage("Dapat 12 koin", "Coba lagi nanti");
  }
}

void adventure() {
  if (energy < 25 || hunger < 15) {
    showMessage("Belum siap jalan", "Beri makan/istirahat");
    return;
  }
  energy -= 25;
  hunger -= 15;
  int reward = random(8, 31);
  coins += reward;
  happiness += 12;
  if (random(100) < 30) petLevel++;
  clampStats();
  showMessage("Petualangan selesai!", "+" + String(reward) + " koin");
}

void showStatus() {
  String line = String(petNames[pet]) + " Lv." + String(petLevel);
  showMessage(line, "Rawat agar kuat!");
}

void selectMenu() {
  if (menuIndex == 0) feedPet();
  else if (menuIndex == 1) doGacha();
  else if (menuIndex == 2) adventure();
  else showStatus();
}

void setup() {
  pinMode(BUTTON_UP, INPUT_PULLUP);
  pinMode(BUTTON_DOWN, INPUT_PULLUP);
  pinMode(BUTTON_SELECT, INPUT_PULLUP);
  Wire.begin(OLED_SDA, OLED_SCL);
  display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS);
  randomSeed(esp_random());
  showMessage("Pocket Pet siap!", "Pilih menu");
  render();
}

void loop() {
  bool redraw = false;
  if (messageMode && millis() >= messageUntil) {
    messageMode = false;
    redraw = true;
  }

  if (!messageMode) {
    if (pressed(BUTTON_UP)) {
      menuIndex = (menuIndex + 3) % 4;
      redraw = true;
    }
    if (pressed(BUTTON_DOWN)) {
      menuIndex = (menuIndex + 1) % 4;
      redraw = true;
    }
    if (pressed(BUTTON_SELECT)) {
      selectMenu();
      redraw = true;
    }
  }

  if (millis() - lastDecay >= DECAY_INTERVAL_MS) {
    lastDecay = millis();
    hunger -= 2;
    happiness -= 1;
    energy += 3;
    clampStats();
    redraw = true;
  }

  if (redraw) render();
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik