/* ===================================
* Arduino Demo
*
* by computerarchiv-muenchen.de
*
* http://demo95.makercafe-muenchen.de
* ===================================
*/
#include <avr/pgmspace.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// Hardware-Einstellungen
#define BUTTON_PIN 2 // Taster an Pin 2
#define OLED_WIDTH 128 // Display Breite
#define OLED_HEIGHT 64 // Display Höhe
#define OLED_RESET -1
#define OLED_ADDRESS 0x3C // Display Adresse
Adafruit_SSD1306 display( OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET );
// Spiel-Einstellungen
#define PIPE_SPEED 10 // Spielgeschwindigkeit
#define MAX_PIPES 3 // Hindernisse
#define JUMP_STRENGTH -25 // Sprung-Beschleunigung bei gedrücktem Taster
#define GRAVITY 5 // Schwerkraft, die pro Frame auf Spielfigur einwirkt
#define SCALE_FACTOR 10 // Interne Skalierung um Faktor 10 für präzisere Bewegungen
#define FRAME_TIME 40 // 40ms pro Frame = 25 FPS
// Spielfigur, konvertiert aus PNG mit https://javl.github.io/image2cpp/
// 'bird1', 10x8px
#define BIRD_WIDTH_PX 10
#define BIRD_HEIGHT_PX 8
const unsigned char epd_bitmap_bird1[] PROGMEM = {
0x0e, 0x00, 0x15, 0x00,
0x60, 0x80, 0x81, 0x80,
0xfc, 0xc0, 0xfb, 0x80,
0x71, 0x00, 0x1e, 0x00
};
// 'bird2', 10x8px
const unsigned char epd_bitmap_bird2[] PROGMEM = {
0x0e, 0x00, 0x15, 0x00,
0x70, 0x80, 0xf9, 0x80,
0xfc, 0xc0, 0x83, 0x80,
0x71, 0x00, 0x1e, 0x00
};
// Array of all bitmaps for convenience. ( Total bytes used to store images in PROGMEM = 64 )
const unsigned char* epd_bitmap_allArray[ 2 ] = {
epd_bitmap_bird1,
epd_bitmap_bird2
};
// Hinderniss
const int PIPE_WIDTH = SCALE_FACTOR * 15; // Rohre an Display 15px breit
const int PIPE_GAP = SCALE_FACTOR * 30; // Lücke in Rohren 30px hoch
const int PIPE_SPACING = SCALE_FACTOR * 65; // Default Abstand zwischen 2 Pipes
struct Pipe{
int x;
int height;
};
Pipe pipes[ MAX_PIPES ];
// Variablen
int birdY; // Vertikale Position des Spielers
float velocity = 0; // Kombinierte vertikale Geschwindigkeit der Spielfigur
bool gameOver = false;
unsigned long frameCounter = 0;
unsigned long lastFrameTime = 0;
unsigned long gameOverTime = 0;
int score = 0; // Punktestand
long highscores[ 5 ] = {}; // Highscore
int newHighscoreIndex = -1;
unsigned long highscoreTime = 0;
bool showBlink = true;
unsigned long lastBlinkTime = 0;
// Zustandmodus des Programms
enum GameState{
STARTSCREEN,
HIGHSCORE,
PLAYING
};
GameState gameState = STARTSCREEN;
void setup() {
Serial.begin( 9600 );
pinMode( BUTTON_PIN, INPUT_PULLUP );
randomSeed( analogRead( 0 ) );
// Display initialisieren oder Pause bei Fehler
if( !display.begin( SSD1306_SWITCHCAPVCC, OLED_ADDRESS ) ) {
Serial.println( F( "SSD1306 allocation failed" ) );
while ( 1 ) {
delay( 100 );
}
}
gameState = STARTSCREEN;
}
void loop() {
unsigned long currentTime = millis();
// Nächsten Frame erst nach FRAME_TIME berechnen lassen
if( currentTime - lastFrameTime < FRAME_TIME ) return;
lastFrameTime = currentTime;
frameCounter++;
switch ( gameState ) {
case STARTSCREEN:
renderStartScreen();
if( anyKeyPressed() ) {
resetGame(); // Variablen zurücksetzen
gameState = PLAYING; // Spiel starten
}
break;
case PLAYING:
if( gameOver ) {
// Bei Game over nach 3 Sekunden automatisch zum Startscreen wechseln
if( millis() - gameOverTime > 3000 ) {
gameState = HIGHSCORE;
highscoreTime = millis();
}
// Oder Spieler drückt den Taster ( 1 sec Zwangspause ), dann sofort neues Spiel starten
if( (millis() - gameOverTime > 1000)
&& anyKeyPressed() ) {
resetGame();
gameState = PLAYING;
}
} else {
// Wenn Button gedrückt, negative Beschleunigung ( nach oben ) addieren
if( digitalRead( BUTTON_PIN ) == LOW ) {
velocity = JUMP_STRENGTH;
}
velocity += GRAVITY; // Schwerkraft anwenden ( positiver Wert = nach unten )
velocity = constrain( velocity, -50, 50 ); // Begrenzung, damit Spielfigur nicht zu schnell fällt oder steigt
birdY += velocity; // Position der Spielfigur berechnen
if( birdY < 0 ) birdY = 0; // Spielfigur in der Anzeige halten
movePipes();
checkCollision();
}
drawGame();
break;
case HIGHSCORE:
renderHighscoreScreen();
// Nach 8 sec ohne Tastendruck zum Startscreen wechseln
if( millis() - gameOverTime > 8000 ) {
gameState = STARTSCREEN;
}
// oder neues Spiel starten wenn Tastendruck
if( anyKeyPressed() ) {
resetGame();
gameState = PLAYING;
}
break;
}
}
// PULLUP-Logik: LOW bedeutet gedrückt
bool anyKeyPressed() {
return digitalRead( BUTTON_PIN ) == LOW;
}
// ---- Reset Spiel ---- //
void resetGame() {
velocity = 0; // Schwerkraft
score = 0; // Punktestand
gameOver = false; // Spielzustand
birdY = OLED_HEIGHT * SCALE_FACTOR / 5; // Startposition der Spielfigur auf 1/5 der Bildschirmhöhe
pipes[ 0 ].x = OLED_WIDTH * SCALE_FACTOR; // Erstes Hinderniss außerhalb des Bildschirmrandes generieren
pipes[ 0 ].height = generatePipeHeight(); // Zufällige Höhe des Hinderniss generieren
// weitere Hindernisse mit zufälligem Abstand erzeugen
for ( int i = 1; i < MAX_PIPES; i++ ) {
pipes[ i ].x = OLED_WIDTH * SCALE_FACTOR + i * PIPE_SPACING + random( -20, 20 ) * SCALE_FACTOR;
pipes[ i ].height = generatePipeHeight();
}
}
// Zufällige Höhe des oberen Hinderniss mit Platz für Öffnung und 10px für unteren Teil
int generatePipeHeight() {
return random( 10 * SCALE_FACTOR, OLED_HEIGHT * SCALE_FACTOR - PIPE_GAP - 10 * SCALE_FACTOR );
}
// Hindernisse auf Spielfigur zu bewegen
void movePipes() {
int currentSpeed = PIPE_SPEED + score; // Pipes bewegen sich immer schneller
// Position der Hindernisse nach links verschieben
for ( int i = 0; i < MAX_PIPES; i++ ) {
pipes[ i ].x -= currentSpeed; // Geschwindigkeit
// Falls Hinderniss den linken Rand verlässt, sofort neues Hindernisse am rechten Rand erzeugen
if( pipes[ i ].x < -PIPE_WIDTH ) {
pipes[ i ].x = findFurthestPipe() + PIPE_SPACING + random( -20, 20 ) * SCALE_FACTOR; // Setze neues Hinderniss ganz rechts
pipes[ i ].height = generatePipeHeight();
score++; // Spielstand erhöhen
}
}
}
// Hilfsfunktion, um die am weitesten rechts stehende Pipe zu finden
int findFurthestPipe() {
int maxX = 0;
for ( int i = 0; i < MAX_PIPES; i++ ) {
if( pipes[ i ].x > maxX ) {
maxX = pipes[ i ].x;
}
}
return maxX;
}
// Kollisionserkennung
void checkCollision() {
// Kollisionsprüfung mit um 2px verkleinerter Hitbox ( Collision Forgiveness ),
// da Spielfigur eher rund ist → vermeidet unfaire Treffer an Ecken der Spielfigur
for ( int i = 0; i < MAX_PIPES; i++ ) {
// Prüfen, ob die Spielfigur horizontal mit einem Rohr überlappt
if( pipes[ i ].x < ( 15 + 2 ) * SCALE_FACTOR && pipes[ i ].x + PIPE_WIDTH > ( 15 - 2 ) * SCALE_FACTOR ) {
// Prüfen, ob die Spielfigur vertikal gegen das Rohr stößt ( oben oder unten )
if( ( birdY - 2 * SCALE_FACTOR ) < pipes[ i ].height
|| ( birdY + 2 * SCALE_FACTOR ) > pipes[ i ].height + PIPE_GAP ) {
gameOver = true;
gameOverTime = millis();
saveHighscores( score );
}
}
}
// Prüfen, ob Spielfigur den unteren Rand verlassen hat
if( birdY >= OLED_HEIGHT * SCALE_FACTOR ) {
gameOver = true;
gameOverTime = millis();
saveHighscores( score );
}
}
// Spielszene zeichnen
void drawGame() {
display.clearDisplay(); // Anzeige löschen
drawBird(); // Spielfigur darstellen
drawPipes(); // Hindernisse darstellen
drawScore(); // Punktezahl darstellen
// Falls Game over, dann Spielende anzeigen
if( gameOver ) displayGameOver();
display.display(); // Anzeige aktualisieren
}
void drawBird() {
int birdFrame = ( frameCounter / 5 ) % 2; // // alle 5 Frames abwechselnde Grafik der Spielfigur dargestellen
display.drawBitmap(
15 - BIRD_WIDTH_PX / 2,
birdY / SCALE_FACTOR - BIRD_HEIGHT_PX / 2,
epd_bitmap_allArray[ birdFrame ],
BIRD_WIDTH_PX,
BIRD_HEIGHT_PX,
SSD1306_WHITE );
}
void drawPipes() {
for ( int pipeIndex = 0; pipeIndex < MAX_PIPES; pipeIndex++ ) {
int pipeWidth = PIPE_WIDTH / SCALE_FACTOR;
int gapHeight = PIPE_GAP / SCALE_FACTOR;
int pipeX = pipes[ pipeIndex ].x / SCALE_FACTOR;
int upperPipeHeight = pipes[ pipeIndex ].height / SCALE_FACTOR;
int lowerPipeY = upperPipeHeight + gapHeight;
int lowerPipeHeight = OLED_HEIGHT - lowerPipeY;
// Oberes Rohrsegment ( vertikal )
display.fillRect(
pipeX,
0,
pipeWidth,
upperPipeHeight - 6,
SSD1306_WHITE );
// Rohrkopf oben ( horizontaler Abschluss )
display.fillRect(
pipeX - 2,
upperPipeHeight - 6,
pipeWidth + 4,
6,
SSD1306_WHITE );
// Rohrkopf unten ( horizontaler Abschluss unterhalb der Lücke )
display.fillRect(
pipeX - 2,
lowerPipeY,
pipeWidth + 4,
6,
SSD1306_WHITE );
// Unteres Rohrsegment ( vertikal )
display.fillRect(
pipeX,
lowerPipeY + 6,
pipeWidth,
lowerPipeHeight - 6,
SSD1306_WHITE );
}
}
// Rechts oben im Display aktuelle Punktezahl anzeigen
void drawScore() {
display.setTextSize( 1 );
display.setTextColor( SSD1306_WHITE );
display.setCursor( 110, 2 );
display.print( score );
}
// ---- Startscreen anzeigen ---- //
void renderStartScreen() {
display.clearDisplay();
display.setTextSize( 2 );
display.setTextColor( SSD1306_WHITE );
display.setCursor( 5, 20 );
display.println( "Happy Bird" );
display.setTextSize( 1 );
display.setCursor( 25, 45 );
display.println( "Taste druecken" );
display.display();
}
// ---- Game Over anzeigen ---- //
void displayGameOver() {
display.fillRect(
20,
5,
OLED_WIDTH - 40,
OLED_HEIGHT - 10,
SSD1306_WHITE );
display.setTextColor( SSD1306_BLACK );
display.setTextSize( 1 );
display.setCursor( 38, 10 );
display.println( "GAME OVER" );
display.setCursor( 30, 20 );
display.println( "Punktestand" );
int16_t x1, y1;
uint16_t textWidth, textHeight;
char scoreBuffer[ 10 ]; // Buffer für Zahl als Zeichenkette
sprintf( scoreBuffer, "%d", score ); // Zahl in Zeichenkette umwandeln
display.setTextSize( 2 );
display.getTextBounds( scoreBuffer, 0, 0, &x1, &y1, &textWidth, &textHeight ); // Score-Breite berechnen für Zentrierung
display.setCursor( 64 - textWidth / 2, 36 );
// display.setCursor( 40, 26 );
display.println( scoreBuffer );
}
// Highscore Liste anzeigen
void renderHighscoreScreen() {
display.clearDisplay();
display.setTextSize( 1 );
display.setTextColor( SSD1306_WHITE );
display.setCursor( 25, 2 );
display.println( "HIGHSCORES" );
display.setTextSize( 1 );
// Aktuell erreichter Score soll blinken, wenn in Top10, Blinkstatus alle 100ms ändern
if( millis() - lastBlinkTime > 100 ) {
showBlink = !showBlink;
lastBlinkTime = millis();
}
for ( int i = 0; i < 5; i++ ) {
if( i == newHighscoreIndex && !showBlink ) {
continue;
}
display.setCursor( 30, 14 + ( i * 9 ) );
display.print( i + 1 );
display.print( ". " );
display.print( highscores[ i ] );
}
display.display();
}
// Neuen Highscore eintragen
void saveHighscores( int newScore ) {
newHighscoreIndex = -1;
// neuer Score wird mit bestehenden Scores verglichen und ggf richtig einsortiert
for ( int i = 0; i < 5; i++ ) {
if( newScore > highscores[ i ] ) {
for ( int j = 4; j > i; j-- ) {
highscores[ j ] = highscores[ j - 1 ];
}
highscores[ i ] = newScore;
newHighscoreIndex = i;
break;
}
}
}