Community project

Flappy Core Handheld Game

Photo of Flappy Core Handheld Game
Generated with AI

kinara bada ramadhan

Last updated November 14, 2026

Flappy Core is a compact handheld version of the classic Flappy Bird game, built around an ESP32 microcontroller with a 128x64 OLED display and a single mechanical keyboard switch for gameplay. The bird automatically falls due to gravity, and players tap the switch to make it flap and navigate through scrolling pipes, with the goal of achieving the highest score before collision.

This guide provides everything needed to assemble and play the game: a wiring diagram showing how to connect the display and switch to the ESP32, a complete parts list, the Arduino firmware with game logic and high-score storage, and step-by-step assembly instructions including display verification and USB connection checks.

Wiring diagram

Wiring diagram for Flappy Core Handheld Game
Loading diagram…

Gather all the parts

QtyComponent
1

SSD1306 OLED

0.96-inch 128 x 64 white monochrome OLED display module driven by an SSD1306 controller over I2C (address 0x3C). It runs from 3 V to 5 V and connects through a 4-pin 2.54 mm header (GND, VCC, SCL, SDA); the self-emitting pixels need no backlight.

1

Linear Red mechanical keyboard switch

Normally-open non-illuminated switch; use its two metal contact legs, not plastic locating posts.

Assemble it in 4 steps

1. Check display labels before connecting

This prototype targets the common 0.96-inch 128x64 SSD1306 I2C white OLED. Find the printed GND, VCC, SDA and SCL labels on your module. Follow the labels, not the physical header order. If the labels differ or the controller is different, leave it disconnected until identified.

  • Reversing VCC and GND can damage the display.

2. Connect the display with USB unplugged

Connect OLED VCC to board 3V3 (power), GND to GND (ground), SDA to GPIO0 (data), and SCL to GPIO1 (clock). GPIO8 and GPIO9 are not used because of their startup functions.

3. Connect one gameplay switch

Connect one mechanical switch's two metal electrical contacts to GPIO3 and GND. The firmware uses the board's internal pull-up, so no extra resistor is needed. This one switch starts, flaps, restarts and opens the high-score screen. Leave your other two switches unconnected for V0.1.

4. Check the USB prototype

Check that bare contacts cannot touch each other and that OLED power labels match the wiring. Power the board only through its USB socket. No battery, charger, converter, buzzer or external resistors are used.

Review all connections

1. Connections between "oled" and "ESP32"

FunctionoledESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 0
i2cSCLGPIO 1

2. Connections between "key" and "ESP32"

FunctionkeyESP32
digitalAGPIO 3
groundBGND

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Preferences.h>
#include <esp_system.h>
constexpr int SDA_PIN=0, SCL_PIN=1, KEY_PIN=3;
Adafruit_SSD1306 screen(128,64,&Wire,-1);
Preferences prefs;
enum State {BOOT,MENU,READY,PLAY,DYING,OVER,BEST_SCREEN};
State state=BOOT;
uint32_t entered=0, frame=0, changed=0, pressedAt=0;
bool raw=false, stable=false, dirty=true, holdUsed=false, storage=false, displayOK=false;
uint32_t best=0,score=0;
float birdY=30,velocity=0;
struct Pipe {float x; int gap; bool passed;};
Pipe pipes[2];
void enter(State s){state=s;entered=millis();dirty=true;}
void resetGame(){birdY=30;velocity=0;score=0;for(int i=0;i<2;i++)pipes[i]={float(145+i*76),int(random(19,43)),false};}
void text(int x,int y,const char* s){screen.setCursor(x,y);screen.print(s);}
void bird(){int y=int(birdY);screen.fillRect(25,y-3,7,6,WHITE);screen.fillRect(31,y-1,3,2,WHITE);screen.drawPixel(30,y-2,BLACK);screen.drawFastHLine(25,y+1,4,BLACK);}
void world(){for(auto &p:pipes){int x=int(p.x),top=p.gap-13,bottom=p.gap+13;screen.drawRect(x,10,12,top-10,WHITE);screen.fillRect(x-1,top-3,14,3,WHITE);screen.drawRect(x,bottom,12,60-bottom,WHITE);screen.fillRect(x-1,bottom,14,3,WHITE);}screen.drawFastHLine(0,60,128,WHITE);bird();screen.setCursor(0,0);screen.print("SC:");screen.print(score);screen.setCursor(72,0);screen.print("HI:");screen.print(best);}
void render(){screen.clearDisplay();screen.setTextSize(1);screen.setTextColor(WHITE);
 switch(state){
 case BOOT:text(28,16,"FLAPPY//CORE");text(28,32,"SYSTEM ONLINE");screen.drawRect(15,49,98,5,WHITE);screen.fillRect(17,51,min(94,int((millis()-entered)*94/1200)),1,WHITE);break;
 case MENU:screen.drawRect(0,0,128,64,WHITE);text(28,10,"FLAPPY//CORE");text(22,29,"TAP   > START");text(22,41,"HOLD  > BEST");text(28,53,"[ C3 V0.1 ]");break;
 case READY:world();screen.fillRect(35,22,90,25,BLACK);text(42,24,"READY?");text(42,38,"TAP TO FLY");break;
 case PLAY:world();break;
 case DYING:world();screen.drawCircle(29,int(birdY),int((millis()-entered)/40)+4,WHITE);break;
 case OVER:text(34,8,"CORE CRASH");screen.setCursor(25,24);screen.print("SCORE ");screen.print(score);screen.setCursor(25,35);screen.print("BEST  ");screen.print(best);text(19,53,"TAP > RESTART");break;
 case BEST_SCREEN:text(25,10,"// HIGH SCORE");screen.setTextSize(2);screen.setCursor(42,28);screen.print(best);screen.setTextSize(1);text(25,53,"TAP > MENU");break;
 }screen.display();dirty=false;}
void crash(){if(score>best){best=score;if(storage)prefs.putUInt("best",best);}enter(DYING);}
void step(){velocity+=0.16f;birdY+=velocity;for(auto &p:pipes){p.x-=1.15f;if(p.x<-15){p.x+=152;p.gap=random(19,43);p.passed=false;}if(!p.passed && p.x+13<25){p.passed=true;score++;}if(p.x-1<34 && p.x+13>25 && (birdY-3<p.gap-13 || birdY+3>=p.gap+13)){crash();return;}}if(birdY-3<10 || birdY+3>=60)crash();dirty=true;}
void setup(){Serial.begin(115200);pinMode(KEY_PIN,INPUT_PULLUP);Wire.begin(SDA_PIN,SCL_PIN);Wire.setClock(400000);Wire.beginTransmission(0x3C);uint8_t address=0x3C;if(Wire.endTransmission()!=0){Wire.beginTransmission(0x3D);if(Wire.endTransmission()!=0){Serial.println("OLED missing: check 3V3, GND, SDA 0, SCL 1");return;}address=0x3D;}displayOK=screen.begin(SSD1306_SWITCHCAPVCC,address,true,false);if(!displayOK){Serial.println("OLED allocation failed");return;}screen.setTextWrap(false);storage=prefs.begin("flappycore",false);if(storage)best=prefs.getUInt("best",0);randomSeed(esp_random());entered=millis();frame=entered;Serial.println("FLAPPY//CORE V0.1 ready - SSD1306 128x64 assumed");}
void loop(){if(!displayOK){delay(100);return;}uint32_t now=millis();bool sample=digitalRead(KEY_PIN)==LOW;bool down=false,up=false;if(sample!=raw){raw=sample;changed=now;}if(now-changed>=18 && stable!=raw){stable=raw;if(stable){down=true;pressedAt=now;holdUsed=false;}else up=true;}
 if(state==MENU){if(stable && !holdUsed && now-pressedAt>=700){holdUsed=true;enter(BEST_SCREEN);}else if(up && !holdUsed){resetGame();enter(READY);}}
 else if(down){switch(state){case READY:velocity=-1.85f;enter(PLAY);frame=now;break;case PLAY:velocity=-1.85f;break;case OVER:resetGame();enter(READY);break;case BEST_SCREEN:enter(MENU);break;default:break;}}
 if(state==BOOT && now-entered>=1200)enter(MENU);
 if(state==DYING && now-entered>=500)enter(OVER);
 if(now-frame>=33){frame=now;if(state==PLAY)step();else if(state==BOOT || state==DYING)dirty=true;if(dirty)render();}delay(1);
}

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