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)