Build a Flappy Bird Clone in Under 300 Lines of C++ — and Run It on Your Phone!
What if you could write a Flappy Bird clone in modern C++, under 300 lines, and have it run at 60fps directly in your mobile browser — no Apple or Google approval required?
With BEEP-8, that dream is not just possible — it’s easy.
In this post, we’ll build a smooth, sprite-based Flappy Bird-style game using the BEEP-8 SDK
, a fantasy console that combines PICO-8-style graphics with a C++20 codebase, optimized for mobile.
🎮 What is BEEP-8?
BEEP-8 is a fantasy console built for C/C++ developers. Think of it as a PICO-8-like platform but:
✅ Written in standard C++20
✅ Targeting web browsers (PC & mobile)
✅ Featuring a fixed 128×240 vertical resolution (perfect for smartphones)
✅ No app store approvals required — just ship and share a URL
✅ Includes an emulator, PPU (Pixel Processing Unit), and APU (Namco C30-style audio)
You can try live games at https://beep8.org/
Or download the SDK here:
👉 https://beep8.github.io/beep8-sdk/
🐤 The Game: FlappyFlyer
FlappyFlyer is a lightweight Flappy Bird clone featuring:
- Sprite-based rendering using the PPU
- A 2D physics system (gravity & jump)
- Procedural pipe generation and scrolling map
- Collision detection via
fget()
flags - Title screen and score tracking
All written in modern C++ (no float
, only fixed-point fx8
) and runs on BEEP-8.
🧠 Game Architecture
The core game class inherits from Pico8
, the high-level BEEP-8 API wrapper:
class FlappyFlyerApp : public Pico8 {
...
};
It overrides the _init()
, _update()
, and _draw()
virtual methods. These form the main loop, similar to init()
, update()
, and draw()
in PICO-8 or Unity.
📦 Sprites and Tilemap
All environment objects — ground, pipes, sensors — are mapped via mset()
or msett()
calls.
Collision flags are assigned using:
fset(SPR_PIPELINE, 0xff, FLAG_WALL);
This allows fget()
to easily check what the flyer touches every frame.
🧱 Procedural Pipe Generation
To keep the game lightweight, we generate the background tilemap on-the-fly as the flyer moves forward:
void generateMapColumns() {
while( xgen_map < pos_flyer.x + 192 ) {
...
mset(xt, YT_GROUND, SPR_GROUND_GREEN);
...
if (xgen_map > 128 && !(xt & 7)) {
...
const int ytop = ...;
msett(xt, ytop, BG_TILE_PIPE_L_VFLIP);
...
}
}
}
Pipes are placed in pairs and a hidden SPR_SENSOR
tile is added between them to detect when the player passes through for scoring.
🏃 Physics & Input
The flyer uses a basic physics loop:
v_flyer.y = v_flyer.y + GRAVITY;
pos_flyer += v_flyer;
The jump is triggered via any button:
if (!dead && btnp(BUTTON_ANY)) {
v_flyer.y = VJUMP;
}
No third-party physics engine required!
☠️ Collision & Game Over
Each frame checks the tile beneath the flyer:
const u8 collide = checkCollision();
if (!dead) {
dead = (collide == FLAG_WALL);
}
If the player hits a wall or falls off screen, the game resets to the title screen after a delay.
🏁 Title Screen and Score
The game features a built-in title screen with animation and score tracking.
print("\e[13;4H HI:%d", hi_score);
print("\e[15;4H SC:%d", score);
The score is only updated when the flyer passes a sensor tile (i.e., gets through the pipe pair).
📕 Here’s the full source code for the app!
#include <pico8.h>
using namespace std;
using namespace pico8;
namespace {
constexpr u8 FLAG_WALL = 1;
constexpr u8 FLAG_SENSOR = 2;
constexpr u8 SPR_EMPTY = 0;
constexpr u8 SPR_FLYER = 4;
constexpr u8 SPR_GROUND_GREEN = 9;
constexpr u8 SPR_GROUND = 8;
constexpr u8 SPR_PIPELINE = 16;
constexpr u8 SPR_TITLE = 80;
constexpr u8 SPR_SENSOR = 10;
constexpr u8 SPR_CLOUD = 12;
constexpr fx8 VJUMP(-29,10);
constexpr fx8 GRAVITY(17,100);
constexpr b8PpuBgTile BG_TILE_PIPE_L = {.YTILE=1, .XTILE=9, };
constexpr b8PpuBgTile BG_TILE_PIPE_R = {.YTILE=1, .XTILE=10, };
constexpr b8PpuBgTile BG_TILE_PIPE_L_VFLIP = {.YTILE=1, .XTILE=9, .VFP=1 };
constexpr b8PpuBgTile BG_TILE_PIPE_R_VFLIP = {.YTILE=1, .XTILE=10,.VFP=1 };
constexpr u8 PAL_COIN_BLINK = 3;
constexpr u8 PAL_SHADOW = 4;
constexpr u8 YT_GROUND = 23;
constexpr BgTiles XTILES = TILES_32;
constexpr BgTiles YTILES = TILES_32;
}
enum class GameState { Nil, Title, Playing };
constexpr inline u8 tileId(b8PpuBgTile tile ) {
return static_cast<u8>((tile.YTILE << 4) | (tile.XTILE & 0x0F));
}
class FlappyFlyerApp : public Pico8 {
int frame = 0;
GameState reqReset = GameState::Nil;
GameState status = GameState::Nil;
Vec cam;
Vec pos_flyer;
Vec v_flyer;
int xgen_map = 0;
fx8 ygen;
bool dead = false;
bool req_red = false;
u8 dcnt_stop_update = 0;
int hi_score = 0;
int score = 0;
int disp_score = 0;
int xlast_got_score = 0;
int cnt_title = 0;
int calculatePipeSpan() {
if (score <= 10) {
return 8;
} else if (score <= 20) {
static constexpr int values[] = {8, 7, 7, 7, 6};
return rndt(values);
} else if (score <= 50) {
static constexpr int values[] = {7, 6, 7, 7, 7};
return rndt(values);
} else if (score <= 75) {
static constexpr int values[] = {8, 7, 6, 6, 7, 6, 6};
return rndt(values);
} else if (score <= 100) {
static constexpr int values[] = {7, 7, 6, 6, 6, 6, 6};
return rndt(values);
} else if (score <= 110) {
static constexpr int values[] = {9, 8, 8, 7};
return rndt(values);
} else if (score <= 140) {
static constexpr int values[] = {7, 6, 6, 6, 6};
return rndt(values);
} else if (score <= 180) {
static constexpr int values[] = {6, 6, 6, 6, 5};
return rndt(values);
} else {
static constexpr int values[] = {6, 6, 5, 5, 5};
return rndt(values);
}
}
void generateMapColumns(){
int xdst = pos_flyer.x + 192;
while( xgen_map < xdst ){
int yy;
const u32 xt = (xgen_map >> 3) & (XTILES-1);
mset(xt,YT_GROUND, SPR_GROUND_GREEN);
for( yy=YT_GROUND+1 ; yy<YTILES ; ++yy ){
mset(xt,yy,SPR_GROUND);
}
xgen_map += 8;
if( !(xt & 7) && xgen_map > 128 ){
for( yy=0 ; yy<YT_GROUND ; ++yy ){
mset(xt, yy,SPR_EMPTY);
mset(xt+1,yy,SPR_EMPTY);
}
const int yt = int(ygen)>>3;
const int ytop = yt - 3;
for( yy=0 ; yy<ytop ; ++yy ){
mset(xt, yy,SPR_PIPELINE);
mset(xt+1,yy,SPR_PIPELINE+1);
}
msett(xt, ytop,BG_TILE_PIPE_L_VFLIP);
msett(xt+1,ytop,BG_TILE_PIPE_R_VFLIP);
const int ybottom = ytop + calculatePipeSpan();
if( ybottom < YT_GROUND ){
msett(xt, ybottom,BG_TILE_PIPE_L);
msett(xt+1,ybottom,BG_TILE_PIPE_R);
for( yy=ybottom+1 ; yy<YT_GROUND ; ++yy ){
mset(xt, yy,SPR_PIPELINE);
mset(xt+1,yy,SPR_PIPELINE+1);
}
}
// sensor
for( yy=ytop+1 ; yy<=ybottom-1 ; ++yy ){
mset(xt+1,yy,SPR_SENSOR);
}
ygen += pico8::rnd(96)-fx8(48);
ygen = pico8::mid(ygen ,32, (YT_GROUND<<3)-48 );
}
}
}
u8 checkCollision() {
return fget(
mget(
(static_cast< u32 >( pos_flyer.x ) >> 3) & (XTILES-1),
(static_cast< u32 >( pos_flyer.y ) >> 3) & (YTILES-1)
),
0xff
);
}
void _init() override {
extern const uint8_t b8_image_sprite0[];
hi_score = 53;
lsp(0, b8_image_sprite0);
mapsetup(XTILES, YTILES,std::nullopt,B8_PPU_BG_WRAP_REPEAT,B8_PPU_BG_WRAP_REPEAT);
fset( tileId(BG_TILE_PIPE_L) , 0xff, FLAG_WALL);
fset( tileId(BG_TILE_PIPE_R) , 0xff, FLAG_WALL);
fset( tileId(BG_TILE_PIPE_L_VFLIP) ,0xff, FLAG_WALL);
fset( tileId(BG_TILE_PIPE_R_VFLIP) ,0xff, FLAG_WALL);
fset( SPR_GROUND, 0xff, FLAG_WALL);
fset( SPR_GROUND_GREEN, 0xff, FLAG_WALL);
fset( SPR_PIPELINE , 0xff, FLAG_WALL);
fset( SPR_PIPELINE+1, 0xff, FLAG_WALL);
fset( SPR_SENSOR,0xff,FLAG_SENSOR);
reqReset = GameState::Title;
}
void updatePlaying(){
if( (!dead) && btnp( BUTTON_ANY ) ) v_flyer.y = VJUMP;
pos_flyer += v_flyer;
v_flyer.y = v_flyer.y + GRAVITY;
cam.x = pos_flyer.x - 32;
cam.y = 0;
pos_flyer.y = pico8::max( pos_flyer.y , 0 );
const u8 collide = checkCollision();
if( !dead ){
req_red = dead = (collide == FLAG_WALL);
if( dead ){
dcnt_stop_update = 7;
}
}
if( (!dead) && pos_flyer.x > xlast_got_score + 9 ){
if( collide == FLAG_SENSOR ) ++score;
hi_score = pico8::max( score , hi_score);
xlast_got_score = pos_flyer.x;
}
if( dead && pos_flyer.y > 240 ) reqReset = GameState::Title;
generateMapColumns();
}
void enterTitle(){
cnt_title = 0;
print("\e[3;7H ");
print("\e[3q\e[13;4H HI:%d\e[0q" , hi_score );
print("\e[15;4H SC:%d", score );
}
void enterPlaying(){
print("\e[2J");
pos_flyer.set(0,64);
v_flyer.set(fx8(2,2),0);
xgen_map = pos_flyer.x - 64;
ygen = pos_flyer.y;
dead = false;
score = 0;
disp_score = -1;
xlast_got_score = 0;
b8PpuBgTile tile = {};
mcls(tile);
generateMapColumns();
}
void updateTitle() {
generateMapColumns();
cnt_title++;
if( btnp( BUTTON_ANY ) ) reqReset = GameState::Playing;
}
void _update() override {
++frame;
if( reqReset != GameState::Nil ){
switch( reqReset ){
case GameState::Nil: break;
case GameState::Title: enterTitle(); break;
case GameState::Playing: enterPlaying(); break;
}
status = reqReset;
reqReset = GameState::Nil;
}
if( dcnt_stop_update > 0 ){
--dcnt_stop_update;
return;
}
switch( status ){
case GameState::Playing: updatePlaying(); break;
case GameState::Title: updateTitle(); break;
case GameState::Nil: break;
}
}
void _draw() override {
// Enable or disable the debug string output via dprint().
dprintenable(false);
pal( WHITE, RED , 3 );
camera();
cls(req_red ? RED : BLUE);
req_red = false;
setz(maxz()-1);
camera(cam.x, cam.y);
map(cam.x, cam.y, BG_0);
setz(maxz()-3);
const u8 palsel = 1;
pal(WHITE, BLACK, palsel);
// Draw the yellow round-faced Foo sprite.
switch( status ){
case GameState::Nil:
case GameState::Title:{
camera();
setz(1);
spr(SPR_TITLE,4, pico8::min(48,(cnt_title*3)-32),15,4);
const u8 anm = ((cnt_title>>3)&1)<<1;
spr(SPR_FLYER + anm, (cnt_title+44)&255, 140, 2, 2);
}break;
case GameState::Playing:{
const u8 anm = dead ? 0 : ((static_cast< u32 >( pos_flyer.y ) >> 3) & 1)<<1;
spr(SPR_FLYER + anm, pos_flyer.x-8, pos_flyer.y-8, 2, 2, false, dead );
if( score != disp_score ){
disp_score = score;
print("\e[21;1H%d",disp_score);
}
}break;
}
camera();
setz(maxz());
spr(SPR_CLOUD, (((255-(frame>>2)))&255)-64,7,4,4);
}
public: virtual ~FlappyFlyerApp(){}
};
int main() {
FlappyFlyerApp app;
app.run();
return 0;
}
📦 Source Code
The full source code is already short and clean — under 300 lines — thanks to the tight BEEP-8 API.
Want to try it yourself?
- Download the SDK here: https://beep8.github.io/beep8-sdk/
- Drop the
main.cpp
into theapp/
folder - Run the game using the BEEP-8 build system
- Open it in your browser — no installation, no approval needed
🚀 Conclusion
With BEEP-8, writing retro-style mobile games in pure C++ becomes simple and fun. Whether you're building a jam game or learning game programming, BEEP-8 gives you:
✅ True 60fps sprite rendering
✅ Easy input and collision APIs
✅ Simple sprite/tilemap management
✅ Out-of-the-box mobile support
✅ Zero deployment stress
Give it a try at https://beep8.org/ and let your C++ creativity soar.
Got questions or want to see more BEEP-8 tutorials? Drop a comment below or check out the SDK docs.
Happy coding! 🚀
Top comments (0)