Gioco con Arduino e Display LCD 16×2: salta gli ostacoli
Realizziamo un piccolo runner game direttamente su un display LCD 16×2: il personaggio corre, gli ostacoli scorrono sullo schermo e il giocatore deve saltarli al momento giusto.
Come funziona il gioco
All’avvio compare la scritta Press Start. Premendo il pulsante inizia la partita: il personaggio corre sulla riga inferiore del display e deve evitare i blocchi generati casualmente.
Ogni ostacolo superato aumenta progressivamente il punteggio visualizzato nell’angolo superiore destro. In caso di collisione la partita termina e può essere riavviata con il pulsante.
Video dimostrativo
Materiale necessario
🧰 Kit Arduino consigliato
Un kit completo è utile per avere breadboard, cavetti, resistenze, display e molti componenti di base per sperimentare con Arduino.
👉 Guarda il kit Arduino consigliatoLink di affiliazione Amazon: in qualità di Affiliato Amazon ricevo un guadagno dagli acquisti idonei, senza alcun costo aggiuntivo per te.
📘 Vuoi imparare Arduino passo dopo passo?
Se vuoi approfondire e realizzare progetti più completi, puoi dare un’occhiata anche ai miei libri dedicati ad Arduino. All’interno troverai spiegazioni semplici, schemi chiari e molti progetti pratici pensati per chi parte da zero.
👉 Scopri i miei libri su ArduinoLink di affiliazione Amazon: in qualità di Affiliato Amazon ricevo un guadagno dagli acquisti idonei, senza alcun costo aggiuntivo per te.
Schema del circuito
Schema del gioco con Arduino Uno e LCD 16×2. L’immagine mantiene una larghezza massima di 641 px.
Pin utilizzati
| Funzione | Pin Arduino |
|---|---|
| Pulsante START / salto | D2 |
| Autoplay | D1 |
| LCD RS | D11 |
| LCD E | D9 |
| LCD D4 | D6 |
| LCD D5 | D5 |
| LCD D6 | D4 |
| LCD D7 | D3 |
Perché il display può mostrare un personaggio
Il controller dei comuni LCD HD44780 consente di creare alcuni caratteri personalizzati. Lo sketch usa questa possibilità per disegnare il personaggio durante la corsa, il salto e i diversi tipi di blocco.
Ogni sprite è definito come una piccola matrice di 5 × 8 pixel e viene caricato con lcd.createChar().
Codice Arduino commentato
/*
GIOCO RUNNER CON ARDUINO UNO E DISPLAY LCD 16x2
Funzionamento:
- il personaggio corre sul display;
- con il pulsante START / SALTO evita gli ostacoli;
- il punteggio aumenta con la distanza percorsa;
- gli ostacoli vengono generati in modo casuale;
- la modalità AUTOPLAY può generare automaticamente il salto.
Collegamenti LCD:
RS -> D11
E -> D9
D4 -> D6
D5 -> D5
D6 -> D4
D7 -> D3
Pulsante START / SALTO -> D2
Autoplay -> D1
R/W LCD -> D10
Contrasto -> D12
NOTA:
D1 è anche il pin TX della seriale di Arduino Uno.
Se utilizzi la funzione AUTOPLAY come in questo circuito,
evita di usare contemporaneamente la comunicazione Serial.
*/
#include <LiquidCrystal.h>
#define PIN_BUTTON 2
#define PIN_AUTOPLAY 1
#define PIN_READWRITE 10
#define PIN_CONTRAST 12
// Caratteri personalizzati usati per il personaggio
#define SPRITE_RUN1 1
#define SPRITE_RUN2 2
#define SPRITE_JUMP 3
#define SPRITE_JUMP_UPPER '.'
#define SPRITE_JUMP_LOWER 4
// Caratteri personalizzati usati per il terreno
#define SPRITE_TERRAIN_EMPTY ' '
#define SPRITE_TERRAIN_SOLID 5
#define SPRITE_TERRAIN_SOLID_RIGHT 6
#define SPRITE_TERRAIN_SOLID_LEFT 7
// Posizione orizzontale fissa del personaggio
#define HERO_HORIZONTAL_POSITION 1
#define TERRAIN_WIDTH 16
#define TERRAIN_EMPTY 0
#define TERRAIN_LOWER_BLOCK 1
#define TERRAIN_UPPER_BLOCK 2
// Stati possibili del personaggio
#define HERO_POSITION_OFF 0
#define HERO_POSITION_RUN_LOWER_1 1
#define HERO_POSITION_RUN_LOWER_2 2
#define HERO_POSITION_JUMP_1 3
#define HERO_POSITION_JUMP_2 4
#define HERO_POSITION_JUMP_3 5
#define HERO_POSITION_JUMP_4 6
#define HERO_POSITION_JUMP_5 7
#define HERO_POSITION_JUMP_6 8
#define HERO_POSITION_JUMP_7 9
#define HERO_POSITION_JUMP_8 10
#define HERO_POSITION_RUN_UPPER_1 11
#define HERO_POSITION_RUN_UPPER_2 12
// Display LCD 16x2 in modalità parallela
LiquidCrystal lcd(11, 9, 6, 5, 4, 3);
// Buffer che rappresentano le due righe del terreno
static char terrainUpper[TERRAIN_WIDTH + 1];
static char terrainLower[TERRAIN_WIDTH + 1];
// Flag modificato dall'interrupt del pulsante
static volatile bool buttonPushed = false;
// ------------------------------------------------------
// Crea i caratteri personalizzati per personaggio e terreno
// ------------------------------------------------------
void initializeGraphics() {
static byte graphics[] = {
// Corsa: posizione 1
B01100,
B01100,
B00000,
B01110,
B11100,
B01100,
B11010,
B10011,
// Corsa: posizione 2
B01100,
B01100,
B00000,
B01100,
B01100,
B01100,
B01100,
B01110,
// Salto
B01100,
B01100,
B00000,
B11110,
B01101,
B11111,
B10000,
B00000,
// Parte inferiore del personaggio durante il salto
B11110,
B01101,
B11111,
B10000,
B00000,
B00000,
B00000,
B00000,
// Blocco pieno
B11111,
B11111,
B11111,
B11111,
B11111,
B11111,
B11111,
B11111,
// Mezzo blocco destro
B00011,
B00011,
B00011,
B00011,
B00011,
B00011,
B00011,
B00011,
// Mezzo blocco sinistro
B11000,
B11000,
B11000,
B11000,
B11000,
B11000,
B11000,
B11000
};
// Carica 7 caratteri personalizzati nella CGRAM del display
for (int i = 0; i < 7; ++i) {
lcd.createChar(i + 1, &graphics[i * 8]);
}
// All'avvio il terreno è vuoto
for (int i = 0; i < TERRAIN_WIDTH; ++i) {
terrainUpper[i] = SPRITE_TERRAIN_EMPTY;
terrainLower[i] = SPRITE_TERRAIN_EMPTY;
}
}
// ------------------------------------------------------
// Fa scorrere il terreno verso sinistra
// ------------------------------------------------------
void advanceTerrain(char* terrain, byte newTerrain) {
for (int i = 0; i < TERRAIN_WIDTH; ++i) {
char current = terrain[i];
char next =
(i == TERRAIN_WIDTH - 1)
? newTerrain
: terrain[i + 1];
switch (current) {
case SPRITE_TERRAIN_EMPTY:
terrain[i] =
(next == SPRITE_TERRAIN_SOLID)
? SPRITE_TERRAIN_SOLID_RIGHT
: SPRITE_TERRAIN_EMPTY;
break;
case SPRITE_TERRAIN_SOLID:
terrain[i] =
(next == SPRITE_TERRAIN_EMPTY)
? SPRITE_TERRAIN_SOLID_LEFT
: SPRITE_TERRAIN_SOLID;
break;
case SPRITE_TERRAIN_SOLID_RIGHT:
terrain[i] = SPRITE_TERRAIN_SOLID;
break;
case SPRITE_TERRAIN_SOLID_LEFT:
terrain[i] = SPRITE_TERRAIN_EMPTY;
break;
}
}
}
// ------------------------------------------------------
// Disegna personaggio, terreno e punteggio.
// Restituisce true se il personaggio collide con un ostacolo.
// ------------------------------------------------------
bool drawHero(
byte position,
char* terrainUpper,
char* terrainLower,
unsigned int score
) {
bool collide = false;
char upperSave =
terrainUpper[HERO_HORIZONTAL_POSITION];
char lowerSave =
terrainLower[HERO_HORIZONTAL_POSITION];
byte upper;
byte lower;
switch (position) {
case HERO_POSITION_OFF:
upper = lower = SPRITE_TERRAIN_EMPTY;
break;
case HERO_POSITION_RUN_LOWER_1:
upper = SPRITE_TERRAIN_EMPTY;
lower = SPRITE_RUN1;
break;
case HERO_POSITION_RUN_LOWER_2:
upper = SPRITE_TERRAIN_EMPTY;
lower = SPRITE_RUN2;
break;
case HERO_POSITION_JUMP_1:
case HERO_POSITION_JUMP_8:
upper = SPRITE_TERRAIN_EMPTY;
lower = SPRITE_JUMP;
break;
case HERO_POSITION_JUMP_2:
case HERO_POSITION_JUMP_7:
upper = SPRITE_JUMP_UPPER;
lower = SPRITE_JUMP_LOWER;
break;
case HERO_POSITION_JUMP_3:
case HERO_POSITION_JUMP_4:
case HERO_POSITION_JUMP_5:
case HERO_POSITION_JUMP_6:
upper = SPRITE_JUMP;
lower = SPRITE_TERRAIN_EMPTY;
break;
case HERO_POSITION_RUN_UPPER_1:
upper = SPRITE_RUN1;
lower = SPRITE_TERRAIN_EMPTY;
break;
case HERO_POSITION_RUN_UPPER_2:
upper = SPRITE_RUN2;
lower = SPRITE_TERRAIN_EMPTY;
break;
}
// Inserisce temporaneamente il personaggio nel terreno
if (upper != ' ') {
terrainUpper[HERO_HORIZONTAL_POSITION] = upper;
collide =
(upperSave == SPRITE_TERRAIN_EMPTY)
? false
: true;
}
if (lower != ' ') {
terrainLower[HERO_HORIZONTAL_POSITION] = lower;
collide |=
(lowerSave == SPRITE_TERRAIN_EMPTY)
? false
: true;
}
// Numero di cifre da riservare al punteggio
byte digits =
(score > 9999) ? 5 :
(score > 999) ? 4 :
(score > 99) ? 3 :
(score > 9) ? 2 : 1;
// Termina le due stringhe
terrainUpper[TERRAIN_WIDTH] = '\0';
terrainLower[TERRAIN_WIDTH] = '\0';
// Disegna la riga superiore lasciando spazio al punteggio
char temp = terrainUpper[16 - digits];
terrainUpper[16 - digits] = '\0';
lcd.setCursor(0, 0);
lcd.print(terrainUpper);
terrainUpper[16 - digits] = temp;
// Disegna la riga inferiore
lcd.setCursor(0, 1);
lcd.print(terrainLower);
// Mostra il punteggio nell'angolo in alto a destra
lcd.setCursor(16 - digits, 0);
lcd.print(score);
// Ripristina il terreno originale sotto il personaggio
terrainUpper[HERO_HORIZONTAL_POSITION] = upperSave;
terrainLower[HERO_HORIZONTAL_POSITION] = lowerSave;
return collide;
}
// ------------------------------------------------------
// Routine chiamata quando viene premuto il pulsante su D2
// ------------------------------------------------------
void buttonPush() {
buttonPushed = true;
}
void setup() {
// R/W del display mantenuto LOW
pinMode(PIN_READWRITE, OUTPUT);
digitalWrite(PIN_READWRITE, LOW);
// Pin previsto dal circuito per il contrasto
pinMode(PIN_CONTRAST, OUTPUT);
digitalWrite(PIN_CONTRAST, LOW);
// Pulsante con pull-up interno
pinMode(PIN_BUTTON, INPUT);
digitalWrite(PIN_BUTTON, HIGH);
// Uscita usata dalla logica AUTOPLAY
pinMode(PIN_AUTOPLAY, OUTPUT);
digitalWrite(PIN_AUTOPLAY, HIGH);
// D2 corrisponde all'interrupt esterno 0 su Arduino Uno
attachInterrupt(
digitalPinToInterrupt(PIN_BUTTON),
buttonPush,
FALLING
);
lcd.begin(16, 2);
initializeGraphics();
}
void loop() {
static byte heroPos =
HERO_POSITION_RUN_LOWER_1;
static byte newTerrainType =
TERRAIN_EMPTY;
static byte newTerrainDuration = 1;
static bool playing = false;
static bool blink = false;
static unsigned int distance = 0;
// ----------------------------------------------------
// SCHERMATA INIZIALE / GAME OVER
// ----------------------------------------------------
if (!playing) {
drawHero(
blink ? HERO_POSITION_OFF : heroPos,
terrainUpper,
terrainLower,
distance >> 3
);
if (blink) {
lcd.setCursor(0, 0);
lcd.print("Press Start");
}
delay(250);
blink = !blink;
// Una pressione avvia una nuova partita
if (buttonPushed) {
initializeGraphics();
heroPos =
HERO_POSITION_RUN_LOWER_1;
playing = true;
buttonPushed = false;
distance = 0;
}
return;
}
// ----------------------------------------------------
// SCORRIMENTO DEL TERRENO
// ----------------------------------------------------
advanceTerrain(
terrainLower,
newTerrainType == TERRAIN_LOWER_BLOCK
? SPRITE_TERRAIN_SOLID
: SPRITE_TERRAIN_EMPTY
);
advanceTerrain(
terrainUpper,
newTerrainType == TERRAIN_UPPER_BLOCK
? SPRITE_TERRAIN_SOLID
: SPRITE_TERRAIN_EMPTY
);
// ----------------------------------------------------
// GENERAZIONE CASUALE DEGLI OSTACOLI
// ----------------------------------------------------
if (--newTerrainDuration == 0) {
if (newTerrainType == TERRAIN_EMPTY) {
newTerrainType =
(random(3) == 0)
? TERRAIN_UPPER_BLOCK
: TERRAIN_LOWER_BLOCK;
newTerrainDuration =
2 + random(10);
}
else {
newTerrainType =
TERRAIN_EMPTY;
newTerrainDuration =
10 + random(10);
}
}
// ----------------------------------------------------
// SALTO MANUALE
// ----------------------------------------------------
if (buttonPushed) {
if (heroPos <= HERO_POSITION_RUN_LOWER_2) {
heroPos = HERO_POSITION_JUMP_1;
}
buttonPushed = false;
}
// ----------------------------------------------------
// DISEGNO E CONTROLLO COLLISIONE
// ----------------------------------------------------
if (
drawHero(
heroPos,
terrainUpper,
terrainLower,
distance >> 3
)
) {
// Collisione: partita terminata
playing = false;
}
else {
// Aggiorna le varie fasi della corsa e del salto
if (
heroPos == HERO_POSITION_RUN_LOWER_2 ||
heroPos == HERO_POSITION_JUMP_8
) {
heroPos =
HERO_POSITION_RUN_LOWER_1;
}
else if (
heroPos >= HERO_POSITION_JUMP_3 &&
heroPos <= HERO_POSITION_JUMP_5 &&
terrainLower[HERO_HORIZONTAL_POSITION] !=
SPRITE_TERRAIN_EMPTY
) {
heroPos =
HERO_POSITION_RUN_UPPER_1;
}
else if (
heroPos >= HERO_POSITION_RUN_UPPER_1 &&
terrainLower[HERO_HORIZONTAL_POSITION] ==
SPRITE_TERRAIN_EMPTY
) {
heroPos =
HERO_POSITION_JUMP_5;
}
else if (
heroPos == HERO_POSITION_RUN_UPPER_2
) {
heroPos =
HERO_POSITION_RUN_UPPER_1;
}
else {
++heroPos;
}
++distance;
// Logica AUTOPLAY:
// genera automaticamente il segnale di salto
// quando viene rilevato un ostacolo poco più avanti.
digitalWrite(
PIN_AUTOPLAY,
terrainLower[
HERO_HORIZONTAL_POSITION + 2
] == SPRITE_TERRAIN_EMPTY
? HIGH
: LOW
);
}
// Velocità del gioco.
// Riduci il valore per aumentare la difficoltà.
delay(200);
}
Come funziona lo sketch
1. Terreno su due righe
Gli array terrainUpper e terrainLower rappresentano le due righe del display. Ad ogni ciclo il terreno viene fatto scorrere verso sinistra.
2. Generazione casuale degli ostacoli
Il programma decide casualmente se inserire un blocco nella parte alta o bassa dello schermo e per quanto tempo deve restare presente.
3. Pulsante tramite interrupt
Il pulsante è collegato a D2, che su Arduino Uno può generare un interrupt esterno. Quando viene premuto, la funzione buttonPush() imposta un flag che avvia il salto.
4. Animazione del salto
Il salto non è una singola posizione: viene suddiviso in diverse fasi, così il personaggio sale, resta per alcuni frame nella riga superiore e poi torna verso il terreno.
5. Collisione
Prima di disegnare il personaggio, il programma controlla se nella stessa posizione è presente un blocco. Se sì, la funzione restituisce una collisione e la partita termina.
6. Punteggio
La variabile distance cresce durante la partita. Una versione ridotta del valore viene visualizzata nell’angolo superiore destro come punteggio.
Funzione AUTOPLAY
La logica AUTOPLAY osserva il terreno poco davanti al personaggio e genera automaticamente il comando di salto quando rileva un ostacolo nella riga inferiore.
Modificare la velocità del gioco
La velocità dipende soprattutto dal ritardo alla fine del ciclo:
delay(200);
Riducendo il valore gli ostacoli scorrono più velocemente e il gioco diventa più difficile. Aumentandolo il movimento rallenta.
Simulazione Tinkercad
▶️ Apri la simulazione Tinkercad
Scarica lo sketch
⬇️ Scarica arduino_gioco_lcd_game.ino
Debug rapido
- se il display non mostra nulla, controlla i collegamenti RS, E e D4–D7;
- se il pulsante non risponde, verifica D2 e il collegamento verso GND;
- se i caratteri appaiono strani, controlla che
lcd.begin(16, 2)sia corretto; - se il gioco è troppo veloce, aumenta il
delay(200)finale; - se l’AUTOPLAY interferisce con altri circuiti, ricorda che utilizza D1;
- se modifichi i pin del display, aggiorna anche il costruttore
LiquidCrystal.
Varianti del progetto
- aggiungere più tipi di ostacoli;
- creare livelli con velocità crescente;
- aggiungere un buzzer per salto e game over;
- salvare il record nella EEPROM;
- mostrare vite o bonus;
- aggiungere un secondo pulsante per nuove azioni;
- realizzare una versione su display OLED.
Cosa devi ricordare
- Il gioco utilizza un LCD 16×2 in modalità parallela.
- Il pulsante START e salto è collegato a D2.
- Il personaggio e i blocchi sono caratteri LCD personalizzati.
- Gli ostacoli vengono generati casualmente.
- Il punteggio aumenta con la distanza percorsa.
- La velocità può essere modificata cambiando il ritardo finale.
Domande frequenti
Serve una libreria aggiuntiva?
No. Il progetto utilizza LiquidCrystal, normalmente disponibile con Arduino IDE.
Posso usare un display LCD I2C?
Sì, ma il codice va adattato alla libreria I2C perché cambiano inizializzazione e collegamenti del display.
Come aumento la difficoltà?
Puoi ridurre il ritardo finale, aumentare la frequenza degli ostacoli oppure modificare la loro durata.
Posso salvare il record?
Sì. Puoi memorizzare il punteggio massimo nella EEPROM e confrontarlo con il punteggio ottenuto al termine di ogni partita.

