#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <LiquidCrystal_I2C.h>
// OLED setup
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define OLED_ADDR 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// LCD setup
#define LCD_ADDR 0x27
LiquidCrystal_I2C lcd(LCD_ADDR, 16, 2);
// Button pin
#define BUTTON_PIN 4
// Game constants
#define GRAVITY 0.25 // Acceleration downward
#define FLAP_VELOCITY -5 // Upward velocity on flap
#define PIPE_SPEED 2 // Pixels per frame
#define PIPE_WIDTH 20 // Width of pipes
#define PIPE_GAP 35 // Vertical gap between pipes
#define PIPE_INTERVAL 80 // Frames between new pipes
#define BIRD_RADIUS 4 // Size of bird (circle)
#define GROUND_HEIGHT 8 // Height of ground at bottom
#define MAX_PIPES 4 // Max pipes on screen
// Game variables
float birdY = SCREEN_HEIGHT / 2; // Bird starting Y position
float birdVelocity = 0; // Bird vertical velocity
int score = 0; // Current score
bool gameOver = true; // Start in game over state (wait for start)
bool gameStarted = false;
// Interrupt handling
volatile bool buttonPressedFlag = false;
unsigned long lastDebounceTime = 0;
unsigned long debounceDelay = 50; // 50ms debounce
// Pipe structure
struct Pipe {
int x; // X position
int gapY; // Y position of the top of the gap
bool scored; // Has the bird passed this pipe?
};
Pipe pipes[MAX_PIPES];
int pipeCount = 0;
int frameCounter = 0;
// Interrupt Service Routine
void IRAM_ATTR buttonISR() {
unsigned long now = millis();
if (now - lastDebounceTime > debounceDelay) {
buttonPressedFlag = true;
lastDebounceTime = now;
}
}
void setup() {
Serial.begin(115200);
// Initialize button with pull-up and attach interrupt
pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), buttonISR, FALLING);
// Initialize OLED
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
Serial.println(F("SSD1306 allocation failed"));
for (;;); // Halt if failed
}
display.clearDisplay();
display.display();
// Initialize LCD
lcd.init();
lcd.backlight();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Flappy Bird");
lcd.setCursor(0, 1);
lcd.print("Score: 0");
showStartScreen();
}
void loop() {
// Check for button press via interrupt flag
bool buttonJustPressed = false;
if (buttonPressedFlag) {
buttonPressedFlag = false;
buttonJustPressed = true;
Serial.println("Button pressed!"); // Debug print
}
if (gameOver) {
// Wait for button press to start/restart
if (buttonJustPressed) {
resetGame();
Serial.println("Starting game..."); // Debug
}
return;
}
// Flap on button press
if (buttonJustPressed && gameStarted) {
birdVelocity = FLAP_VELOCITY;
Serial.println("Flap!"); // Debug
}
// Update physics
birdVelocity += GRAVITY;
birdY += birdVelocity;
// Check ground/ceiling collision
if (birdY + BIRD_RADIUS >= SCREEN_HEIGHT - GROUND_HEIGHT || birdY - BIRD_RADIUS <= 0) {
gameOver = true;
showGameOver();
return;
}
// Update pipes
updatePipes();
// Check collisions with pipes
if (checkCollisions()) {
gameOver = true;
showGameOver();
return;
}
// Rendering
renderGame();
// Frame delay (approx 50 FPS)
delay(20);
frameCounter++;
}
// Reset game state
void resetGame() {
birdY = SCREEN_HEIGHT / 2;
birdVelocity = 0;
score = 0;
pipeCount = 0;
frameCounter = 0;
gameOver = false;
gameStarted = true;
updateScoreDisplay();
display.clearDisplay();
display.display();
}
// Update pipes: move, add new, remove old, score
void updatePipes() {
// Add new pipe periodically
if (frameCounter % PIPE_INTERVAL == 0 && pipeCount < MAX_PIPES) {
pipes[pipeCount].x = SCREEN_WIDTH;
pipes[pipeCount].gapY = random(PIPE_GAP + 10, SCREEN_HEIGHT - GROUND_HEIGHT - PIPE_GAP - 10); // Random gap position
pipes[pipeCount].scored = false;
pipeCount++;
}
// Move pipes and check scoring
for (int i = 0; i < pipeCount; i++) {
pipes[i].x -= PIPE_SPEED;
// Score if bird passes pipe
if (!pipes[i].scored && pipes[i].x + PIPE_WIDTH < SCREEN_WIDTH / 4) { // Bird is at fixed X (assume SCREEN_WIDTH / 4)
pipes[i].scored = true;
score++;
updateScoreDisplay();
}
// Remove off-screen pipes
if (pipes[i].x + PIPE_WIDTH < 0) {
// Shift array left
for (int j = i; j < pipeCount - 1; j++) {
pipes[j] = pipes[j + 1];
}
pipeCount--;
i--; // Re-check current index
}
}
}
// Check if bird collides with any pipe
bool checkCollisions() {
int birdX = SCREEN_WIDTH / 4; // Bird fixed X position
for (int i = 0; i < pipeCount; i++) {
// Check top pipe
if (birdX + BIRD_RADIUS > pipes[i].x && birdX - BIRD_RADIUS < pipes[i].x + PIPE_WIDTH &&
birdY - BIRD_RADIUS < pipes[i].gapY) {
return true;
}
// Check bottom pipe
if (birdX + BIRD_RADIUS > pipes[i].x && birdX - BIRD_RADIUS < pipes[i].x + PIPE_WIDTH &&
birdY + BIRD_RADIUS > pipes[i].gapY + PIPE_GAP) {
return true;
}
}
return false;
}
// Render game on OLED
void renderGame() {
display.clearDisplay();
// Draw bird (simple circle)
display.fillCircle(SCREEN_WIDTH / 4, birdY, BIRD_RADIUS, SSD1306_WHITE);
// Draw pipes
for (int i = 0; i < pipeCount; i++) {
// Top pipe
display.fillRect(pipes[i].x, 0, PIPE_WIDTH, pipes[i].gapY, SSD1306_WHITE);
// Bottom pipe
display.fillRect(pipes[i].x, pipes[i].gapY + PIPE_GAP, PIPE_WIDTH, SCREEN_HEIGHT - GROUND_HEIGHT - (pipes[i].gapY + PIPE_GAP), SSD1306_WHITE);
}
// Draw ground
display.fillRect(0, SCREEN_HEIGHT - GROUND_HEIGHT, SCREEN_WIDTH, GROUND_HEIGHT, SSD1306_WHITE);
display.display();
}
// Update score on LCD
void updateScoreDisplay() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Score: ");
lcd.print(score);
}
// Show start screen on OLED
void showStartScreen() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(10, 20);
display.println(F("Flappy Bird"));
display.setCursor(10, 35);
display.println(F("Press button to start"));
display.display();
}
// Show game over on OLED
void showGameOver() {
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(10, 20);
display.println(F("Game Over!"));
display.setCursor(10, 35);
display.print(F("Score: "));
display.println(score);
display.setCursor(10, 50);
display.println(F("Press to restart"));
display.display();
}