Built-in project · Arduino Uno R3

Flappy Bird on an OLED, on an Arduino Uno

Flappy Bird on an OLED with an Arduino: one button to flap through the pipes, gravity in whole numbers, sound on a piezo and the best score in EEPROM. The whole build, an Arduino Uno and 3 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Intermediate Runs in your browser. Free, and no account needed.

The parts retail for about $33. On Mokxi they're free.

Flappy Bird on an OLED · Arduino Uno R3live0.000 s 0.00x
Press Run, watch the demo, then press the button to flap
The circuit itself, running here on the simulator. Press what can be pressed; click anything else to open it in the editor.

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// Flappy Bird on a 128x64 OLED with one button and a piezo, on an Arduino Uno.
//
//   OLED     SDA -> A4, SCL -> A5, VCC -> 5 V, GND -> GND (address 0x3C)
//   button   pin 2 and GND (INPUT_PULLUP: a press reads LOW)
//   piezo    pin 8 and GND
//
// Press the button to flap. Gravity pulls the bird down all the time, and a
// flap throws it up. Fly through the gaps in the pipes: each pipe you pass is
// a point, and the best score is kept in the EEPROM so it survives a reset.
// Touch a pipe or the ground and the game is over.
//
// Before a game the computer plays: the bird flaps whenever it is below the
// middle of the next gap. Press the button to take over.
//
// # Gravity in whole numbers
//
// The bird's height and speed are kept in sixteenths of a pixel. Every frame
// the speed grows by 4 (gravity) and the height grows by the speed, so the
// bird falls slowly at first and then faster and faster, like a real thing
// falling. A flap just sets the speed to -36, upward. No floating point is
// needed, which keeps the Uno fast.
//
// # Drawing only what changes
//
// The OLED library sends only the parts of the screen that changed. So each
// frame, every pipe draws the two columns of pixels it moves into and rubs
// out the two it leaves, and the bird is rubbed out and drawn again.
// Clearing the whole screen each frame would send all 1024 bytes over I2C
// every time.

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

const uint8_t BUTTON = 2;
const uint8_t PIEZO = 8;
const int BEST_ADDRESS = 0;

const int W = 128;
const int H = 64;
const int TOP = 10;  // the score bar is above this line

const int BIRD_X = 24;
const int BIRD_W = 6;
const int BIRD_H = 5;
const int GRAVITY = 4;  // sixteenths of a pixel a frame, each frame
const int FLAP = -36;
const int MAX_FALL = 48;

const int PIPES = 2;
const int PIPE_W = 10;
const int GAP = 24;
const int SPACING = 72;  // from one pipe to the next
const int SPEED = 2;  // pixels the pipes move each frame
const unsigned long FRAME_MS = 40;

Adafruit_SSD1306 display(W, H, &Wire, -1);

int birdY;   // the top of the bird, in sixteenths of a pixel
int birdV;   // its speed, the same units, + is down
int shownBirdY = -100;
int pipeX[PIPES];
int gapY[PIPES];   // the top of each pipe's gap
bool passed[PIPES];
unsigned int score = 0;
uint8_t best = 0;
bool demo = true;
bool lastButton = HIGH;

int newGap() {
  return random(TOP + 6, H - 6 - GAP);
}

// One column of a pipe: everything from the top bar to the bottom except
// the gap.
void pipeColumn(int x, int gap, uint16_t color) {
  if (x < 0 || x >= W) return;
  display.drawFastVLine(x, TOP, gap - TOP, color);
  display.drawFastVLine(x, gap + GAP, H - gap - GAP, color);
}

void drawBar() {
  display.fillRect(0, 0, W, TOP - 1, SSD1306_BLACK);
  display.setCursor(0, 0);
  if (demo) {
    display.print("DEMO  press to play");
  } else {
    display.print("SCORE ");
    display.print(score);
    display.setCursor(80, 0);
    display.print("BEST ");
    display.print(best);
  }
}

void newGame(bool asDemo) {
  demo = asDemo;
  score = 0;
  birdY = 30 * 16;
  birdV = 0;
  shownBirdY = -100;
  display.clearDisplay();
  display.drawFastHLine(0, TOP - 1, W, SSD1306_WHITE);
  for (int i = 0; i < PIPES; i++) {
    pipeX[i] = W + i * SPACING;
    gapY[i] = newGap();
    passed[i] = false;
  }
  drawBar();
  display.display();
  Serial.println(demo ? "demo: press the button to play" : "game on: press to flap");
}

void flap() {
  birdV = FLAP;
  if (!demo) tone(PIEZO, 880, 20);
}

// The pipe the bird has to get through next.
int nextPipe() {
  int next = 0;
  for (int i = 0; i < PIPES; i++) {
    if (pipeX[i] + PIPE_W < BIRD_X) continue;
    if (pipeX[next] + PIPE_W < BIRD_X || pipeX[i] < pipeX[next]) next = i;
  }
  return next;
}

bool hitsPipe(int y) {
  for (int i = 0; i < PIPES; i++) {
    bool level = BIRD_X + BIRD_W > pipeX[i] && BIRD_X < pipeX[i] + PIPE_W;
    bool inGap = y >= gapY[i] && y + BIRD_H <= gapY[i] + GAP;
    if (level && !inGap) return true;
  }
  return false;
}

void gameOver() {
  tone(PIEZO, 150, 400);
  if (!demo) {
    Serial.print("game over: score ");
    Serial.println(score);
    if (score > best) {
      best = score;
      EEPROM.write(BEST_ADDRESS, best);
      Serial.print("new best: ");
      Serial.println(best);
    }
  }
  display.fillRect(34, 28, 60, 12, SSD1306_BLACK);
  display.setCursor(37, 30);
  display.print("GAME OVER");
  display.display();
  delay(2000);
  newGame(true);
}

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON, INPUT_PULLUP);
  pinMode(PIEZO, OUTPUT);
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("No OLED at 0x3C: check SDA and SCL");
    while (true) {
    }
  }
  // The OLED is happy at 400 kHz, four times the I2C default, and every
  // frame is that much quicker to send.
  Wire.setClock(400000);
  display.setTextColor(SSD1306_WHITE);
  display.setTextSize(1);
  // A blank EEPROM reads 255: no best score yet.
  best = EEPROM.read(BEST_ADDRESS);
  if (best == 255) best = 0;
  // Nothing is wired to A0, so it reads noise: different pipes each time.
  randomSeed(analogRead(A0));
  newGame(true);
}

void loop() {
  unsigned long started = millis();

  bool button = digitalRead(BUTTON);
  bool pressed = button == LOW && lastButton == HIGH;
  lastButton = button;
  if (pressed && demo) newGame(false);
  if (pressed) flap();

  // The computer flaps when the bird is below the middle of the next gap.
  if (demo) {
    int p = nextPipe();
    if (birdV >= 0 && birdY / 16 + BIRD_H / 2 > gapY[p] + GAP / 2 + 2) flap();
  }

  // Gravity, then move.
  birdV = min(birdV + GRAVITY, MAX_FALL);
  birdY += birdV;
  if (birdY < TOP * 16) {
    birdY = TOP * 16;
    birdV = 0;
  }

  // The pipes slide left: draw the columns each moves into and rub out the
  // ones it leaves.
  for (int i = 0; i < PIPES; i++) {
    for (int step = 0; step < SPEED; step++) {
      pipeX[i]--;
      pipeColumn(pipeX[i], gapY[i], SSD1306_WHITE);
      pipeColumn(pipeX[i] + PIPE_W, gapY[i], SSD1306_BLACK);
    }
    if (pipeX[i] + PIPE_W < 0) {
      // Off the left edge: it comes back behind the other one.
      int other = pipeX[(i + 1) % PIPES];
      pipeX[i] = max(other + SPACING, W);
      gapY[i] = newGap();
      passed[i] = false;
    }
    if (!passed[i] && pipeX[i] + PIPE_W < BIRD_X) {
      passed[i] = true;
      score++;
      if (!demo) {
        tone(PIEZO, 1760, 30);
        Serial.print("point! score ");
        Serial.println(score);
        drawBar();
      }
    }
  }

  int y = birdY / 16;
  if (y + BIRD_H >= H || hitsPipe(y)) {
    gameOver();
    return;
  }
  if (y != shownBirdY) {
    if (shownBirdY >= 0) display.fillRect(BIRD_X, shownBirdY, BIRD_W, BIRD_H, SSD1306_BLACK);
    display.fillRect(BIRD_X, y, BIRD_W, BIRD_H, SSD1306_WHITE);
    display.drawPixel(BIRD_X + 4, y + 1, SSD1306_BLACK);  // the eye
    shownBirdY = y;
  }
  display.display();

  while (millis() - started < FRAME_MS) {
  }
}

Parts list

5 parts, plus the jumper wires. They retail for about $33; on Mokxi they're free.

How it is wired

8 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.

  • Arduino Uno R3 pin 8; Piezo speaker pin 1
  • Arduino Uno R3 pin 2; Arcade button pin 2
  • Arduino Uno R3 pin A4; OLED display, 128x64 pin SDA
  • Arduino Uno R3 pin A5; OLED display, 128x64 pin SCL
  • Ground: Arcade button pin 1
  • Ground: Piezo speaker pin 2
  • Ground: OLED display, 128x64 pin GND
  • 5 V: OLED display, 128x64 pin VCC

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.