DEV Community

Cover image for Build a Flappy Bird Clone in Under 300 Lines of C++ — and Run It on Your Phone!
Official Beep8
Official Beep8

Posted on • Edited on

Build a Flappy Bird Clone in Under 300 Lines of C++ — and Run It on Your Phone!

Build a Flappy Bird Clone in Under 300 Lines of C++ — and Run It on Your Phone!

Image description

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.

Image description


🧠 Game Architecture

The core game class inherits from Pico8, the high-level BEEP-8 API wrapper:

class FlappyFlyerApp : public Pico8 { ... }; 
Enter fullscreen mode Exit fullscreen mode

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); 
Enter fullscreen mode Exit fullscreen mode

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); ... } } } 
Enter fullscreen mode Exit fullscreen mode

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; 
Enter fullscreen mode Exit fullscreen mode

The jump is triggered via any button:

if (!dead && btnp(BUTTON_ANY)) { v_flyer.y = VJUMP; } 
Enter fullscreen mode Exit fullscreen mode

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); } 
Enter fullscreen mode Exit fullscreen mode

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); 
Enter fullscreen mode Exit fullscreen mode

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; } 
Enter fullscreen mode Exit fullscreen mode

📦 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?

  1. Download the SDK here: https://beep8.github.io/beep8-sdk/
  2. Drop the main.cpp into the app/ folder
  3. Run the game using the BEEP-8 build system
  4. Open it in your browser — no installation, no approval needed

Image description


🚀 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)