Hilfe bei meinem Dino-Spiel!

Hallo zusammen!
Ich arbeite aktuell an einem Ableger des Chrome Dino Spiels für den Arduino. Zur Darstellung verwende ich ein LCD-Display und ein 4x3 Keypad. Der Dino soll sich nach dem Starten des Spiels automatisch auf der unteren Reihe des Displays nach rechts Richtung Hindernis bewegen. Dies geschieht allerdings nicht. Kann mir jemand helfen und sagen, wieso? Hat jemand Verbesserungsvorschläge oder Alternativlösungen? Vielen Dank im Voraus!

Hier mein Code:


#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);  // I2C-Display-Adresse und Zeilenzahl

const byte ROWS = 4; //three rows
const byte COLS = 3; //four columns
char keys[ROWS][COLS] = {
  {'1','2','3'},
  {'4','5','6'},
  {'7','8','9'},
  {'*','0','#'}
};
byte rowPins[ROWS] = {8, 7, 6, 5}; //connect to the row pinouts of the keypad
byte colPins[COLS] = {4, 3, 2}; //connect to the column pinouts of the keypad

Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);

int dinoX = 1; // X-wert des ">"
int dinoY = 1; // Y-wert des ">"

int obstacleX = 15; //X-wert des Hindernisses "#"
int obstacleY = 1;  //Y-wert des Hindernisses "#"

int gameStarted = 0; //wird später zum starten auf 1 gesetzt




void setup() { //LCD Display initialisieren und "Startmenü" anzeigen
  Serial.begin(9600);
  lcd.init();
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print("Press 0 to start");
  lcd.setCursor(0, 1);
  lcd.print("FAKE DINO RUNNER");

  pinMode(2, INPUT); //pins für das keypad
  pinMode(3, INPUT);
  pinMode(4, INPUT);
  pinMode(6, INPUT);
  pinMode(6, INPUT);
  pinMode(7, INPUT);
  pinMode(8, INPUT);
  
}




//LOOP
void loop() {

  char key = keypad.getKey(); // Wenn "O" auf dem keypad gedrückt wird, soll das Unterprogramm startGame() ausgeführt werden
  if(key == '0'){
    delay(1000);
    startGame();
    gameStarted == 1;
  
  }

  moveGame(); 
  updateGame();
}



void moveGame() { // automatische Bewegung des ">" (Dinos) nach rechts Richtung Hinderniss "#"

  if(gameStarted == 1){
    for(dinoX=1;dinoX<16;dinoX++){
    Serial.println(dinoX);
    lcd.clear();
    lcd.setCursor(dinoX, dinoY);
    lcd.print(">");
    delay(250);
    }            

  }

}


void updateGame() {

  char key = keypad.getKey();  // mit der Taste "2" soll man später springen können, aktuell unrelevant
  if (key == '2') {
    dinoY = 0;
  }

  if (dinoX == obstacleX && dinoY == obstacleY) { // wenn der "Dino" im Hinderniss ist, hat man verloren
    endGame();
  }
}



void endGame() { // GAME-OVER screen 
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("GAME OVER");
  lcd.setCursor(0, 1);
  lcd.print("Press 0 to restart");
  obstacleX = 15;
  dinoY = 1;
}

void startGame() { //">" und "#", Dino und Hinderniss werden auf das Display geprintet
  lcd.clear();
  lcd.setCursor(dinoX, dinoY);
  lcd.print(">");
  lcd.setCursor(obstacleX, obstacleY);
  lcd.print("#");
}

Hallo peterandreas123

Ich gehe davon aus, dass du das Programm selbst geschrieben haben, dann ist es recht einfach, den Fehler zu finden.

Es gibt einen Trick, um herauszufinden, warum etwas nicht funktioniert:
Verwende einen Logik-Analysator, um zu sehen, was passiert.

Füge an verschiedenen Stellen im Code Serial.print-Anweisungen als Diagnoseausdrucke ein, um die Werte der Variablen, insbesondere derjenigen, die den Dino steuern, zu sehen und zu prüfen, ob sie Ihren Erwartungen entsprechen.

Ich wünsche einen schönen Tag und viel Spaß beim Programmieren in C++.

1 Like

Hi @peterandreas123,

bist Du noch am Tüfteln?

ec2021

konkret zum Problem:

gamestarted == 1; macht nicht das, was du möchtest. '==' ist für Vergleiche und '=' für Zuweisungen. Also: gamestarted = 1;

Verbesserungs- und Alternativvorschläge:
Da sehe ich noch einige Baustellen auf dich zukommen. Ich würde die Entwicklungsschritte anders abarbeiten:

  • Menu (warten auf Spielstart) und Kollisionsabfrage erstmal außer Acht lassen.
  • Deinen Dino erstmal nur von links nach rechts über den Bildschirm wandern lassen und dann wieder von vorn (Sicher, daß er sich aktiv von links nach rechts bewegen soll anstatt die Hindernisse von rechts nach links zu bewegen/scrollen?)
  • wenn die Bewegung klappt, bring ihm das Springen bei (Tastenabfrage während der Zeit zwischen den einzelnen Bewegungsschritten, wie lange in der Luft bleiben, evtl. Ducken hinzufügen etc.)
  • dann die Kollisionsabfrage mit den Hindernissen hinzufügen
  • Level (steigender Schwierigkeitsgrad, Geschwindigkeit, Häufigkeit der Hindernisse etc) anlegen
  • Zum Schluss erst das Menu drumrumbasteln
  • Dino animieren :sauropod:

...und falls du das Jump'n'Run-Rad nicht komplett neu erfinden willst, schau dir doch mal LCD Hill Run an. Entweder zur Inspiration oder um gleich nur aus dem Strichmännchen einen Dino zu machen.. :wink:

1 Like

@ec2021 Ja :slight_smile:

Vielen Dank für deine Hilfe! ich werde mir deine Ratschläge mal anschauen.

Bist Du mehr daran interessiert, das Vorgehen zu erlernen ... oder an einer Lösung?

An einer Lösung :wink:

Nur für das eine Deiner Probleme? :wink:

Eine (weitgehend) funktionierende Lösung sieht schon ziemlich anders als Dein bisheriger Sketch ...

Was wäre denn dein Vorschlag? :slight_smile:

Ich habe vor zwei Stunden Deinen Sketch genau so, wie er oben ist, in Wokwi übertragen:

und danach eine Lösung mit Zustandsmaschine geschrieben, die schon ziemlich weit gediehen ist.

Es ist aber Deine Entscheidung, ob Du schrittweise zum eigenen Programm kommen möchtest, oder ob Du das meine übernehmen möchtest.

Der Ball liegt bei Dir :wink:

Dies ist mein erstes richtiges Projekt, an das ich mich rangetraut habe, ich bin also noch nicht erfahren. Wäre echt super wenn du eine Lösung parat hättest, da ich echt schon lanmge daran rumtüftle :wink:

Du hast es so gewollt :wink:

Ich bin gerade dabei den Code etwas zu kommentieren, Du kannst aber gerne mal reinschauen:

Noch wenig kommentierter Sketch ... ist gerade in der Mache ...
/*
  Forum: https://forum.arduino.cc/t/hilfe-bei-meinem-dino-spiel/1195564
  Wokwi: https://wokwi.com/projects/382919062988146689

  Sehr weitgehend umgeschrieben auf eine einfache "State Machine"
  mit 
    - Leveln, bei denen die Position des Hindernisses jeweils per Zufall neu gesetzt wird
              und die Geschwindigkeit des ">" Zeichens zunimmt (nur bis Level 9)
    - 

*/

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);  // I2C-Display-Adresse und Zeilenzahl

const byte ROWS = 4; //three rows
const byte COLS = 3; //four columns
char keys[ROWS][COLS] = {
  {'1', '2', '3'},
  {'4', '5', '6'},
  {'7', '8', '9'},
  {'*', '0', '#'}
};
byte rowPins[ROWS] = {8, 7, 6, 5}; //connect to the row pinouts of the keypad
byte colPins[COLS] = {4, 3, 2}; //connect to the column pinouts of the keypad

Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);

struct position {
  byte x;
  byte y ;
};

position dino {1, 1};
position hindernis {10, 1};
static char richtung;
byte schritteOben = 0;
constexpr byte erlaubterSprung = 2;
int level = 0;


int gameStarted = 0; //wird später zum starten auf 1 gesetzt

enum zustandsAuswahl {ANSAGE, DINOLAEUFT,PAUSE, GAMEOVER};
zustandsAuswahl zustand = ANSAGE;
unsigned long zuletztBewegt = 0;
unsigned long zeitProSchritt {550};  // Jede Sekunde ein Schritt


void setup() { //LCD Display initialisieren und "Startmenü" anzeigen
  Serial.begin(115200);
  lcd.init();
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print("Press 0 to start");
  lcd.setCursor(0, 1);
  lcd.print("FAKE DINO RUNNER");
  Serial.println("Startschirm");
}


void loop() {
  zustandsMaschine();
}

void zustandsMaschine() {
  char key = keypad.getKey();
  switch (zustand) {
    case ANSAGE:
      if (key == '0') {
        startGame();
        zustand =  DINOLAEUFT;
      }
      break;
    case DINOLAEUFT:
      bewegeDino(key);
      if (key == '5'){
        zustand = PAUSE;
      }
      break;
    case PAUSE:
       if (key != NO_KEY) {
        zustand = DINOLAEUFT;
       }
       break;  
    case GAMEOVER:
      gameOver();
      zustand = ANSAGE;
      break;
  }
}

void bewegeDino(char aKey) { // automatische Bewegung des ">" (Dinos) nach rechts Richtung Hinderniss "#"
  if (aKey != NO_KEY) {
    richtung = aKey;
  }
  if (millis() - zuletztBewegt >= zeitProSchritt) {
    zuletztBewegt = millis();
    lcd.setCursor(dino.x, dino.y);
    lcd.print(" ");
    if (richtung == '2') {
      dino.y = 0;
      richtung = NO_KEY;
    }
    if (richtung == '8') {
      dino.y = 1;
      richtung = NO_KEY;
      schritteOben = 0;
    }
    if (schritteOben > erlaubterSprung) {
      dino.y = 1;
      schritteOben = 0;
    }
    dino.x++;
    if (kollision()) {
      zustand = GAMEOVER;
      Serial.println("Game Over");
    } else {
      if (dino.x >= 16) {
        dino.x = 1;
        dino.y = 1;
        setzeHindernis(true);
      }
      lcd.setCursor(dino.x, dino.y);
      lcd.print(">");
      if (dino.y == 0) {
        schritteOben++;
      }
    }
  }
}

boolean kollision() {
  return (dino.x == hindernis.x && dino.y == hindernis.y);
}

void setzeHindernis(boolean loeschen) {
  if (loeschen) {
    lcd.setCursor(hindernis.x, hindernis.y);
    lcd.print(" ");
  }
  hindernis.x = random(5, 16);
  lcd.setCursor(hindernis.x, hindernis.y);
  lcd.print("#");
  level++;
  if (level < 10) {
    zeitProSchritt = 550 - level * 50;
  }
  Serial.print("Level : ");
  Serial.println(level);
}

void startGame() { //">" und "#", Dino und Hindernis werden auf das Display geprintet
  Serial.println("Dino läuft ..");
  lcd.clear();
  schritteOben = 0;
  level = 0;
  zeitProSchritt = 550;
  dino.x = 1;
  dino.y = 1;
  lcd.setCursor(dino.x, dino.y);
  lcd.print(">");
  setzeHindernis(false);
}

void gameOver() { // GAME-OVER screen
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("   GAME OVER");
  lcd.setCursor(0, 1);
  lcd.print("Restart with 0");
}

// Falls die Daten mal geprüft werden müssen ...
void printPos() {
  Serial.print("dino.x ");
  Serial.println(dino.x);
  Serial.print("dino.y ");
  Serial.println(dino.y);
  Serial.print("hindernis.x ");
  Serial.println(hindernis.x);
  Serial.print("hindernis.y ");
  Serial.println(hindernis.y);
}


1 Like

Ich habe auch bei Chrome Dino I mal den Vorschlag von @derGeppi umgesetzt und jetzt bewegt er sich! Ich bin dir sehr dankbar für deine Hilfe! Vielen vielen Dank! :heart_decoration:

Der ist ja auch schon fix und fertig spielbar.. cool :wink: :+1:

Ist bestimmt noch verbesserbar, aber für 2h von Anfang bis Ende bin ich einigermaßen zufrieden :wink:

Es drängt sich eigentlich auf, die Bewegungsfunktionen für "Dino" und "Hindernis" in eine Klasse zu vereinen, Da reduziert Fehlermöglichkeiten und erhöht letztlich die Les- und Erweiterbarkeit ...

Mal sehen ...
:slight_smile:

Ich kommentiere erstmal den aktuellen Stand, damit der Code besser nachvollzogen werden kann.

Hier die kommentierte Version:

/*
  Forum: https://forum.arduino.cc/t/hilfe-bei-meinem-dino-spiel/1195564
  Wokwi: https://wokwi.com/projects/382927977210321921

  Sehr weitgehend umgeschrieben auf eine einfache "State Machine"
  mit 
    - Leveln, bei denen die Position des Hindernisses jeweils per Zufall neu gesetzt wird
              und die Geschwindigkeit des ">" Zeichens zunimmt (nur bis Level 9)
    - automatischer "Rückkehr" auf den "Boden" nach vorgegebener Sprunglänge
    - automatische "Rückkehr" des ">" an den linken Rand und die untere Zeile bei
      Überschreiten des rechten Randes
    - einer "Pause" -Taste auf der '5', die durch eine beliebigen Taste aufgehoben wird

*/

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);  // I2C-Display-Adresse und Zeilenzahl

const byte ROWS = 4; //three rows
const byte COLS = 3; //four columns
char keys[ROWS][COLS] = {
  {'1', '2', '3'},
  {'4', '5', '6'},
  {'7', '8', '9'},
  {'*', '0', '#'}
};
byte rowPins[ROWS] = {8, 7, 6, 5}; //connect to the row pinouts of the keypad
byte colPins[COLS] = {4, 3, 2}; //connect to the column pinouts of the keypad

Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);

// Struktur zum Zusammenfassen der dino und hindernis-Positionen 
// unter einem Namen, damit man es sich einfacher merken kann,

struct position {
  byte x;
  byte y ;
};

// Deklaration der Variablen für dino und hindernis, incl. Default-Werte
position dino {1, 1};
position hindernis {10, 1};

// Da die Abfrage des keyPads und die Bewegung des "Dinos" nicht mit gleicher 
// Geschwindigkeit verlaufen, müssen wir uns die keyPad-Daten in "richtung" merken
// um sie in der weniger häufig aufgerufenen Funktion zum Bewegen verwenden zu
// können
char richtung;

// Die Schritte, die Dino in der oberen Zeile vollzogen hat
byte schritteOben = 0;

// Die maximale Schrittweite ist erlaubterSprung+1, nach der wird 
// der Dino wieder in die untere Zeile gezogen
// Durch " if (schritteOben > erlaubterSprung)" ergibt sich, dass 
// ein Schritt mehr als erlaubterSprung zur Auflösung true führt
// Das könnte (sollte) man in einem nächsten Entwicklungsschritt bereinigen!
constexpr byte erlaubterSprung = 2;

// Der Zähler für das Level, mit dessen Hilfe im Verlauf auch 
// die Schrittdauer verkürzt wird, damit der Dino sich schneller 
// bewegt
int level = 0;

// Ein enum ist ein Aufzählungstyp, wo der Compiler den einzelnen
// Elementen beim Compilieren einen fortlaufenden Zahlenwert zuweist,
// um den wir uns deshalb nicht selbst kümmern müssen.
// Fügen wir hier neue Elemente ein, löschen sie oder ordnen wir sie um
// ist völlig egal, da wir im Code immer(!) den Elementenamen verwenden.
// Ob der intern eine 1, 7 oder 43 ist, ist uns egal.
enum zustandsAuswahl {ANSAGE, DINOLAEUFT,PAUSE, GAMEOVER};

// Jetzt deklarieren wir eine Variable vom o.a. enum-Typen (den wir hier mal
// zustandsAuswahl genannt haben). Der Variablen zustand weisen wir schon gleich
// das Element ANSAGE zu.
zustandsAuswahl zustand = ANSAGE;


// Um den Dino nicht mit "voller" Controllergeschwindigkeit über das LCD
// zu hetzen, bedienen wir uns der bekannten "millis()"-Funktion, wofür wir
// ein paar globale Variable vorbereiten:
// in zuletztBewegt merken wir uns jeweils die aktuelle Controllerzeit in [ms]
// bei jeder Bewegung
unsigned long zuletztBewegt = 0;
// und warten für den nächsten Schritt darauf, dass zeitProSchritt (hier 0,55 s)
// verstrichen sind (Achtung: Hier nur Vorbereitung, die eigentliche Funktion, die 
// das umsetzt folgt später! siehe millis()-Funktion im Folgenden
unsigned long zeitProSchritt {550};  // Jede Sekunde ein Schritt


// Im Setup() bereiten wir die Serielle Schnittstelle und das LCD vor.
// Außerdem schreiben wir schon mal die Startmeldung auf das LCD.
// Da wir die Variable zustand = ANSAGE in der Deklaration schon gesetzt
// haben, brauchen wir hier nichts weiter zu tun.
// 
// Auch die pins des keypads brauchen wir nicht zu behandeln, das machen die
// Funktion der keyPad Library selbst!

void setup() { //LCD Display initialisieren und "Startmenü" anzeigen
  Serial.begin(115200);
  lcd.init();
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print("Press 0 to start");
  lcd.setCursor(0, 1);
  lcd.print("FAKE DINO RUNNER");
  Serial.println("Startschirm");
}



// Die loop() kommt ziemlich aufgeräumt daher ;-)
// Hier wird nur die Funktion zustandsMaschine() aufgerufen.
// Die erledigt alles andere ... wie schön ... ;-)


void loop() {
  zustandsMaschine();
}

// Hier kommt das "Arbeitstier", die zustandsMaschine
//
// Gleich am Anfang sammeln wir eventuelle Tastatureingaben ein, ohne uns 
// darum zu kümmern, was damit passieren soll
//
// danach folgt ein switch/case Konstrukt. Das ist die eigentlich "State Machine"
// auf Deutsch eigentlich "Zustandsautomat"
//
// Wie der Name sagt, unterscheidet diese Maschine verschiedene Zustände der Software.
// Es wird immer nur der aktuelle case-Zweig bis zum jeweiligen break; durchlaufen.
// Damit können wir in der gleichen(!) Schleife (loop()) unterschiedlich auf Dinge reagieren.
//
// In unserem Fall gibt es die vier Zustände ANSAGE, DINOLAEUFT, PAUSE, GAMEOVER.
// 
// ANSAGE: Hier starten wir, da wir ja zustand = ANSAGE gesetzt hatten.
//         Hier passiert ... nichts... solange die Taste '0' nicht betätigt wird.
//         Dann allerdings wird genau einmal die Funktion startGame() ausgeführt
//         und dann zustand auf DINOLAEUFT gesetzt. Durch Letzteres laufen wir beim
//         nächsten loop()-Durchlauf nicht wieder in den case ANSAGE sondern in DINOLAEUFT
//         Was ANSAGE anzeigt, schreibt schon der Zustand vorher aufs LCD, ANSAGE ist 
//         nur ein gutmütiger Helfer oder williger Gehilfe(!), dem es egal ist,
//         ob man gerade anfängt oder schon ein paar GameOver-Frustrationen hinter sich hat.
//
// DINOLAEUFT: Durch startGame() wurde das LCD für das Spiel vorbereitet und der erste Schritt
//         des Dino kann erfolgen. Dazu wird hier regelmäßig (und zwar extrem häufig!) bewegeDino(key)
//         aufgerufen. Die Funktion bewegeDino() wird aber erträglich verlangsamt, indem es nur dann
//         wirklich aktiv wird, wenn zeitProSchritt verstrichen ist (Genaueres steht bei der Funktion unten).
//         Eine eventuelle Tasteneingabe wird immer weitergeleitet (key), damit die Funktion sie 
//         bei Bedarf, also vor der Bewegung, auswerten kann.
//         Falls man eine '5' eingegeben hat, wechselt der zustand auf "PAUSE"
//
// PAUSE:  Macht genau, was ihr Name verspricht: Nix, außer auf eine Störung durch eine erneute
//         beliebige Tasteneingabe zu warten... Dann allerdings geht's sofort mit DINOLAEUFT weiter.
//         
// GAMEOVER: Zeigt nur die traurige Game-Over-Meldung und die Restart-Möglichkeit an und geht
//         ohne weitere Umstände zum Zustand ANSAGE. Und der ist ja ein williger Gehilfe...



void zustandsMaschine() {
  char key = keypad.getKey();
  switch (zustand) {
    case ANSAGE:
      if (key == '0') {
        startGame();
        zustand =  DINOLAEUFT;
      }
      break;
    case DINOLAEUFT:
      bewegeDino(key);
      if (key == '5'){
        zustand = PAUSE;
      }
      break;
    case PAUSE:
       if (key != NO_KEY) {
        zustand = DINOLAEUFT;
       }
       break;  
    case GAMEOVER:
      gameOver();
      zustand = ANSAGE;
      break;
  }
}


// bewegeDino hat es schon in sich, hier "spielt die Musik"
//
// Die Funktion lässt sich von der Zustandsmaschine die jeweils aktuelle Tasteneingabe übergeben
// Sollte aKey nicht für "nix gedrückt seit dem letzten Aufruf" stehen, also ungleich NO_KEY sein,
// merken wir uns den Wert in der globalen Variablen richtung. Diese muss global sein, damit sie
// auch beim nächsten Aufruf von bewegeDino() noch ihren alten Wert behält. Lokale Variable verfallen
// bekanntermaßen beim Verlassen der Funktion ihre Inhalte, globale (wie auch static/statische) nicht.
// 
// Jetzt kommt die berühmt-berüchtigte millis()-Funktion ... Es ranken sich diverse Legenden darum,
// wie sie funktioniert, wo und wann nicht usw. ...
//
// Man nehme ... eine if-Anweisung ... deren Inhalt nur ausgeführt wird, wenn 
//
// die aktuelle Controller-Zeit [ms] abzüglich des Zeitpunktes der letzten Ausführung einer Aktion
//                  millis()            -                    zuletztBewegt
//
// größer oder gleich der gewünschten Zeitspanne zeitProSchritt ist.
//
// Warum man das so und zwar genauso(!) schreiben sollte, ergibt sich aus dem mathematischen
// Wunder der Binärzahlenrechnung, die auch bei Überläufen der millis() Funktion funktioniert.
//
// Oder an einem vereinfachten Überlaufbeispiel mit Uhrzeiten ausgedrückt: 
//
//                                   Endtermin     Starttermin           Ergebnis
// Wieviele Stunden liegen zwischen   15 Uhr   und    13 Uhr     15 - 13 = 2 Stunden
// Wieviele Stunden liegen zwischen   01 Uhr   und    22 Uhr     01 - 22 = -21 Stunden?????
//                                                       
// Da Zeit (nach menschlicher Kenntnis) nur in eine Richtung verläuft, korrigiert man die -21 h 
// durch Addition von 24 h -> 24h + (-21h) = 3 h, schon stimmt's. 
// So (zumindest im Ergebnis) sorgt die o.a. Schreibweise immer für richtige Ergebnisse, auch
// wenn millis() zwischendurch im Betrieb(!) wieder mit 0 ms beginnt. Passiert so ca. alle 50 Tage
// bei einem vorzeichenlosen 32-Bit Zähler.
// Näheres siehe: 
// https://www.norwegiancreations.com/2018/10/arduino-tutorial-avoiding-the-overflow-issue-when-using-millis-and-micros/
//
//
// Nun weiter: Wenn wir tatsächlich, z.B. aller lächerliche 500 ms mal in die bedingten Funktionen 
// hineindürfen, dann merken wir uns diese Zeit in zuletztBewegt. Schliesslich sollen es die nächsten 
// Schleife nicht einfacher haben, als wir ...
// 
// Danach löschen wir mal den Dino an seinem "alten Platz".
//
// Wir prüfen den Inhalt von "richtung" darauf, ob in der Zwischenzeit, während wir auf das Verstreichen
// der zeitProSchritt warten mussten, die Tasten '2' oder '8' eingegeben wurden.
// 
// Im Falle der '2' lupfen wir Dino auf die ober Zeile und löschen richtung, denn das soll ja nur einmal
// pro Tastendruck geschehen.
// Im Falle der '8' bewegen wir Dino wieder auf die untere Ebene und löschen den schritteOben-Zähler,
// er ist ja jetzt wieder unten. Ausserdem löschen wir auch hier die Tasteneingabe.
//
// Sollte Dino bereits einen mehr als die erlaubten Schritte gemacht haben, zieht die Schwerkraft
// (oder ersatzweise wir in diesem Fall) ihn wieder nach unten. Auch hier löschen wir den Schrittzähler.
//
// Jetzt macht Dino tatsächlich einen Schritt nach vorne: dino.x++;
//
// Nun, dann prüfen wir mal, ob da nicht gerade schon das Hindernis ist ... die boolsche
// Funktion kollision() gibt true zurück, wenn beide jetzt das gleiche Feld x auf der gleichen
// Ebene y belegen. In diesem Fall "Gute Nacht" oder neudeutsch GAMEOVER.
//
// Falls nicht, prüfen wir zunächst, ob dino.x jetzt größer/gleich 16 ist. Falls ja,
// schieben wir ihn auf das Feld links unten und lassen uns das Hindernis an eine neue
// Stelle setzen (setzeHindernis(true)). Das (true) veranlasst diese Funktion dazu, das 
// "alte" Hindernis zunächst zu löschen, bevor das neue gesetzt wird.
//
// Ist das geschafft (oder noch nicht erforderlich), erscheint Dino an seiner neuen Position.
// Sollte er dabei in "höheren Gefilden" schweben, zählen wir seine schritteOben gleich
// hier mal mit..



void bewegeDino(char aKey) { // automatische Bewegung des ">" (Dinos) nach rechts Richtung Hinderniss "#"
  if (aKey != NO_KEY) {
    richtung = aKey;
  }
  if (millis() - zuletztBewegt >= zeitProSchritt) {
    zuletztBewegt = millis();
    lcd.setCursor(dino.x, dino.y);
    lcd.print(" ");
    if (richtung == '2') {
      dino.y = 0;
      richtung = NO_KEY;
    }
    if (richtung == '8') {
      dino.y = 1;
      richtung = NO_KEY;
      schritteOben = 0;
    }
    if (schritteOben > erlaubterSprung) {
      dino.y = 1;
      schritteOben = 0;
    }
    dino.x++;
    if (kollision()) {
      zustand = GAMEOVER;
      Serial.println("Game Over");
    } else {
      if (dino.x >= 16) {
        dino.x = 1;
        dino.y = 1;
        setzeHindernis(true);
      }
      lcd.setCursor(dino.x, dino.y);
      lcd.print(">");
      if (dino.y == 0) {
        schritteOben++;
      }
    }
  }
}

// Eigentlich selbsterklärend: 
// Gibt true zurück, wen die verglichenen Koordinaten paarweise identisch
// sind, mithin also hindernis und dino den gleichen Platz einnehmen,
// was bei massiven Körpern zu bekannten Problemem führt und daher
// zu vermeiden ist ... ;-)

boolean kollision() {
  return (dino.x == hindernis.x && dino.y == hindernis.y);
}


// Dies Funktion hat es relativ einfach und gemütlich,
// da sie im Vergleich zu anderen in der loop() selten
// arbeiten muss.
//
// Falls gewünscht (loeschen == true), lässt sie das Hindernis
// an seiner zuletzt bekannten Stelle verschwinden
// 
// Danach hilft ihr die Pseudozufallsfunktion random() dabei
// einen neuen schönen Platz für das Hindernis zu erwürfeln
// Der Wert soll minimal 5 und maximal 15 sein, bei random(a,b)
// ist a inklusiv, b aber exklusiv (wird also nicht gewürfelt)
// daher steht dort eine 16.
//
// Nun wird das Hindernis am neuen Platz auf dem LCD ausgegeben
// und die Variable Level erhöht. Das Spiel  startet immer mit
// dem Level 1, indem "level" zuvor geschickterweise auf Null gesetzt wird.
// Solange der Wert von level kleiner als 10 ist (also 1...9), reduziert sich
// im folgenden Schritt die zeitProSchritt von 550 - 50 = 500 ms bei Level 1
// schrittweise bis 550 - 9*50 = 550 -450 = 100 ms = 1/10 s
// Für den Interessierten geben wir das Level noch auf der seriellen Schnittstelle aus.
// Das erreichte Level könnte allerdings auch - wenn man es noch einbaut -
// als kleiner Trost bei GameOver ausgegeben werden.


void setzeHindernis(boolean loeschen) {
  if (loeschen) {
    lcd.setCursor(hindernis.x, hindernis.y);
    lcd.print(" ");
  }
  hindernis.x = random(5, 16);
  lcd.setCursor(hindernis.x, hindernis.y);
  lcd.print("#");
  level++;
  if (level < 10) {
    zeitProSchritt = 550 - level * 50;
  }
  Serial.print("Level : ");
  Serial.println(level);
}

// startGame hat es noch ruhiger als setzteHindernis.
// Es bereitet das LCD,  Hindernis und einige Spielstart-relevante Variable auf einen
// frischen Start vor.

void startGame() { //">" und "#", Dino und Hindernis werden auf das Display geprintet
  Serial.println("Dino läuft ..");
  lcd.clear();
  schritteOben = 0;
  level = 0;
  zeitProSchritt = 550;
  dino.x = 1;
  dino.y = 1;
  lcd.setCursor(dino.x, dino.y);
  lcd.print(">");
  setzeHindernis(false);
}

// gameOver() hat die traurige Aufgabe, das Ende einer steilen Dino-Karriere anzuzeigen.
// A dirty job but someone's got to do it ... Wie der des Englischen fähige Franzose sagt.
// Die weitere Arbeit muss dann wieder die ANSAGE erledigen, aber selbst das  überlässt man
// hier der zustandsMaschine() ...


void gameOver() { // GAME-OVER screen
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("   GAME OVER");
  lcd.setCursor(0, 1);
  lcd.print("Restart with 0");
}

// Das hier wird im Code nicht benötigt, ausser man möchte irgendwelchen Mauscheleien
// des Codes auf den Grund gehen, der einfach Kollisionen behauptet, obwohl man doch
// ganz sicher noch rechtzeitig die '2' gedrückt hatte... die '2' ... ja ... und der
// Dino war oben, ganz sicher, ich schwör' ...
//
// Also, falls die Daten mal geprüft werden müssen,
// an passender Stelle printPos() aufrufen ...
void printPos() {
  Serial.print("dino.x ");
  Serial.println(dino.x);
  Serial.print("dino.y ");
  Serial.println(dino.y);
  Serial.print("hindernis.x ");
  Serial.println(hindernis.x);
  Serial.print("hindernis.y ");
  Serial.println(hindernis.y);
}

Siehe Chrome Dino II Commented - Wokwi ESP32, STM32, Arduino Simulator

Da können noch Bugs lauern, aber es scheint weitgehend zu funktionieren.

Viel Spaß!

P.S.: Für sehr viele Aufgaben sind "Zustandsautomaten" einfache und manche fast unverzichtbare Umsetzungen. Wer das Prinzip einmal einigermaßen verstanden hat, kann sich damit vieles, vor allem hochkomplexe if/else-Konstrukte ersparen, die am Ende kaum noch beherrschbar sind.
Siehe u.a.: Endlicher Automat – Wikipedia Hier kann ich auch nur die Grafiken empfehlen! Auch bei einem Spiel wie hier, was so einfach erscheint, lohnt es sich Zustände und Übergangsbedingungen mal aufzuzeichnen. Als Werkzeug eignen sich Vektorgrafiktools wie z.B. LibreOffice Draw ...

[Edit 2023-12-06] Kleine Schönheitskorrektur; im Sketch war noch ein "static" bei einer globalen Variablen ( aus einer Zwischenversion des Sketches, wo diese Variable lokal verwendet wurde) hängengeblieben, das da zwar nicht stört, aber auch nicht hingehört. Zur Erläuterung: Wenn eine Variable lokal in einer Funktion deklariert wird, verliert sie ihre Gültigkeit und ihren Wert, wenn die Funktion verlassen wird. Ist das im Einzelfall einmal anders gewünscht, nämlich dass sie beim erneuten Aufruf der Funktion ihren letzten Wert wiederverwendet, setzt man das Schlüsselwort "static" vor die Deklaration. Die Inhalte solcher Variablen bleiben - wie globale - erhalten, sind aber - im Gegensatz zu globaler Deklaration - auch nur in dieser Funktion nutzbar. Man sollte sich allerdings im Klaren darüber sein, was man da macht ... Sonst kann es schon zu längeren Debug-Sitzungen führen ... :wink:

1 Like

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.