Arduino Projects

LCD Hill Run – An Arduino Jump’n’Run

Joystick in front of old computer

Who reads data from sensors all day long deserves a round of gambling in the evening! ūüôā For this little Arduino Jump’n’Run you only need a few components (and some wires). You create it in a few minutes and can start right away.

LCD Hill Run in Action

The idea and the code for this game came from Miles C., who published it under the license CC BY-SA 4.0 in the Arduino Project Hub. We won’t explain the whole sketch of the game, but we will explain a great function you might not know yet: attachInterrupt()

For this project you need:

* Amazon Affiliate Links – If you order there, we receive a small commission.

How the game works

The “story” of the game is quickly told: You play a small character who runs on an LCD screen by himself and has to avoid obstacles. You have to either jump or duck. For each obstacle you overcome, you receive one point. If you get stuck on an obstacle, you lose and your score is displayed.

The set up of the project

As you can see on the screenshot below, there are many wires involved in this project. The LCD display needs a lot of cables. If you use a display with I²C and an integrated potentiometer, you can reduce the amount of cables a lot.

Set up the game as follows and check all connections again before you start:

Setting up the Arduino game Hill Run
Screenshot: Tinkercad

The sketch of the Arduino Jump’n’Run

We will not go into every detail at this point, many of the concepts in the following sketch are explained in detail in these projects and tutorials on Pollux Labs:

One function in the sketch is quite rare, although it can be very helpful: attachInterrupt()

The function attachInterrupt()

Let’s first look at a common concept: In this sketch you want to react to a pressed button.

void loop() {
  
  buttonState = digitalRead(buttonPin);

  if (buttonState == HIGH) {
    digitalWrite(ledPin, HIGH);
  }
  else {
    digitalWrite(ledPin, LOW);
  }
}

Here, digitalRead() first reads the state of the button and then responds with an LED in an if/else request.

This works, but is a bit impractical if you have something else to do in the loop than just waiting for someone to press the button. It is better to let the monitoring of the button do a so-called interrupt, because with this you can run several processes at the same time. In this game this is the reading of the two buttons and the animations on your LCD display. Such an interrupt can look like this:

volatile int buttonState = 0;

void setup() {
 
  pinMode(ledPin, OUTPUT);
  pinMode(buttonPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(buttonPin), pin_ISR, CHANGE);
}

void loop() {
}

void pin_ISR() {
  buttonState = digitalRead(buttonPin);
  digitalWrite(ledPin, buttonState);
}

As you can see, the loop is even empty here, the button request is now in the setup. This means you have space in the loop for other things. The function attachInterrupt() has the following syntax:

attachInterrupt(digitalPinToInterrupt(pin), ISR, mode) 

The parameters interrupt (the pin to be monitored), ISR (the function executed when the button is pressed) and mode (When is the trigger activated?) play a role here. For detailed information please refer to the Arduino reference.

Back to the sketch of the game: As soon as you press one of the two buttons, a corresponding function is called, which makes your character either jump – seeJumping() – or duck – seeDucking().

Apart from that, most of the sketch of this Arduino Jump’n’Run consists of requests and animations that will probably overwhelm you at first sight. But with a little practice and time you’ll get through it – if you want to and don’t want to just play. ūüôā

Here is the complete sketch:

/*
 * Copyright (c) 2020 by Miles C.
*/

#include <LiquidCrystal.h>
#include "pitches.h"
LiquidCrystal lcd(7, 8, 9, 10, 11, 12);

const int JUMP_PIN = 2;
const int BUZZER_PIN = 5;
const int DUCK_PIN = 3;

const int JUMP_PITCH = 2700; //sounds when button pressed
const int JUMP_PITCH_DURATION = 50; //sounds when button pressed
const int DUCK_PITCH = 1350; //sounds when button pressed
const int DUCK_PITCH_DURATION = 50; //sounds when button pressed
const int DIE_PITCH = 200; //sounds on death
const int DIE_PITCH_DURATION = 500; //sounds on death
const int TICKSPEED = 90; //ms per gametick, 1 gametick per hill move.
const int JUMP_LENGTH = 3; //chars jumped over when jump is pressed.
const byte stickStep1[8] = {
  B01110,
  B01110,
  B00101,
  B11111,
  B10100,
  B00110,
  B11001,
  B00001,
};
const byte stickStep2[8] = {
  B01110,
  B01110,
  B00101,
  B11111,
  B10100,
  B00110,
  B01011,
  B01000,
};
const byte stickJump[8] = {
  B01110,
  B01110,
  B00100,
  B11111,
  B00100,
  B11111,
  B10001,
  B00000,
};
const byte stickDuck[8] = {
  B00000,
  B00000,
  B00000,
  B01110,
  B01110,
  B11111,
  B00100,
  B11111,
};
const byte hill[8] = {
  B00000,
  B00000,
  B01110,
  B01110,
  B01110,
  B11111,
  B11111,
  B11111,
};
const byte crow1[8] = {
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B01110,
  B01110,
  B01110,
};
const byte crow2[8] {
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B01110,
  B01110,
  B01110,
};

volatile int jumpPhase = JUMP_LENGTH + 1;
int gameTick = 0;
int crowX = 40;
int hillX = 25;
bool playerY = 0;
volatile bool ducking = LOW;
bool loopBreaker = 1;
bool crowGo = 0;
int score = 0;

void setup() {
  pinMode(JUMP_PIN, INPUT);
  pinMode(BUZZER_PIN, OUTPUT);
  lcd.begin(16, 2);
  lcd.createChar(0, hill);
  lcd.createChar(1, stickStep1);
  lcd.createChar(2, stickStep2);
  lcd.createChar(3, stickJump);
  lcd.createChar(4, stickDuck);
  lcd.createChar(5, crow1);
  lcd.createChar(6, crow2);
  attachInterrupt(digitalPinToInterrupt(JUMP_PIN), seeJumping, RISING);
  attachInterrupt(digitalPinToInterrupt(DUCK_PIN), seeDucking, CHANGE);
}

void loop() {
  playerY = 0;
  if (jumpPhase < JUMP_LENGTH) {
    playerY = 1;
  }

  drawSprites();

  loopBreaker = 1;
  if (hillX < 16) {
    if (crowX < hillX) {
      hillX += 8;
      loopBreaker = 0;
    }
    if (loopBreaker) {
      lcd.setCursor(hillX, 1);
      lcd.write((byte)0);
    }
  }
  if (hillX < 1) {
    if (jumpPhase < JUMP_LENGTH) {
      score++;
      hillX = 16 + rand() % 8;
    } else {
      endGame();
    }
  }
  if (crowX < 16) {
    lcd.setCursor(crowX, 0);
    if (gameTick % 8 < 4) {
      lcd.write((byte)5);
    } else {
      lcd.write((byte)6);
    }
  }
  if (crowX < 1) {
    if (ducking) {
      score++;
      crowX = 24 + rand() % 16;
    } else {
      endGame();
    }
  }
  lcd.setCursor(0, playerY);
  lcd.print(" ");
  jumpPhase++;
  hillX--;
  crowGo = !crowGo;
  crowX -= crowGo;
  gameTick++;
  delay(TICKSPEED);
}

void endGame() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Score: ");
  lcd.setCursor(7, 0);
  lcd.print(score);
  tone(BUZZER_PIN, DIE_PITCH, DIE_PITCH_DURATION);
  while (!digitalRead(JUMP_PIN)) {
    lcd.setCursor(0, 1);
    if (millis() % 500 < 250) {
      lcd.print("Jump to Continue");
    } else {
      lcd.print("                ");
    }
  }
  lcd.clear();
  score = 0;
  hillX = 25;
  crowX = 40;
}

void drawSprites() {
  lcd.setCursor(0, 1 - playerY);

  if (!ducking) {
    if (!playerY) {
      if ((gameTick % 4) < 2 ) {
        lcd.write((byte)1);
      } else {
        lcd.write((byte)2);
      }
    } else {
      lcd.write((byte)3);
    }
  } else {
    lcd.write((byte)4);
  }
  lcd.setCursor(1, 1);
  lcd.print("               ");
  lcd.setCursor(1, 0);
  lcd.print("               ");
}
void seeJumping() {
  if (jumpPhase > (JUMP_LENGTH + 2) && !ducking) {
    jumpPhase = 0;
    tone(BUZZER_PIN, JUMP_PITCH, JUMP_PITCH_DURATION);
  }

}
void seeDucking() {
  ducking = digitalRead(DUCK_PIN);
  if (ducking) {
    jumpPhase = JUMP_LENGTH;
    tone(BUZZER_PIN, DUCK_PITCH, DUCK_PITCH_DURATION);
  }
}

You may also like

Comments are closed.