Arduino Infinite Runner

For my main project, I made an infinite runner game using an Arduino and a LCD display. In infinite runner games, it’s your job as the player to run, infinitely, while dodging randomly generated obstacle. While carrying around an Arduino circuit board may be a little awkward, as circuit boards can break, putting it in a case can solve this problem, making for a fun, portable game.

Engineer

Aidan R

Area of Interest

Video games / computer engineering

School

Garden City High School

Grade

Rising Junior

Modification

Modification: Neopixels

As a modification, I added Neopixels onto my project and I have them set to change colors depending on whether or not the player is alive in the game. The neopixels have three wires attached to them, one is attached to power, another one is attached to a pin on the arduino, and the last one is grounded. To actually change the color of the lights, I used the colorWipe(); function in my code and I can change one of three values to control the color. Those three values are red, green, and blue. So if I have 255 red, 0 green, and 0 blue then the neopixels will display red

Second Milestone

Code.

#include <LiquidCrystal.h>

// Constants
#define BTN_PIN 7

#define SPRITE_RUN1 1
#define SPRITE_RUN2 2
#define SPRITE_JUMP 3
#define SPRITE_JUMP_UPPER ‘.’ // Use the ‘.’ character for the head
#define SPRITE_JUMP_LOWER 4
#define SPRITE_TERRAIN_EMPTY ‘ ‘ // User the ‘ ‘ character
#define SPRITE_TERRAIN_SOLID 5
#define SPRITE_TERRAIN_SOLID_RIGHT 6
#define SPRITE_TERRAIN_SOLID_LEFT 7

#define HERO_HORIZONTAL_POSITION 1 // Horizontal position of hero on screen

#define TERRAIN_WIDTH 16
#define TERRAIN_EMPTY 0
#define TERRAIN_LOWER_BLOCK 1
#define TERRAIN_UPPER_BLOCK 2

#define HERO_POSITION_OFF 0 // Hero is invisible
#define HERO_POSITION_RUN_LOWER_1 1 // Hero is running on lower row (pose 1)
#define HERO_POSITION_RUN_LOWER_2 2 // (pose 2)

#define HERO_POSITION_JUMP_1 3 // Starting a jump
#define HERO_POSITION_JUMP_2 4 // Half-way up
#define HERO_POSITION_JUMP_3 5 // Jump is on upper row
#define HERO_POSITION_JUMP_4 6 // Jump is on upper row
#define HERO_POSITION_JUMP_5 7 // Jump is on upper row
#define HERO_POSITION_JUMP_6 8 // Jump is on upper row
#define HERO_POSITION_JUMP_7 9 // Half-way down
#define HERO_POSITION_JUMP_8 10 // About to land

#define HERO_POSITION_RUN_UPPER_1 11 // Hero is running on upper row (pose 1)
#define HERO_POSITION_RUN_UPPER_2 12

// Globals
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
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;
}

For my second milestone, I got the sample code from the project website and edited it to match my wiring and pin setup in order for my game to work. The code uses a mix of defined variables, input pins, input pins, and static Booleans to figure out if the button is pressed, make the player jump, and move the obstacles forward. I also changed my wiring for this milestone. I removed the resistors and I changed out the potentiometer. This is the end of my base project, I can now move on to modifications.

Link to project website.

First Milestone

ms1_cropped

My goal for my first milestone was to have the LCD display take input from the button. During the first milestone I made sure I received all the parts and I made sure they all worked. After that, I had to learn some of the syntax for coding Arduino and how to wire all the hardware. I then tested the button by lighting up an LED and the display by displaying text. After that, I just combined the two and coded it. The circuit works by having the electric current from a computer or battery flow into the Arduino and then to the breadboard. The current then breaks into two paths, one powers the LCD so it can display words and the other meets the button. If the button is unpressed, the current will immediately be grounded, but if the button is pressed, the current will go through the button and into pin 7 which tells the display to change the text through a series of code.

wiring diagram

wiringdiagram

Starter Project: TV-B-Gone

My Starter Project is the TV-B-Gone, a device which can turn off nearly any TV. If you press the button with a clear line of sight of a TV, the TV will turn off within seconds. I enjoyed this project because I learned about different electrical components, their roles in this circuit, and how to solder.

project

tbg_cropped

How it works

The TV-B-Gone is basically a remote control that can turn off almost any TV within 150 feet. This kit is made up of a circuit board, a green 3mm LED, a button, two 1.0 kohm resistors (brown, black, red, gold), a PNP transistor (EBC pinout; PN2907), two narrow-beam 940nm IR lights (blueish), two wide-beam 940nm IR lights (clear), one 8mHz ceramic oscillator, a 220uF or greater capacitor with a 6.3V or higher rating, a 0.1uF ceramic capacitor, an IC1 microcontroller, IC1’ 8-pin socket, and four NPN transistors (TO-92)(PN2222). The TV-B-Gone is powered by two AA batteries and must have a power source that is within 2.5V to 5V as to not damage the kit. The power source must also be able to easily supply 400-1000mA. When electricity is flowing from the batteries to the circuit board, pressing the button will allow the current to enter the circuit and the green test light should light up, this means that there’s electricity flowing. The electric current then flows through a couple resistors and a transistor to get to the pre-programmed microcontroller which is like the brain of the circuit board, it controls everything. Along with the microcontroller is a capacitor, which stores electricity for a very brief period of time, and the oscillator. The oscillator is like a clock or metronome, it keeps time, and by working with the microcontroller, it keeps all the different aspects of the device in sync so that it can properly complete its task. In this case, the oscillator-microcontroller combo makes sure the IR LEDs are sending the correct pattern of infrared waves, invisible radiation to the TV’s IR receiver. After reaching the microcontroller, the electric current flows to more transistors in front of the IR LEDs. In this case, as well as amplifying the current, the transistors are acting as switches to turn on and off the LEDs. On when there’s a current, and off when there isn’t. The TV-B-Gone turns off TVs by mimicking a remote control and it does this by sending out infrared waves with a wavelength of 940nm, the same as a TV remote, and it sends them in a certain pattern so that the TV’s IR receiver can translate them into binary commands that the microcontroller in the TV can understand, such as shut down. The two types of IR LEDs emit the waves differently. The blue ones emit waves in a narrow path but long range and the clear ones do a wide path and short range. This project was my first experience with soldering and circuit boards, and I’m very excited to begin working on my main project, an arduino infinite runner game.

This is a link to the instructions

Completed Circuit Board

tbg_cropped1
Image Source: https://github.com/adafruit/Adafruit-MiniPOV4-Kit/tree/master/Hardware

Start typing and press Enter to search

Bluestamp Engineering