Videogioco Dino
Descrizione
Questo progetto è un piccolo gioco per Arduino con display LCD, ispirato al classico gioco del dinosauro che corre. Utilizza una scheda Arduino, un display LCD I2C 16x2 e un pulsante. Il dinosauro rimane sul lato sinistro dello schermo, mentre un cactus si muove da destra verso sinistra. Quando il pulsante viene premuto, il dinosauro salta per evitare il cactus. Se il cactus raggiunge la stessa posizione del dinosauro, la partita termina e viene mostrata una schermata di FINE GIOCO. Questo progetto è utile per imparare i caratteri personalizzati per LCD, il controllo di un display I2C, l'input da pulsante, una semplice logica di gioco, il rilevamento delle collisioni, la temporizzazione con millis() e l'animazione di base su un display Arduino.
Componenti necessari:
- 1x Arduino UNO
- 1x Display LCD con modulo I2C integrato
- 1x Bottone NC
- 1x Resistore 10kΩ
- Cavi jumper (breadboard opzionale)
Schema:
Codice:
// https://nemiatools.com
//https://nemiatools.com
#include <Wire.h> // Includi la libreria di comunicazione I2C
#include <LiquidCrystal_I2C.h> // Includi la libreria LCD I2C
#define buttonPin 2 // Definisci il pin di input del pulsante
LiquidCrystal_I2C lcd(0x27, 16, 2); // Crea l'oggetto display LCD
byte dino1[8] = { // Definisci il primo sprite del dinosauro
B00111,
B00101,
B00111,
B00100,
B01111,
B11100,
B11100,
B00100
};
byte dino2[8] = { // Definisci il secondo sprite del dinosauro
B00111,
B00101,
B00111,
B00100,
B01111,
B11100,
B11100,
B10000
};
byte cactus[8] = { // Definisci lo sprite personalizzato del cactus
B00100,
B00100,
B10100,
B10101,
B11111,
B00100,
B00100,
B01110
};
//coordinate del dinosauro
const int dinoX = 4; // Imposta la posizione orizzontale del dinosauro
int dinoY = 1; // Imposta la posizione verticale del dinosauro
//coordinate del cactus
int cactusX = 15; // Imposta la posizione orizzontale del cactus
const int cactusY = 1; // Imposta la posizione verticale del cactus
//millis per il salto
unsigned long lastJump = 0; // Memorizza l'ora dell'ultimo salto
unsigned long jumpDuration= 600; // Imposta il tempo della durata del salto
//millis per il tempo di gioco
unsigned long startGame = 0; // Memorizza l'ora di inizio del gioco
unsigned long gameTime = 0; // Memorizza il tempo trascorso
int timeX; // Memorizza la posizione del cursore del timer
//stati del pulsante
bool buttonState; // Memorizza lo stato corrente del pulsante
bool oldButtonState = false; // Memorizza lo stato precedente del pulsante
void setup() {
pinMode(buttonPin, INPUT); // Configura il pulsante come input
lcd.init(); // Inizializza il display LCD
lcd.backlight(); // Accende la retroilluminazione LCD
// Salva la memoria dei caratteri personalizzati
lcd.createChar(0, dino1); // Salva il primo sprite del dinosauro
lcd.createChar(1, dino2); // Salva il secondo sprite del dinosauro
lcd.createChar(2, cactus); // Salva lo sprite personalizzato del cactus
//Menu iniziale
lcd.setCursor(0, 0); // Imposta il cursore sulla prima riga
lcd.print("DINO GAME"); // Stampa il testo del titolo del gioco
lcd.setCursor(0, 1); // Imposta il cursore sulla seconda riga
lcd.print("by NEMIAtools"); // Stampa il testo del nome dell'autore
while (true){ // Aspetta finché non viene premuto il pulsante
if(digitalRead(buttonPin)) break; // Esci quando il pulsante viene premuto
}
while(digitalRead(buttonPin)){ // Aspetta finché il pulsante non viene rilasciato
}
}
void game(){ // Esegui la funzione principale del gioco
startGame = millis(); // Salva l'ora corrente di inizio
lastJump = millis() - jumpDuration; // Azzera lo stato del tempo di salto
oldButtonState = false; // Azzera lo stato precedente del pulsante
dinoY = 1; // Posiziona il dinosauro a terra
bool dinoState = true; // Imposta lo stato dell'animazione del dinosauro
while(true){ // Ripeti il ciclo principale del gioco per sempre
gameTime = (millis() - startGame) / 10; // Calcola i centesimi di secondo trascorsi
lcd.clear(); // Cancella il contenuto del display LCD
buttonState = digitalRead(buttonPin); // Leggi lo stato corrente del pulsante
if(buttonState && !oldButtonState && millis() - lastJump >= jumpDuration){ // Controlla una richiesta valida di salto
lastJump = millis(); // Salva il nuovo tempo di salto
}
oldButtonState = buttonState; // Aggiorna lo stato precedente del pulsante
if(millis() - lastJump < jumpDuration) dinoY = 0; // Sposta il dinosauro verso l'alto mentre salta
else dinoY = 1; // Riporta il dinosauro verso il basso
if(gameTime > 99999) gameTime = 99999; // Limita il tempo massimo visualizzato
timeX = String(gameTime).length(); // Conta la lunghezza delle cifre del timer
lcd.setCursor(16 - timeX, 0); // Allinea il timer sul lato destro
lcd.print(gameTime); // Stampa il tempo di gioco corrente
lcd.setCursor(cactusX, cactusY); // Imposta il cursore sul cactus
lcd.write(byte(2)); // Disegna il carattere personalizzato del cactus
lcd.setCursor(dinoX, dinoY); // Imposta il cursore sul dinosauro
if(dinoState) lcd.write(byte(0)); // Disegna il primo frame del dinosauro
else lcd.write(byte(1)); // Disegna il secondo frame del dinosauro
if((cactusX == dinoX) && (cactusY == dinoY)){ // Controlla la collisione con il cactus
lcd.setCursor((dinoX + 1), cactusY); // Imposta il cursore accanto al dinosauro
lcd.write(byte(2)); // Disegna il cactus dopo la collisione
break; // Esci immediatamente dal ciclo del gioco
}
cactusX--; // Muovi il cactus verso sinistra una volta
if(cactusX < 0) cactusX = 15; // Reimposta il cactus a destra
dinoState = !dinoState; // Cambia il frame dell'animazione del dinosauro
delay(100); // Aspetta prima del frame successivo
}
return; // Ritorna dalla funzione game
}
void gameover(){ // Esegui la schermata di game over
lcd.setCursor(0, 0); // Imposta il cursore sulla prima riga
lcd.print("GAME OVER"); // Stampa il testo game over
lcd.setCursor(1, 1); // Imposta il cursore sulla seconda riga
while(digitalRead(buttonPin)){ // Aspetta il rilascio del pulsante
}
while(!digitalRead(buttonPin)){ // Aspetta la pressione del pulsante
}
while(digitalRead(buttonPin)){ // Aspetta il rilascio del pulsante
}
return; // Ritorna dalla funzione gameover
}
void loop() {
cactusX = 15; // Reimposta la posizione iniziale del cactus
lcd.clear(); // Cancella l'LCD prima del gioco
game(); // Avvia la funzione principale del gioco
gameover(); // Mostra la schermata di game over
}
Come funziona:
Questo progetto funziona come un semplice gioco del dinosauro che corre su un display LCD I2C 16x2. Il giocatore preme un pulsante per far saltare il dinosauro sopra un cactus che si muove da destra verso sinistra.
La riga #include <Wire.h> abilita la comunicazione I2C, mentre #include <LiquidCrystal_I2C.h> permette ad Arduino di controllare l'LCD tramite l'adattatore I2C. Grazie a I2C, il display necessita solo delle linee di comunicazione SDA e SCL invece di molti pin paralleli dell'LCD.
La riga LiquidCrystal_I2C lcd(0x27, 16, 2); crea l'oggetto LCD. Il valore 0x27 è l'indirizzo I2C del display, mentre 16, 2 significa che l'LCD ha 16 colonne e 2 righe.
Gli array byte dino1[8], byte dino2[8] e byte cactus[8] definiscono caratteri personalizzati per l'LCD. Ognuno è formato da 8 righe di pixel. Le due immagini del dinosauro vengono utilizzate per creare un'animazione di corsa, mentre l'immagine del cactus rappresenta l'ostacolo.
La riga const int dinoX = 4; fissa la posizione orizzontale del dinosauro. La variabile int dinoY = 1; controlla la posizione verticale: quando è 1, il dinosauro è a terra; quando è 0, il dinosauro sta saltando.
La variabile int cactusX = 15; fa partire il cactus dal lato destro del display. A ogni frame del gioco, la riga cactusX--; lo sposta di una posizione verso sinistra. Quando esce dallo schermo, if(cactusX < 0) cactusX = 15; lo riporta nuovamente sul lato destro.
La parte più importante della temporizzazione di questo progetto utilizza millis(). In Arduino, millis() restituisce il numero di millisecondi trascorsi da quando la scheda è stata accesa o reimpostata. È utile perché permette al codice di misurare il tempo trascorso senza dipendere solamente da ritardi fissi.
All'inizio del gioco, la riga startGame = millis(); salva il momento preciso in cui il gioco inizia. Successivamente, la riga gameTime = (millis() - startGame) / 10; calcola quanto tempo è passato dall'inizio della partita. La divisione per 10 converte i millisecondi in centesimi di secondo, quindi il valore mostrato aumenta come un punteggio veloce del gioco.
La variabile lastJump memorizza il momento in cui è iniziato l'ultimo salto. La riga lastJump = millis() - jumpDuration; viene utilizzata all'avvio del gioco per rendere il dinosauro immediatamente pronto a saltare. Poiché jumpDuration è già stata sottratta, la condizione che permette un nuovo salto è già verificata.
La riga unsigned long jumpDuration = 600; imposta la durata del salto a 600 millisecondi. Questo significa che, dopo una pressione valida del pulsante, il dinosauro rimane sulla riga superiore per circa 0,6 secondi prima di tornare a terra.
La riga buttonState = digitalRead(buttonPin); legge il pulsante. La condizione if(buttonState && !oldButtonState && millis() - lastJump >= jumpDuration) controlla tre cose: il pulsante è attualmente premuto, non era premuto nel frame precedente e il salto precedente è già terminato.
Questa condizione è importante perché rileva solo una nuova pressione del pulsante, non un pulsante che viene mantenuto premuto. La parte !oldButtonState impedisce al dinosauro di saltare continuamente mentre il giocatore tiene premuto il pulsante.
Quando il salto è valido, la riga lastJump = millis(); salva il tempo corrente come inizio del nuovo salto. Da quel momento, il codice confronta il valore attuale di millis() con lastJump per capire se il dinosauro deve ancora rimanere in aria.
La condizione if(millis() - lastJump < jumpDuration) controlla se sono passati meno di 600 millisecondi dall'inizio del salto. Se è vero, dinoY = 0; sposta il dinosauro sulla riga superiore. Altrimenti, dinoY = 1; lo riporta a terra.
Il timer mostrato sul display viene allineato a destra usando timeX = String(gameTime).length(); e lcd.setCursor(16 - timeX, 0);. Questo mantiene il punteggio nella posizione corretta anche quando il numero di cifre aumenta.
Il cactus viene disegnato usando lcd.setCursor(cactusX, cactusY); e lcd.write(byte(2));. Il dinosauro viene disegnato usando lcd.setCursor(dinoX, dinoY);. La variabile dinoState alterna tra dino1 e dino2 per creare una semplice animazione di corsa.
La collisione viene controllata con if((cactusX == dinoX) && (cactusY == dinoY)). Se il cactus e il dinosauro si trovano nella stessa posizione dell'LCD, il gioco si ferma e il giocatore perde.
La riga delay(100); controlla la velocità dei frame del gioco. Anche se la temporizzazione del salto viene calcolata con millis(), questo ritardo influenza comunque la frequenza con cui lo schermo viene aggiornato e la velocità con cui il cactus si muove.
Quando avviene una collisione, la funzione gameover() mostra GAME OVER e aspetta che il giocatore prema nuovamente il pulsante prima di riavviare il gioco.
In generale, millis() viene utilizzato in due modi principali: per calcolare il punteggio del gioco dal momento dell'avvio e per controllare con precisione la durata del salto. Questo rende il salto dipendente dal tempo trascorso invece che solamente dal numero di cicli del loop.
Per un comportamento stabile del pulsante, si consiglia di utilizzare una resistenza di pull-down o pull-up, oppure di adattare il codice per usare INPUT_PULLUP.
Sviluppi futuri:
Questo progetto potrebbe essere migliorato aggiungendo un buzzer per creare semplici effetti sonori. Ad esempio, il buzzer potrebbe riprodurre un breve tono quando il dinosauro salta e un suono diverso quando il giocatore colpisce il cactus. Questo renderebbe il gioco più interattivo e più simile a un vero mini gioco in stile arcade.