/* ===================================
* Arduino Demo
*
* by computerarchiv-muenchen.de
*
* http://demo93.makercafe-muenchen.de
* ===================================
*/
#include <avr/pgmspace.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define UBUTTON_PIN 2 // Push button oben an Digitalpin 2
#define DBUTTON_PIN 3 // Push button unten an Digitalpin 3
#define BUZZER_PIN 6 // Buzzer an Digitalpin 6
// Display
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
#define OLED_RESET -1
#define OLED_ADDRESS 0x3C
Adafruit_SSD1306 display( OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET );
//
#define MAX_OBSTACLES 10 // maximale Anzahl der möglichen Gegnerautos
#define INCREASE_SPEED 20 // alle 20 Frames die Geschwindigkeit erhöhen
#define PLAYER_X 10 // Position X des Spielerautos
#define PLAYER_WIDTH 10 // Breite der Spielefigur in px
#define PLAYER_HEIGHT 8 // Höhe der Autos in px
#define OBSTACLE_WIDTH 10 // Breite der Gegner in px
#define LANES 4 // Anzahl der Spuren
const int LANE_HEIGHT = ( OLED_HEIGHT / LANES ); // Höhe einer Spur
#define FRAME_TIME 40 // Ziel-Framerate: 40 ms pro Frame = 25 FPS
#define BITMAP_SPEED 5 // Pixels per frame
#define MIN_FRAMES_BETWEEN_SPAWNS 20 // Mindestwartezeit pro Spur vor neuem Gegner
#define DEBOUNCE_FRAMES 3 // Anzahl der Frames, in denen kein erneuter Button-Input akzeptiert wird
long frameCounter = 0;
unsigned long lastFrameTime = 0;
unsigned long gameOverTime = 0; // Speichert den Zeitpunkt des Game Over
// Enumeration für mögliche Spielzustände
enum GameState { STARTSCREEN,
HIGHSCORE,
PLAYING };
GameState gameState = STARTSCREEN;
// Achtung: x-Koordinaten, Geschwindigkeiten und zurückgelegte Strecken sind um Faktor 10 hochskaliert.
// Berechnungen erfolgen mit int, um Gleitkommazahlen zu vermeiden.
// Vor der Darstellung auf dem Display wird durch 10 geteilt.
// Datentyp für Eigenschaften eines Gegners
struct Obstacle {
int x;
int lane;
int cartype;
bool active;
};
// High Score Liste
long highscores[ 5 ] = {};
int newHighscoreIndex = -1;
unsigned long highscoreTime = 0;
unsigned long lastBlinkTime = 0;
bool showBlink = true;
// Intro Screen Animation
int introBitmapX1 = 12; // Bild 1 Start off-screen ( left )
int introBitmapX2 = -52; // Bild 2 Start off-screen ( left )
unsigned long lastBitmapMove = 0;
// Eigenschaften für Spieler, Gegner und Fahrspuren
Obstacle obstacles[ MAX_OBSTACLES ]; // Array für Gegner
int playerLane = 2; // Spieler startet in Spur 2
bool gameOver = false;
int spawnProbability = 5; // Start-Wahrscheinlichkeit für neue Gegner ( 1-100 )
int maxActiveObstacles = 5; // Start mit 5 Gegnern
int dashedLineOffset = 0; // Offset für die Bewegung der gestrichelten Linien
long distanceCounter = 0; // Gesamt zurückgelegte Strecke in Pixeln
int playerSpeed = 15; // Geschwindigkeit des Spielers
int laneSpeeds[ LANES ] = { 13, 10, 8, 5 }; // Geschwindigkeiten auf Fahrspuren in px mit Faktor 10
int laneCooldown[ LANES ] = { 0 }; // Zähler für Sperrzeit pro Spur
int debounceCounter = 0;
int lastBuzzerFreq = 0; // Letzte verwendete Frequenz für Buzzer
// Grafiken für Intro, Spieler und Gegner
// konvertiert aus PNG mit https://javl.github.io/image2cpp/
const unsigned char intro_bitmap_[] PROGMEM = {
// 'player_car, 40x24px
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x1f, 0xfe, 0x00, 0x00, 0x00,
0x30, 0x03, 0x00, 0x00, 0x01, 0xe0, 0x01, 0x80,
0x00, 0x02, 0x20, 0x01, 0xff, 0x00, 0x04, 0x20,
0x01, 0xc0, 0x80, 0x08, 0x20, 0x01, 0xc0, 0xf0,
0x33, 0x30, 0x03, 0xc0, 0x08, 0x23, 0x1f, 0xfe,
0xc1, 0xcc, 0x23, 0x0c, 0xfc, 0xc1, 0xe4, 0x23,
0x1c, 0xfe, 0x40, 0x04, 0x23, 0x3c, 0xff, 0x03,
0x84, 0x22, 0x3c, 0xff, 0x03, 0xc4, 0x20, 0x00,
0x00, 0x00, 0x04, 0x20, 0x00, 0x00, 0x00, 0x0c,
0x21, 0xe0, 0x00, 0x1e, 0x08, 0x33, 0x30, 0x00,
0x33, 0x10, 0x3e, 0x10, 0x00, 0x21, 0x30, 0x1e,
0x1f, 0xff, 0xe1, 0xe0, 0x03, 0x30, 0x00, 0x33,
0x00, 0x01, 0xe0, 0x00, 0x1e, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
const unsigned char player_car_[] PROGMEM = {
// 'player_car, 10x8px
0x33, 0x00, 0x7f, 0x80, 0xc4, 0xc0, 0x56, 0x40,
0x56, 0x40, 0xc4, 0xc0, 0x7f, 0x80, 0x33, 0x00
};
// 'car_8x10_1', 10x8px
const unsigned char car_car_8x10_1[] PROGMEM = {
0x63, 0x00, 0xff, 0xc0, 0x82, 0x40, 0x83, 0x40,
0x83, 0x40, 0x82, 0x40, 0xff, 0xc0, 0x63, 0x00
};
// 'car_8x10_2', 10x8px
const unsigned char car_car_8x10_2[] PROGMEM = {
0x63, 0x00, 0xf7, 0x80, 0x9e, 0x40, 0x83, 0x40,
0x83, 0x40, 0x9e, 0x40, 0xf7, 0x80, 0x63, 0x00
};
// Array of all bitmaps for convenience. ( Total bytes used to store images in PROGMEM = 64 )
const int car_allArray_LEN = 2;
const unsigned char* car_allArray[ 2 ] = {
car_car_8x10_1,
car_car_8x10_2
};
// Setup und Game Loop
void setup() {
Serial.begin( 9600 );
// Display initialisieren oder Pause bei Fehler
if ( !display.begin( SSD1306_SWITCHCAPVCC, OLED_ADDRESS ) ) {
Serial.println( F( "SSD1306 allocation failed" ) );
while( 1 ) {
delay( 100 );
}
}
pinMode( UBUTTON_PIN, INPUT_PULLUP ); // Taster oben
pinMode( DBUTTON_PIN, INPUT_PULLUP ); // Taster unten
pinMode( BUZZER_PIN, OUTPUT ); // Buzzer
randomSeed( analogRead( 0 ) );
// display.setRotation( 2 ); // Darstellung umdrehen
resetGame(); // Spieldaten zurücksetzen
gameState = STARTSCREEN; // Zustandsmodus auf STARTSCREEN
}
void loop() {
unsigned long currentTime = millis();
// Wenn FRAME_TIME vergangen, dannn nächster Frame
if ( currentTime - lastFrameTime >= FRAME_TIME ) {
lastFrameTime = currentTime;
switch ( gameState ) {
case STARTSCREEN:
renderStartScreen();
if ( anyKeyPressed() ) {
resetGame();
gameState = PLAYING;
}
break;
case PLAYING:
// Wenn gameOver, dann je nach verstrichener Zeit Zustand wechseln
if ( gameOver ) {
if ( currentTime - gameOverTime > 3000 ) { // 3 Sekunden nach Game Over zum Highscore-Screen
gameState = HIGHSCORE;
highscoreTime = millis();
}
// 1 Sekunde lang Buttonklicks ignorieren, dann neue Runde startbar
if ( ( currentTime - gameOverTime > 1000 ) && anyKeyPressed() ) {
resetGame();
gameState = PLAYING;
}
} else {
handleInput();
updateGameLogic();
}
renderGame();
break;
case HIGHSCORE:
renderHighscoreScreen();
if ( currentTime - gameOverTime > 8000 ) { // Nach 3s Game Over + 5s im Highscore-Screen zum Startscreen
gameState = STARTSCREEN;
}
if ( anyKeyPressed() ) {
resetGame();
gameState = PLAYING;
}
break;
}
}
}
// ---- Spielsteuerung und Logik ------ //
// PULLUP-Logik: LOW bedeutet gedrückt
bool anyKeyPressed() {
return digitalRead( UBUTTON_PIN ) == LOW || digitalRead( DBUTTON_PIN ) == LOW;
}
void handleInput() {
if ( debounceCounter > 0 ) {
debounceCounter--;
return; // Während Debounce-Phase keine Eingaben akzeptieren
}
// Spurwechsel nach oben nur wenn nicht schon ganz oben
if ( digitalRead( UBUTTON_PIN ) == LOW && playerLane > 0 ) {
playerLane--;
debounceCounter = DEBOUNCE_FRAMES;
}
// Spurwechsel nach unten nur wenn nicht schon ganz unten
if ( digitalRead( DBUTTON_PIN ) == LOW && playerLane < LANES - 1 ) {
playerLane++;
debounceCounter = DEBOUNCE_FRAMES;
}
}
// ---- Spiellogik im aktuellen Frame ----- //
void updateGameLogic() {
updateObstacles();
updateBuzzer();
spawnNewObstacle();
checkCollision();
updateDashedLines();
// Geschwindigkeitserhöhung
frameCounter++;
if ( frameCounter % INCREASE_SPEED == 0 ) {
playerSpeed++;
}
// Cooldown pro Spur, damit nicht 2 Gegner unmittelbar hintereinander auf der gleichenm Spur spawnen
for ( int i = 0; i < LANES; i++ ) {
if ( laneCooldown[ i ] > 0 ) {
laneCooldown[ i ]--;
}
}
// Zu Spielbeginn nur 1 Gegner, bis 4 Gegner gleichzeitig rasch anwachsend, dann alle 500 Pixel Bewegung ( ca. 4 Displaybreiten ) ein Gegner mehr.
if ( distanceCounter < 500 ) {
maxActiveObstacles = 1;
} else if ( distanceCounter < 1000 ) {
maxActiveObstacles = 2;
} else if ( distanceCounter < 2000 ) {
maxActiveObstacles = 3;
} else if ( distanceCounter < 3000 ) {
maxActiveObstacles = 4;
} else // Erhöhe max. Gegneranzahl alle 5000 Distanz ( = 500 Pixel ) bis maximal 10
if ( distanceCounter / 5000 + 5 <= 10 ) {
maxActiveObstacles = distanceCounter / 5000 + 5;
}
}
// ---- Kollisions-Check zwischen Spieler und Gegnern ----- //
void checkCollision() {
for ( int i = 0; i < MAX_OBSTACLES; i++ ) {
// collisionMargin verringert den Bereich, der eine Kollision verursacht, ermöglicht "knappe" Überholmanöver
int collisionMargin = 2 * 10; // Skalierungsfaktor *10 berücksichtigen
if ( ( obstacles[ i ].active && obstacles[ i ].x <= ( PLAYER_X * 10 + PLAYER_WIDTH * 10 - collisionMargin ) && ( obstacles[ i ].x + OBSTACLE_WIDTH * 10 ) >= ( PLAYER_X * 10 + collisionMargin ) && obstacles[ i ].lane == playerLane ) ) {
gameOver = true; // Kollision erkannt
gameOverTime = millis();
saveHighscores( distanceCounter ); // Aktuellen Score in Highscore-Liste eintragen
noTone( BUZZER_PIN ); // Tonausgabe stopppen
}
}
// Wenn keine Kollision in diesem Frame erkannt, Score ( = Distanz ) erhöhen
if ( !gameOver ) {
distanceCounter += playerSpeed;
}
}
// ---- Spieleigenschaften zurücksetzen für neue Runde ----- //
void resetGame() {
gameOver = false;
playerLane = 2; // Spieler startet in der 3. Spur von oben
playerSpeed = 15; // Geschwindigkeit zurücksetzen
spawnProbability = 5; // Spawnrate zurücksetzen
frameCounter = 0;
dashedLineOffset = 0;
distanceCounter = 0;
maxActiveObstacles = 5; // Start mit 5 Gegnern
for ( int i = 0; i < LANES; i++ ) {
laneCooldown[ i ] = 0; // Alle Spuren sofort freigeben
}
resetObstacles();
}
// ---- Gegner bewegen und ggf resetten ----- //
// Gegner bewegen sich in die gleiche Richtung wie der Spieler, aber mit geringerer Geschwindigkeit
// Dadurch kommt der Spieler den Gegnern mit der Differenz der Geschwindigkeiten näher
// Fahrspuren haben unterschiedliche Geschwindigkeiten, gespeichert in laneSpeeds[]
void updateObstacles() {
for ( int i = 0; i < MAX_OBSTACLES; i++ ) {
if ( obstacles[ i ].active ) {
// Neue Position des Gegners relativ zum Spieler anhand Geschwindigkeitsunterschied berechnen
obstacles[ i ].x = obstacles[ i ].x - playerSpeed + laneSpeeds[ obstacles[ i ].lane ];
// Wenn Gegner den Anzeigebereich verlässt, dann deaktivieren ( Skalierungsfaktor 10! )
if ( obstacles[ i ].x < -OBSTACLE_WIDTH * 10 ) {
obstacles[ i ].active = false;
}
}
}
}
// ---- Zufällige Erzeugung neuer Gegner ----- //
void spawnNewObstacle() {
// Zähle aktuell aktive Gegner
int activeObstacles = 0;
for ( int i = 0; i < MAX_OBSTACLES; i++ ) {
if ( obstacles[ i ].active ) {
activeObstacles++;
}
}
// Prüfe, ob die aktuelle maximale Anzahl an Gegnern bereits erreicht ist
if ( activeObstacles >= maxActiveObstacles ) {
return; // Keine neuen Gegner spawnen, wenn Limit erreicht
}
// Wähle zufällige Spur für neuen Gegner
int selectedLane = random( 0, LANES );
// Prüfe, ob die Spur eine Cooldown-Phase hat
// Cooldown verhindert, dass zwei Gegner unmittelbar hintereinander auf selber Spur spawnen
if ( laneCooldown[ selectedLane ] > 0 ) {
return; // Kein Spawn auf dieser Spur, wenn noch Cooldown aktiv ist
}
// Suche nach freiem Slot im Array für einen neuen Gegner
for ( int i = 0; i < MAX_OBSTACLES; i++ ) {
if ( !obstacles[ i ].active ) { // Falls Slot frei
if ( random( 100 ) < spawnProbability ) { // Zufällige Wahrscheinlichkeit für neuen Gegner
obstacles[ i ].x = ( OLED_WIDTH + 10 ) * 10; // Startposition außerhalb des Bildschirms
obstacles[ i ].lane = selectedLane; // Gewählte Spur
obstacles[ i ].cartype = random( 2 ); // Zufälliges Sprite
obstacles[ i ].active = true;
laneCooldown[ selectedLane ] = MIN_FRAMES_BETWEEN_SPAWNS; // Cooldown für diese Spur setzen
return;
}
}
}
}
// ---- Alle Hindernisse initialisieren ------ //
void resetObstacles() {
for ( int i = 0; i < MAX_OBSTACLES; i++ ) {
obstacles[ i ].x = 0; // X-Position = Position entlang der Fahrspur
obstacles[ i ].lane = 0; // zugewiesene Fahrspur
obstacles[ i ].cartype = 0; // 2 verschiedene Sprites
obstacles[ i ].active = false; // gerade aktiv oder nicht?
}
}
// ---- Rendering des Spielfelds im aktuellen Frame ------ //
void renderGame() {
display.clearDisplay(); // Display löschen
drawDashedLines(); // Trennlinien zeichnen
drawObstacles(); // Gegner zeichnen
drawPlayer(); // Spieler zeichnen
if ( gameOver ) drawGameOver(); // Wenn Game over, dann Overlay ausgeben
drawDistanceCounter(); // Punktezahl anzeigen
display.display(); // Displayinhalt ausgeben
}
// ---- Gestrichelte Linie mit Spielergeschwindigkeit rückwärts bewegen ------ //
void updateDashedLines() {
dashedLineOffset -= playerSpeed;
if ( dashedLineOffset < -100 ) {
dashedLineOffset = 0;
}
}
// ---- Startscreen mit Animation ausgeben ------ //
void renderStartScreen() {
display.clearDisplay();
// Position zweier Bitmaps berechnen, soll wie ein endlos durchfahrendes Auto aussehen
if ( millis() - lastBitmapMove > 50 ) { // Alle 50ms ein Update
introBitmapX1 += BITMAP_SPEED; // Bitmap 1 nach unten bewegen
if ( introBitmapX1 > OLED_WIDTH + 12 ) { // Position nach oben außerhalb zurücksetzen, wenn unten aus dem Bild
introBitmapX1 = -52;
}
introBitmapX2 += BITMAP_SPEED; // Bitmap 2 ( gleiches Bild, aber nach unten versetzt ) nach unten bewegen
if ( introBitmapX2 > OLED_WIDTH + 12 ) { // Position nach oben außerhalb zurücksetzen, wenn unten aus dem Bild
introBitmapX2 = -52;
}
lastBitmapMove = millis();
}
// Beide Bitmaps anzeigen, erzeugt Endlosbewegung von oben nach unten
display.drawBitmap( introBitmapX1, 0, player_car_, 10, 8, SSD1306_WHITE );
display.drawBitmap( introBitmapX2, 0, player_car_, 10, 8, SSD1306_WHITE );
// Texte auf Startseite anzeigen
display.setTextSize( 2 );
display.setTextColor( SSD1306_WHITE );
display.setCursor( 5, 15 );
display.println( "Pixel Race" );
// display.setCursor( 3, 66 );
// display.println( "Racer" );
display.setTextSize( 1 );
display.setCursor( 22, 45 );
display.println( "Taste druecken" );
display.display();
}
// ---- Top Highscores ausgeben ------ //
void renderHighscoreScreen() {
display.clearDisplay();
display.setCursor( 2, 6 );
display.drawFastHLine( 2, 16, 60, SSD1306_WHITE );
display.setTextSize( 1 );
display.setTextColor( SSD1306_WHITE );
display.println( "HIGHSCORES" );
// Aktuell erreichter Score soll blinken, wenn in Top10, Blinkstatus alle 100ms ändern
if ( millis() - lastBlinkTime > 100 ) {
showBlink = !showBlink;
lastBlinkTime = millis();
}
// Liste ausgeben, aktuellen Score blinken lassen falls in Liste enthalten
for ( int i = 0; i < 4; i++ ) {
display.setCursor( 10, 20 + ( i * 10 ) ); // Cursor an Position in der Liste
display.print( i + 1 );
display.print( ". " );
int16_t x1, y1;
uint16_t textWidth, textHeight;
char scoreBuffer[ 5 ]; // Buffer für die Zahl als String
sprintf( scoreBuffer, "%ld", highscores[ i ] / 10 ); // Zahl in String umwandeln
display.getTextBounds( scoreBuffer, 0, 0, &x1, &y1, &textWidth, &textHeight ); // Breite des Scores berechnen
display.setCursor( 55 - textWidth, 20 + ( i * 10 ) ); // Cursor setzen für linksbündige Anzeige
// Wenn aktueller Score, dann blinken ( =alle 100ms nicht anzeigen )
// Das continue beendet in dem Fall diesen Durchlauf der for-Schleife, springt sofort zum nächsten Durchlauf,
// dadurch wird der Score in der folgenden Zeile alle 100ms nicht ausgegeben und blinkt
if ( i == newHighscoreIndex && !showBlink ) {
continue;
}
display.print( highscores[ i ] / 10 ); // Score = zurückgelegte Distanz durch Skalierungsfaktor 10 geteilt
}
display.display(); // anzeigen
}
// ---- Gestrichelte Linien zwischen Fahrspuren zeichnen ------ //
void drawDashedLines() {
// Gestrichelte Linien zwischen den Spuren
for ( int i = 1; i < LANES; i++ ) {
for ( int j = 0; j < OLED_WIDTH; j += 10 ) {
// dashedLineOffset wird pro Frame in updateDashedLines() verringert , dadurch "bewegt" sich die Linie
display.drawFastHLine( ( j + dashedLineOffset / 10 ) % OLED_WIDTH, i * LANE_HEIGHT, 6, SSD1306_WHITE );
}
}
}
// ---- Gegnersprites ausgeben ------ //
void drawObstacles() {
for ( int i = 0; i < MAX_OBSTACLES; i++ ) {
if ( obstacles[ i ].active ) {
// x-Koordinate wird durch Skalierungsfaktor 10 geteilt, um in echten Pixeln zu zeichnen
display.drawBitmap( obstacles[ i ].x / 10, lanePosition( obstacles[ i ].lane ), car_allArray[ obstacles[ i ].cartype ], 10, 8, SSD1306_WHITE );
}
}
}
// ---- Spielersprite an aktuelle Position ------ //
void drawPlayer() {
display.drawBitmap( PLAYER_X, lanePosition( playerLane ), player_car_, 10, 8, SSD1306_WHITE );
}
// ---- Aktuelle Distanz/Punktezahl oben ins Display schreiben ------ //
void drawDistanceCounter() {
display.setTextSize( 1 );
display.setTextColor( SSD1306_WHITE );
int16_t x1, y1;
uint16_t textWidth, textHeight;
char scoreBuffer[ 10 ]; // Buffer für die Zahl als String
sprintf( scoreBuffer, "%ld", distanceCounter / 10 ); // Wandelt Zahl in String um, dividiert durch Skalierungsfaktor 10
display.getTextBounds( scoreBuffer, 0, 0, &x1, &y1, &textWidth, &textHeight ); // Score-Breite berechnen für Zentrierung
display.fillRect( OLED_WIDTH / 2 - textWidth / 2 - 2, 0, textWidth + 4, 8, SSD1306_BLACK ); // Rechteck für Punktestand leeren
display.setCursor( OLED_WIDTH / 2 - textWidth / 2, 0 ); // Cursor setzen, Score mittig ausgeben
display.print( distanceCounter / 10 ); // Um Skalierungsfaktor 10 verringern
}
// ---- Game over Overlay über Spielfeld anzeigen ------ //
void drawGameOver() {
display.fillRect( 10, 10, 107, 54, SSD1306_WHITE );
display.setCursor( 10, 15 );
display.setTextSize( 2 );
display.setTextColor( SSD1306_BLACK );
display.println( "GAME OVER" );
display.setTextSize( 1 );
display.setCursor( 34, 35 );
display.println( "Your score" );
// display.setCursor( 30, 80 );
int16_t x1, y1;
uint16_t textWidth, textHeight;
char scoreBuffer[ 10 ]; // Buffer für die Zahl als String
sprintf( scoreBuffer, "%ld", distanceCounter / 10 ); // Wandelt Zahl in String um, dividiert durch Saklierungsfaktor 10
display.setTextSize( 2 );
display.getTextBounds( scoreBuffer, 0, 0, &x1, &y1, &textWidth, &textHeight ); // Score-Breite berechnen für Zentrierung
display.setCursor( 64 - textWidth / 2, 45 );
display.println( distanceCounter / 10 ); // Durch Skalierungsfaktor 10 dividieren
}
// ---- Highscore in Liste der Top10 einsortieren ------ //
void saveHighscores( long newScore ) {
newHighscoreIndex = -1; // Position speichern, falls Ergebnis in Top 10
for ( int i = 0; i < 5; i++ ) { // Neuen Score in das richtige Ranking einfügen
if ( newScore > highscores[ i ] ) { // Neuer Score ist besser?
for ( int j = 4; j > i; j-- ) { // Alte Werte nach unten verschieben
highscores[ j ] = highscores[ j - 1 ];
}
highscores[ i ] = newScore; // Neuen Highscore einfügen
newHighscoreIndex = i; // Index des neuen Scores speichern für Blink-Animation
break;
}
}
}
// ---- Buzzerfrequenz langsam mit playerSpeed ändern ------ //
void updateBuzzer() {
int buzzerFreq = map( playerSpeed, 10, 100, 50, 70 ); // Map playerSpeed zu Frequenz
if ( buzzerFreq != lastBuzzerFreq ) { // Nur updaten wenn sich Frequenz geändert hat
tone( BUZZER_PIN, buzzerFreq );
lastBuzzerFreq = buzzerFreq;
}
}
// ---- Y-Positionen für Autos auf Fahrspuren berechnen ------ //
int lanePosition( int lane ) {
return lane * LANE_HEIGHT + ( LANE_HEIGHT - PLAYER_HEIGHT ) / 2;
}