Endless Runner Game
Introduction
Popularized by mobile games like Temple Run, the endless running type of game is an extremely simplistic spin on the larger "platform" genre where a player has limited control over a character that is constantly moving forward. Flappy bird, while generally not considered a "running" game, was another popular infinite platform game with limited control over the character: users could only tap the screen to make the bird fly upward in order to navigate through obstacles while constantly moving to the right.
We're going to make our own endless runner game using an Arduino, button and character LCD. While not as visually appealing as Temple Run or Flappy Bird, it's almost as addicting.
Required Materials
You can complete this project with parts from the SparkFun Inventor's Kit v4.0. Specifically, you will need:
Suggested Reading
If you aren't familiar with the following concepts, we recommend checking out these tutorials before continuing:
How to Solder: Through-Hole Soldering
What is an Arduino?
Installing Arduino IDE
Hardware Assembly
Using jumper wires, connect the components as shown in the diagram below.
Programming
Note: This example assumes you are using the latest version of the Arduino IDE on your desktop. If this is your first time using Arduino, please review our tutorial on installing the Arduino IDE.
Copy and paste the following code in the Arduino IDE. Click Upload to send your compiled code to the Arduino.
/** * Endless Runner * Date: August 18, 2017 * Author: Joshua Brooks (http://www.instructables.com/member/joshua.brooks/) * Modified by: Shawn Hymel (SparkFun Electronics) * * Adjust LCD contrast with the potentiometer. Press the button * to start the game. During gameplay, press the button to jump * and avoid obstacles. Running into an object will result in * restarting. Points are awarded based on distance. */ // Constants // Globals LiquidCrystal lcd(8, 9, 10, 11, 12, 13); static char terrainUpper[TERRAIN_WIDTH + 1]; static char terrainLower[TERRAIN_WIDTH + 1]; static bool buttonPushed = false; void setup() { initializeGraphics(); lcd.begin(16, 2); pinMode(BTN_PIN, INPUT_PULLUP); } 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; // Check if button is pressed if ( digitalRead(BTN_PIN) == LOW ) { buttonPushed = true; } // Show start screen if not currently playing game 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; if (buttonPushed) { initializeGraphics(); heroPos = HERO_POSITION_RUN_LOWER_1; playing = true; buttonPushed = false; distance = 0; } return; } // Shift the terrain to the left advanceTerrain(terrainLower, newTerrainType == TERRAIN_LOWER_BLOCK ? SPRITE_TERRAIN_SOLID : SPRITE_TERRAIN_EMPTY); advanceTerrain(terrainUpper, newTerrainType == TERRAIN_UPPER_BLOCK ? SPRITE_TERRAIN_SOLID : SPRITE_TERRAIN_EMPTY); // Make new terrain to enter on the right 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); } } // Jump if button is pressed if (buttonPushed) { if (heroPos <= HERO_POSITION_RUN_LOWER_2) heroPos = HERO_POSITION_JUMP_1; buttonPushed = false; } // Draw hero on screen and check for collisions if (drawHero(heroPos, terrainUpper, terrainLower, distance >> 3)) { playing = false; // The hero collided with something. Too bad. } else { 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; } delay(100); } // Create custom character LCD graphics void initializeGraphics() { static byte graphics[] = { // Run position 1 B01100, B01100, B00000, B01110, B11100, B01100, B11010, B10011, // Run position 2 B01100, B01100, B00000, B01100, B01100, B01100, B01100, B01110, // Jump B01100, B01100, B00000, B11110, B01101, B11111, B10000, B00000, // Jump lower B11110, B01101, B11111, B10000, B00000, B00000, B00000, B00000, // Ground B11111, B11111, B11111, B11111, B11111, B11111, B11111, B11111, // Ground right B00011, B00011, B00011, B00011, B00011, B00011, B00011, B00011, // Ground left B11000, B11000, B11000, B11000, B11000, B11000, B11000, B11000, }; int i; // Skip using character 0, this allows lcd.print() to be used // to quickly draw multiple characters for (i = 0; i < 7; ++i) { lcd.createChar(i + 1, &graphics[i * 8]); } // Fill screen with empty terrain for (i = 0; i < TERRAIN_WIDTH; ++i) { terrainUpper[i] = SPRITE_TERRAIN_EMPTY; terrainLower[i] = SPRITE_TERRAIN_EMPTY; } } // Slide the terrain to the left in half-character increments 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; } } } // Draw hero on screen and check for collisions 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, lower; // Draw the appropriate sprite for the hero (run or jump) 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; } // Detect collisions with terrain 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; } // Calculate number of digits needed to draw the score byte digits = (score > 9999) ? 5 : (score > 999) ? 4 : (score > 99) ? 3 : (score > 9) ? 2 : 1; // Draw the scene terrainUpper[TERRAIN_WIDTH] = '\0'; terrainLower[TERRAIN_WIDTH] = '\0'; char temp = terrainUpper[16 - digits]; terrainUpper[16 - digits] = '\0'; lcd.setCursor(0, 0); lcd.print(terrainUpper); terrainUpper[16 - digits] = temp; lcd.setCursor(0, 1); lcd.print(terrainLower); // Draw score in upper right of screen lcd.setCursor(16 - digits, 0); lcd.print(score); terrainUpper[HERO_HORIZONTAL_POSITION] = upperSave; terrainLower[HERO_HORIZONTAL_POSITION] = lowerSave; return collide; }
What You Should See
When the code finishes uploading, you should be presented with a flashing "Press Start" notification. If you do not see anything on the LCD, try turning the potentiometer to adjust the contrast.
Press the button (the only button available) to begin the game. Once the game starts, you need to press the button again to jump in order to avoid the obstacles that zoom by. If you hit an obstacle, you must start over. The farther you run, the higher your score (as shown by the number in the upper right corner).
Resources and Going Further
The code and circuit diagram for the Endless Runner Game project can be found on GitHub.
Challenges
- Add a difficulty modifier to the game: for every 20 points earned, the terrain moves slightly faster.
- Add a second button and remove the "gravity" effect (the part that causes the hero to move back to the first row after jumping). One button causes the hero to move up, and the other causes the hero to move down. This creates more of an "endless flying" game rather than "endless running." Feel free to change the hero graphic to a plane, spaceship, bird, mosquito, etc.
- Use an ultrasonic sensor to control the hero rather than one or more buttons. Set a threshold so that when an object (e.g., a hand) is more than 5 inches from the sensor, the hero is on the top row, and when the object is less than 5 inches from the sensor, the hero is on the bottom row.
Need some inspiration for your next project? Check out some of these related tutorials: