Week 10

Programming an Application.

Flappy Birb with Processing + Arduino

This week I built a small game called Flappy Birb in Processing. The bird jumps when I press a real push-button that is read by an Arduino Nano. The Nano sends the words PRESSED and RELEASED over USB. Processing watches the serial line and makes the bird flap.

Control Circuit

First I wired the push-button like this:

Arduino Sketch

The sketch reads the button state and sends it over the serial port.

Copied!
const int buttonPin = 2;
bool lastState = LOW;

void setup() {
  pinMode(buttonPin, INPUT);
  Serial.begin(9600);
}

void loop() {
  bool currentState = digitalRead(buttonPin);
  if (currentState != lastState) {
    if (currentState == HIGH) {
      Serial.println("PRESSED");
    } else {
      Serial.println("RELEASED");
    }
    lastState = currentState;
  }
  delay(10);
}

Project Structure in Processing

I placed the sound files and the main sketch like this:

Copied!
└── FlappyBirb/
    ├── data/
    │   ├── die.wav
    │   ├── flap.wav
    │   └── score.wav
    └── FlappyBirb.pde

Main Processing File

The full source code for FlappyBirb.pde is below. It handles basic movement, pipe generation, and some visual effects.

Copied!
import processing.serial.*;
import processing.sound.*;

Serial myPort;
String incoming = "";
boolean buttonPressed = false;

float birdY;
float velocity = 0;
float gravity = 0.6;
float jumpStrength = 10;
float birdSize = 30;

ArrayList pipes;
ArrayList particles;

PFont font;
int score = 0;
int finalScore = 0;
int bestScore = 0;
boolean newHigh = false;

boolean playing = false;
boolean firstFlap = false;
boolean gameOver = false;

// Sounds
SoundFile flapSound, scoreSound, dieSound;

// Play button
int btnX = 170;
int btnY = 250;
int btnW = 160;
int btnH = 60;

// Color Madness
boolean colorMadness = false;
color bgColor, pipeColor, birdColor;
int currentPalette = 0;

// Effects
String[] effects = { "Wider Pipes", "Inverted Gravity", "Background Distraction" };
String currentEffect = "";
String nextEffect = "";
int activeEffect = -1;
boolean effectActive = false;
String lastEffect = "";

color[][] colorPalettes = {
  { color(135, 206, 250), color(34, 139, 34), color(255, 255, 0) },    // Sky Blue - Normal
  { color(20, 20, 20), color(255, 0, 0), color(255, 255, 255) },       // Dark mode
  { color(255, 240, 200), color(250, 160, 120), color(120, 90, 150) }, // Sunset
  { color(100, 200, 255), color(0, 100, 200), color(255, 255, 0) },    // Ocean
  { color(230, 255, 240), color(60, 180, 75), color(255, 105, 180) },  // Mint
  { color(50, 0, 100), color(200, 0, 255), color(0, 255, 200) },       // Vaporwave
  { color(245, 222, 179), color(210, 105, 30), color(139, 69, 19) },   // Desert
  { color(173, 216, 230), color(25, 25, 112), color(255, 255, 255) },  // Winter Sky
  { color(255, 182, 193), color(199, 21, 133), color(255, 255, 0) },   // Candy
  { color(34, 139, 34), color(255, 140, 0), color(255, 255, 255) },    // Forest Fire
  { color(176, 224, 230), color(0, 128, 128), color(255, 215, 0) },    // Tropical
  { color(240, 128, 128), color(255, 69, 0), color(255, 255, 255) },   // Lava
  { color(0, 255, 127), color(0, 100, 0), color(255, 255, 255) },      // Jungle
  { color(255, 228, 181), color(255, 160, 122), color(139, 0, 0) },    // Autumn Leaves
  { color(0, 0, 139), color(65, 105, 225), color(255, 255, 0) }        // Deep Space
};

void setup() {
  size(500, 600);
  printArray(Serial.list());
  myPort = new Serial(this, Serial.list()[2], 9600);
  myPort.bufferUntil('\n');

  font = createFont("Arial", 24);
  textFont(font);

  flapSound = new SoundFile(this, "flap.wav");
  scoreSound = new SoundFile(this, "score.wav");
  dieSound = new SoundFile(this, "die.wav");

  resetGame();
}

void draw() {
  background(playing ? (colorMadness ? bgColor : color(135, 206, 250)) : color(135, 206, 250));

  if (!playing) {
    fill(0);
    textAlign(CENTER, CENTER);
    textSize(40);
    text("Flappy Birb", width / 2, 100);

    fill(255);
    rect(btnX, btnY, btnW, btnH, 10);
    fill(0);
    textSize(24);
    text("Play", width / 2, btnY + btnH / 2);

    if (gameOver) {
      fill(0);
      textSize(20);
      text("Score: " + finalScore, width / 2, btnY + btnH + 40);
      text("Best: " + bestScore, width / 2, btnY + btnH + 70);
      if (newHigh) {
        fill(255, 0, 0);
        textSize(22);
        text("New High Score!", width / 2, btnY + btnH + 100);
      }
    }

  } else {
    if (effectActive && activeEffect == 2) {
      for (int i = particles.size() - 1; i >= 0; i--) {
        Particle p = particles.get(i);
        p.update();
        p.show();
        if (p.isDead()) particles.remove(i);
      }
      if (frameCount % 2 == 0) particles.add(new Particle());
    }

    if (!firstFlap) {
      birdY = height / 2 + sin(frameCount * 0.1) * 5;
    } else {
      if (effectActive && activeEffect == 1) {
        velocity -= gravity;
      } else {
        velocity += gravity;
      }
      birdY += velocity;
    }

    if (buttonPressed) {
      if (!firstFlap) firstFlap = true;
      if (effectActive && activeEffect == 1) {
        velocity = jumpStrength;
      } else {
        velocity = -jumpStrength;
      }
      flapSound.play();
      buttonPressed = false;
    }

    fill(colorMadness ? birdColor : color(255, 255, 0));
    ellipse(100, birdY, birdSize, birdSize);

    for (int i = pipes.size() - 1; i >= 0; i--) {
      Pipe p = pipes.get(i);
      p.update();
      p.show();

      if (p.hits(birdY)) {
        endGame();
      }

      if (!p.scored && p.x + p.w < 100) {
        score++;
        p.scored = true;
        scoreSound.play();

        if (score == 2) {
          chooseNextEffect();
        }
        if (score == 3) {
          activateColorMadness();
          activateNextEffect();
        } else if (score > 3) {
          activateColorMadness();
          activateNextEffect();
        }
      }

      if (p.offscreen()) {
        pipes.remove(i);
        pipes.add(new Pipe());
      }
    }

    if (birdY > height || birdY < 0) {
      endGame();
    }

    fill(getContrastColor(colorMadness ? bgColor : color(135, 206, 250)));
    textAlign(LEFT);
    textSize(24);
    text("Score: " + score, 10, 30);
    text("Best: " + bestScore, 10, 60);
    if (score >= 2) {
      textSize(18);
      text("Next: " + nextEffect, 10, 90);
    }
  }
}

void activateColorMadness() {
  colorMadness = true;
  int newPalette;
  do {
    newPalette = int(random(colorPalettes.length));
  } while (newPalette == currentPalette);
  currentPalette = newPalette;

  bgColor = colorPalettes[newPalette][0];
  pipeColor = colorPalettes[newPalette][1];
  birdColor = colorPalettes[newPalette][2];
}

void chooseNextEffect() {
  String newEffect;
  do {
    newEffect = effects[int(random(effects.length))];
  } while (newEffect.equals(lastEffect));
  nextEffect = newEffect;
}

void activateNextEffect() {
  if (nextEffect.equals("Wider Pipes")) {
    activeEffect = 0;
  } else if (nextEffect.equals("Inverted Gravity")) {
    activeEffect = 1;
  } else if (nextEffect.equals("Background Distraction")) {
    activeEffect = 2;
    particles = new ArrayList();
  }
  lastEffect = nextEffect;
  chooseNextEffect(); // pick next after activation
  effectActive = true;
}

void resetGame() {
  activeEffect = -1;
  nextEffect = "";
  effectActive = false;
  lastEffect = "";

  colorMadness = false;
  bgColor = color(135, 206, 250);
  pipeColor = color(34, 139, 34);
  birdColor = color(255, 255, 0);
  currentPalette = 0;

  score = 0;
  velocity = 0;
  birdY = height / 2;
  newHigh = false;
  playing = false;
  gameOver = false;
  firstFlap = false;

  pipes = new ArrayList();
  pipes.add(new Pipe());

  particles = new ArrayList();
}

void endGame() {
  dieSound.play();
  finalScore = score;
  if (score > bestScore) {
    bestScore = score;
    newHigh = true;
  }
  playing = false;
  gameOver = true;
}

void mousePressed() {
  if (!playing && overButton(mouseX, mouseY)) {
    resetGame();
    playing = true;
  } else if (playing) {
    if (!firstFlap) firstFlap = true;
    if (effectActive && activeEffect == 1) {
      velocity = jumpStrength;
    } else {
      velocity = -jumpStrength;
    }
    flapSound.play();
  }
}

void keyPressed() {
  if (playing) {
    if (!firstFlap) firstFlap = true;
    if (effectActive && activeEffect == 1) {
      velocity = jumpStrength;
    } else {
      velocity = -jumpStrength;
    }
    flapSound.play();
  }
}

void serialEvent(Serial myPort) {
  incoming = trim(myPort.readStringUntil('\n'));
  if (incoming.equals("PRESSED")) {
    if (!playing) {
      resetGame();
      playing = true;
    } else {
      buttonPressed = true;
    }
  }
}

boolean overButton(int x, int y) {
  return x > btnX && x < btnX + btnW && y > btnY && y < btnY + btnH;
}

class Pipe {
  float x;
  float top;
  float bottom;
  float w;
  float speed = 3;
  boolean scored = false;

  Pipe() {
    x = width;
    float gap = 150;
    top = random(50, height - gap - 50);
    bottom = height - top - gap;
    w = (effectActive && activeEffect == 0) ? 120 : 60;
  }

  void update() {
    if (firstFlap) x -= speed;
  }

  void show() {
    fill(colorMadness ? pipeColor : color(34, 139, 34));
    rect(x, 0, w, top);
    rect(x, height - bottom, w, bottom);
  }

  boolean hits(float y) {
    return (y < top || y > height - bottom) && (x < 100 + 15 && x + w > 100 - 15);
  }

  boolean offscreen() {
    return x < -w;
  }
}

class Particle {
  float x, y, dx, dy, sz, alpha;

  Particle() {
    x = random(width);
    y = random(height);
    dx = random(-5, 5);
    dy = random(-5, 5);
    sz = random(10, 30);
    alpha = 255;
  }

  void update() {
    x += dx;
    y += dy;
    alpha -= 4;
  }

  void show() {
    noStroke();
    fill(birdColor, alpha);
    ellipse(x, y, sz, sz);
  }

  boolean isDead() {
    return alpha <= 0;
  }
}

color getContrastColor(color bg) {
  float brightness = red(bg) * 0.299 + green(bg) * 0.587 + blue(bg) * 0.114;
  return (brightness > 128) ? color(0) : color(255);
}

Demonstration

The short clip below shows the game running on my notebook with the button in my hand.

Future Ideas

In the future I could add lootboxes, overpriced skins, seasonal content, paid DLCs, subscription fees, premium currencies, sex, background bitcoin mining, pre-order bonuses, lots of bugs, micro-transactions, heavy DRM, daily login rewards, fake loading screens, servers that shut down after a year, intrusive ads, and much more.