Dino Jumper Game In LCD Display

Dino Jumper Game In LCD Display

HARDWARE REQUIRED:

  • PICUNO Microcontroller board
  • 1 × 16x2 I2C LCD Display
  • 1 × HW-504 Joystick Module
  • Jumper wires
  • USB cable
  • 4xAA Battery Pack (for external power supply)

DESCRIPTION:

This project creates a complete, feature-rich "Chrome Dino" style side-scrolling game on a 16x2 character LCD. The player controls a dinosaur character who must dodge randomly spawning obstacles by switching between the top and bottom rows using a joystick. The game features a two-frame running animation, multiple obstacle types (cacti and birds), a real-time scoring system, and a "Game Over" state upon collision. To make it a complete experience, the game also saves the highest score to the microcontroller's permanent memory, displaying it on the start screen.

CIRCUIT DIAGRAM:

Dino Jumper Game In LCD Display
  • Connect the positive terminal of the 4xAA Battery Pack to the Common VCC on breadboard to create constant 5V supply for longer time.
  • Connect the negative terminal of the 4xAA Battery Pack to Common GND on breadboard.
  • Connect the LCD Module's GND pin to GND.
  • Connect the LCD Module's VCC pin to common VCC.
  • Connect the LCD Module's SDA pin GPIO 4 (SDA Pin on PICUNO).
  • Connect the LCD Module's SCL pin GPIO 5 (SCL Pin on PICUNO).
  • JOYSTICK MODULE:
    • Connect the VCC pin to Common VCC.
    • Connect the GND pin to GND.
    • Connect the VRy pin to Analog Pin A1 (GPIO 26).
    • Connect the SW (Switch) pin to GPIO 8.

SCHEMATIC:

LCD VCC → 5V

LCD GND → GND

LCD SDA → GPIO 4 (Board SDA Pin)

LCD SCL → GPIO 5 (Board SCL Pin)

Joystick Module:

VCC → 5V

GND → GND

VRy → A0

SW → GPIO 8

CODE -- C:

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

// --- Setup ---
LiquidCrystal_I2C lcd(0x27, 16, 2);
const int JOY_Y_PIN = A0;
const int RESTART_BUTTON_PIN = 8;

// --- Custom Characters & Sprites ---
#define SPRITE_DINO_RUN1 0
#define SPRITE_DINO_RUN2 1
#define SPRITE_DINO_DEAD 2
#define SPRITE_CACTUS 3
#define SPRITE_BIRD 4

byte dino_run1_char[8] = {0b00111,0b00101,0b00110,0b10100,0b01110,0b01010,0b00110,0b00011};
byte dino_run2_char[8] = {0b00111,0b00101,0b00110,0b10100,0b01110,0b01010,0b00110,0b01010};
byte dino_dead_char[8] = {0b00111,0b00111,0b00110,0b10101,0b01110,0b11011,0b00000,0b00000};
byte cactus_char[8] = {0b00100,0b01110,0b11101,0b11111,0b00100,0b00100,0b00100,0b00100};
byte bird_char[8] = {0b00000,0b00110,0b01111,0b11011,0b00110,0b01100,0b00000,0b00000};

// --- Game Variables ---
int playerY = 1;
int score = 0;
bool gameOver = true;
unsigned long lastFrameTime = 0;
int frameCount = 0;
int gameSpeed = 100; // Faster speed

// --- Obstacle Variables ---
const int MAX_OBSTACLES = 4;
const int OBSTACLE_SPACING = 6;
int obstacleX[MAX_OBSTACLES];
int obstacleY[MAX_OBSTACLES];

void setup() {
  pinMode(JOY_Y_PIN, INPUT);
  pinMode(RESTART_BUTTON_PIN, INPUT_PULLUP);
  lcd.init();
  lcd.backlight();
  // Load characters into memory
  lcd.createChar(SPRITE_DINO_RUN1, dino_run1_char);
  lcd.createChar(SPRITE_DINO_RUN2, dino_run2_char);
  lcd.createChar(SPRITE_DINO_DEAD, dino_dead_char);
  lcd.createChar(SPRITE_CACTUS, cactus_char);
  lcd.createChar(SPRITE_BIRD, bird_char);
  randomSeed(analogRead(A0));
  EEPROM.begin(512); // Initialize EEPROM for RP2040
}

void resetGame() {
    playerY = 1; gameOver = false; score = 0;
    for (int i = 0; i < MAX_OBSTACLES; i++) obstacleX[i] = -1;
    lastFrameTime = millis();
}

void drawGameState() {
  lcd.clear();
  // Draw Player
  lcd.setCursor(1, playerY);
  lcd.write(frameCount % 4 < 2 ? SPRITE_DINO_RUN1 : SPRITE_DINO_RUN2);
  // Draw Obstacles
  for (int i = 0; i < MAX_OBSTACLES; i++) {
    if (obstacleX[i] >= 0) {
      lcd.setCursor(obstacleX[i], obstacleY[i]);
      lcd.write(obstacleY[i] == 0 ? SPRITE_BIRD : SPRITE_CACTUS);
    }
  }
  // Draw Score
  lcd.setCursor(10, 0);
  lcd.print("S:");
  lcd.print(score);
}

void loop() {
  if (gameOver) {
    int highscore = 0;
    EEPROM.get(0, highscore);
    lcd.clear();
    lcd.print("Chrome Dino Game");
    lcd.setCursor(0, 1);
    lcd.print("Best:");
    lcd.print(highscore);
    lcd.print(" Press");
    
    while(digitalRead(RESTART_BUTTON_PIN) == HIGH) { /* Wait */ }
    while(digitalRead(RESTART_BUTTON_PIN) == LOW) { /* Wait for release */ }
    resetGame();
    return;
  }

  if (millis() - lastFrameTime < gameSpeed) return;
  lastFrameTime = millis();
  frameCount++;

  int yValue = analogRead(JOY_Y_PIN);
  if (yValue < 300) playerY = 0;
  else if (yValue > 700) playerY = 1;

  for (int i = 0; i < MAX_OBSTACLES; i++) {
    if (obstacleX[i] >= 0) {
      obstacleX[i]--;
      if (obstacleX[i] < 0) score++;
    }
  }
  bool canSpawn = true;
  for (int x : obstacleX) {
    if (x > (15 - OBSTACLE_SPACING)) canSpawn = false;
  }
  if (canSpawn) {
    for (int i = 0; i < MAX_OBSTACLES; i++) {
      if (obstacleX[i] < 0) {
        if (random(0, 6) == 0) {
          obstacleX[i] = 15;
          obstacleY[i] = random(0, 2);
        }
        break;
      }
    }
  }

  for (int i = 0; i < MAX_OBSTACLES; i++) {
    if (obstacleX[i] == 1 && obstacleY[i] == playerY) {
      gameOver = true;
    }
  }

  drawGameState();

// --- Handle Game Over ---
  if (gameOver) {
    int highscore = 0;
    EEPROM.get(0, highscore);
    
    if (score > highscore) {
      EEPROM.put(0, score);
      EEPROM.commit();
    }
    
    lcd.clear();
    lcd.setCursor(1, 1);
    lcd.write(SPRITE_DINO_DEAD);
    lcd.setCursor(4, 0);
    lcd.print("GAME OVER");
    delay(1500);
    
    EEPROM.get(0, highscore);
    lcd.clear();
    lcd.print("Score: "); lcd.print(score);
    lcd.setCursor(0, 1);
    lcd.print("Best: "); lcd.print(highscore);
    delay(2000);
  }
}
Custom Characters - The code defines pixel patterns for the running animation, a dead dino, a cactus, and a bird. lcd.createChar() loads them into the LCD's memory.

Game Loop - The if (millis() - lastFrameTime < gameSpeed) logic creates a consistent frame rate, making the game speed adjustable and playable.

Joystick Control - analogRead(JOY_Y_PIN) reads the joystick's vertical position and sets the player's row (playerY) accordingly.

Random Obstacles - The code randomly spawns either a high obstacle (bird) or a low obstacle (cactus) at random intervals.

Collision & Scoring - The code checks if the player's coordinates match any obstacle's coordinates. It also increments the score when an obstacle is successfully passed.

High Score - The EEPROM.h library is used to get() the high score from memory at the end of a game. If the new score is higher, it is saved with EEPROM.put() and then permanently committed to flash memory with EEPROM.commit().

CODE -- PYTHON:

from machine import Pin, ADC, I2C
from i2c_lcd import I2cLcd
from time import sleep_ms, ticks_ms, ticks_diff
import random

i2c = I2C(0, scl=Pin(5), sda=Pin(4))
lcd = I2cLcd(i2c, 0x27, 2, 16)
button = Pin(8, Pin.IN, Pin.PULL_UP)
adc_y = ADC(Pin(26))

# Define Custom Characters
dino_run1_char = bytearray([0b00111,0b00101,0b00110,0b10100,0b01110,0b01010,0b00110,0b00011])
dino_run2_char = bytearray([0b00111,0b00101,0b00110,0b10100,0b01110,0b01010,0b00110,0b01010])
dino_dead_char = bytearray([0b00111,0b00111,0b00110,0b10101,0b01110,0b11011,0b00000,0b00000])
cactus_char = bytearray([0b00100,0b01110,0b11101,0b11111,0b00100,0b00100,0b00100,0b00100])
bird_char = bytearray([0b00000,0b00110,0b01111,0b11011,0b00110,0b01100,0b00000,0b00000])

lcd.create_char(0, dino_run1_char); lcd.create_char(1, dino_run2_char)
lcd.create_char(2, dino_dead_char); lcd.create_char(3, cactus_char); lcd.create_char(4, bird_char)

def read_highscore():
    try:
        with open('highscore.txt', 'r') as f: return int(f.read())
    except (OSError, ValueError): return 0

def write_highscore(new_score):
    try:
        with open('highscore.txt', 'w') as f: f.write(str(new_score))
    except OSError: print("Failed to save high score.")

# Game Variables
MAX_OBSTACLES = 4; OBSTACLE_SPACING = 6; game_speed_ms = 100

while True:
    highscore = read_highscore(); lcd.clear(); lcd.putstr("Chrome Dino Game")
    lcd.move_to(0, 1); lcd.putstr(f"Best:{highscore} Press")
    while button.value() == 1: sleep_ms(20)
    while button.value() == 0: sleep_ms(20)

    # Reset Game
    player_y = 1; game_over = False; score = 0; frame_count = 0
    obstacle_x = [-1] * MAX_OBSTACLES; obstacle_y = [-1] * MAX_OBSTACLES
    last_frame_time = ticks_ms()

    # Main Game Loop
    while not game_over:
        if ticks_diff(ticks_ms(), last_frame_time) < game_speed_ms: continue
        last_frame_time = ticks_ms(); frame_count += 1

        y_val = adc_y.read_u16()
        if y_val < 20000: player_y = 0
        elif y_val > 45000: player_y = 1

        # Move obstacles & spawn new ones
        for i in range(MAX_OBSTACLES):
            if obstacle_x[i] >= 0: obstacle_x[i] -= 1
            if obstacle_x[i] < 0: score += 1

        can_spawn = all(x <= (15 - OBSTACLE_SPACING) for x in obstacle_x)
        if can_spawn and random.randint(0, 5) == 0:
            for i in range(MAX_OBSTACLES):
                if obstacle_x[i] < 0: obstacle_x[i] = 15; obstacle_y[i] = random.randint(0, 1); break

        # Collision detection
        for i in range(MAX_OBSTACLES):
            if obstacle_x[i] == 1 and obstacle_y[i] == player_y: game_over = True; break

        # Draw everything
        lcd.clear()
        player_sprite = chr(0 if frame_count % 2 == 0 else 1)
        lcd.move_to(1, player_y); lcd.putstr(player_sprite)
        for i in range(MAX_OBSTACLES):
            if obstacle_x[i] >= 0:
                sprite = chr(4 if obstacle_y[i] == 0 else 3)
                lcd.move_to(obstacle_x[i], obstacle_y[i]); lcd.putstr(sprite)
        lcd.move_to(10, 0); lcd.putstr(f"S:{score}")

    # Game Over
    highscore = read_highscore()
    if score > highscore: write_highscore(score); highscore = score
    lcd.clear(); lcd.move_to(1,1); lcd.putstr(chr(2))
    lcd.move_to(4, 0); lcd.putstr("GAME OVER"); sleep_ms(1500)
    lcd.clear(); lcd.putstr(f"Score: {score}")
    lcd.move_to(0,1); lcd.putstr(f"Best: {highscore}"); sleep_ms(2000)
Custom Characters - The code defines pixel patterns for the running dinosaur animation, a dead dino, a cactus, and a bird, then loads them into the LCD's memory using lcd.create_char().

Game Loop - The ticks_diff() logic creates a consistent frame rate, controlled by game_speed_ms, making the game's difficulty stable and adjustable.

Joystick Control - adc_y.read_u16() reads the joystick's vertical position. The code checks if this value is above or below certain thresholds to move the player to the top or bottom row.

Random Obstacles - The game randomly spawns either a high obstacle (bird) or a low obstacle (cactus) at random intervals, making each playthrough unique.

Collision & Scoring - The code checks if the player's coordinates match any obstacle's coordinates. It also increments the score when an obstacle is successfully passed.

High Score - The read_highscore() and write_highscore() functions handle reading and writing the best score to a file (highscore.txt) in the board's internal flash memory, making it persistent between games.