Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitchesSome links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
This project builds an LCD endless-runner game, not a motorized Arduino car. A custom car sprite runs across the lower row of a 16×2 I2C character LCD while building-like obstacles scroll toward it. Press a tactile button to jump; survive longer to increase the score.
The build is suitable for Arduino beginners and demonstrates I2C wiring, custom LCD characters, button input, timing, collision detection, and simple game-state logic.
How the game works
The display has only two rows, so the game uses a deliberately simple layout:
- The car normally occupies a fixed position on the lower row.
- Terrain and buildings move from right to left.
- Pressing the button moves the car to the upper row for a short jump.
- If the car reaches a non-empty obstacle while it is on the ground, the run ends.
- The score increases as the game continues.
Here, “car” means an on-screen custom character. There are no motors, wheels, motor drivers, ultrasonic sensors, or RC controls.
#1 Best Overall
- BUILD, CODE & DRIVE YOUR OWN ROBOT CAR: Turn coding, electronics and engineering into a working programmable robot car you can assemble, program and drive; ideal for weekend family projects, STEM classrooms, coding clubs, robotics lessons and maker challenges
- EXPLORE FPV, LINE TRACKING & OBSTACLE AVOIDANCE: Control the robot with the ELEGOO app or IR remote, view live FPV video through the onboard camera, follow black lines, avoid obstacles with the ultrasonic sensor and explore multiple interactive driving modes
- BEGINNER-FRIENDLY BUILD WITH GUIDED WIRING: Keyed XH2.54 connectors help reduce wiring mistakes, while the illustrated tutorial and example programs guide beginners step by step from chassis assembly and module connection to programming and the first successful run
- GO BEYOND ASSEMBLY WITH CREATIVE CODING: Program with Arduino IDE to explore movement, sensors and control logic, then modify example code to create custom routes, reactions and robotics experiments that develop coding, problem-solving and engineering skills
- COMPLETE RECHARGEABLE STEM ROBOTICS KIT: Includes an ELEGOO UNO R3 controller board, ESP32-WROVER-based camera and Wi-Fi module, line-tracking and ultrasonic sensors, motors, IR remote and a 2000 mAh rechargeable lithium-ion battery; recommended for ages 8+ with adult guidance for first-time builders
The original project by Bruno Opaiva was published on Arduino Project Hub on March 8, 2022, with a corresponding Hackster project published on March 12, 2022. This version follows the same idea but uses explicit INPUT_PULLUP button wiring and non-blocking timing to make the behavior easier to understand and troubleshoot.
Original Arduino Project Hub project · Hackster project
Parts required
- Arduino Uno Rev3 or Uno-compatible board
- 16×2 LCD with an I2C backpack
- Tactile push button
- Breadboard
- Male-to-male and male-to-female jumper wires as required by your modules
- USB-A-to-B data cable for an Uno
- Arduino IDE
The LCD must include an I2C backpack, or an equivalent I2C adapter. A bare parallel 1602 LCD cannot use the four-wire LCD connection shown below unless an adapter is fitted.
Uno or Nano?
An Uno is the easiest choice for a first build because its layout, USB connection, and documentation are beginner-friendly. A Nano can also run this type of game and is useful when space matters, but some Nano-compatible boards require a different USB driver or a different bootloader option during upload.
Wiring
LCD to Arduino Uno
| LCD I2C pin | Arduino Uno Rev3 |
|---|---|
| GND | GND |
| VCC | 5V |
| SDA | A4, or the dedicated SDA header pin |
| SCL | A5, or the dedicated SCL header pin |
On an Uno Rev3, A4 and A5 provide the I2C data and clock lines. Do not reverse SDA and SCL. Keep the ground connection common to the LCD and button.
Rank #2
- This is a newly designed 4-wheel car frame that can be used with other devices to realize function of tracing, obstacle avoidance, distance testing, autonomous driving, wireless remote control, etc.
- The smart robot car chassis has plenty of fixed mounting holes and room for expansion to add various sensors, actuators and controllers (such as Arduino, Raspberry Pi, Micro bit).
- 4WD Robot Car Kit maximum load 1KG; size of robot car chassis: 10*6*2.5 inches; wheel diameter: 2.56 inches
- 4 pcs TT Robot Gear Motor; Operating voltage: 3V~12VDC (recommended operating voltage of about 6 to 8V) Wires Length: 0.8 inch 24 AWG; Maximum torque: 800gf cm min (3V) ; No-load speed: 1:48 (3V)
- The DIY car kit will be easy to assemble according to the instructions we provide.It also comes with a battery case that can hold two 18650 batteries (batteries not included)
Arduino Uno Rev3 hardware documentation
Button to Arduino
| Button connection | Arduino |
|---|---|
| One terminal | Digital pin 2 |
| Other terminal | GND |
The sketch uses the Uno’s internal pull-up resistor:
pinMode(PIN_BUTTON, INPUT_PULLUP);
That means the input is HIGH when idle and LOW when pressed. A four-leg tactile switch normally has two electrically connected legs on each side. Place the switch across the breadboard’s center gap, or verify the connected legs with a continuity tester; otherwise both wires can accidentally be placed on the same side.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteInstall the software
- Install a current Arduino IDE release.
- Open Tools → Manage Libraries.
- Search for
LiquidCrystal I2C. - Install a library whose API supports the constructor and initialization used by the sketch below.
- Under Tools → Board, select your Uno-compatible board.
- Select the correct port under Tools → Port.
Menu labels can vary slightly between Arduino IDE releases. The built-in Wire library handles I2C communication and requires no separate installation.
LiquidCrystal_I2C is not one perfectly standardized implementation. Libraries with similar names can differ in their constructor, address handling, backlight method, and whether they use init() or a begin() form. Arduino’s library catalog lists version 1.1.2 and warns that it may not be compatible with existing sketches. If lcd.init() or the constructor fails, check the documentation for the library actually installed rather than changing calls blindly.
Arduino LiquidCrystal I2C library information · Original library repository · Alternative library repository
Rank #3
- Beginner-friendly: The ACEBOTT smart robot car kit is controlled by an advanced ESP32 controller board, making programming easy. Through 16 story-rich tutorials, students will systematically master the principles of programming and electronic hardware, and easily master the mysteries of the smart car. (The robot kit does not include batteries)
- Rich Expandability: ACEBOTT based on the classic omnidirectional mecanum wheel robot car kit, we have added a rich set of expansion packs that can be freely matched: camera expansion pack, robotic arm expansion pack, tank expansion pack, solar expansion pack. Whether it is App and IR remote control, photo taking, image recognition, voice recognition, tracking mode, shooting, or multi-degree-of-freedom robotic arms, etc., the STEM robot kit will satisfy your desire for exploration and unleash your creativity!
- All-round control: This ACEBOTT coding robot for kids is equipped with advanced 6cm omnidirectional Mecanum wheels, also known as omnidirectional wheels or lion wheels, which can easily achieve 360° movement in any direction, support multiple movement modes (forward, sideways, diagonal, rotation), and can complete difficult actions such as left and right drifting, and easily cross any position, including narrow bends, narrow alleys, and intricate roads.
- Multi-way Cruise & Multi-direction Obstacle Avoidance: Accurate multi-way cruise allows the rc control car to easily plan the path and realize autonomous navigation; multi-direction obstacle avoidance allows flexible response in the face of obstacles; the new follow mode allows the car to always follow your steps.
- IR remote Control and App Control: Allows children to control this robotics kit through the IR remote control and App, make you enjoy the fun and convenience of intelligent technology. Simply master all the actions of the car with just one touch.
Find the LCD’s I2C address
0x27 is common, but it is not universal. Other backpacks use 0x3F or another address. Run this diagnostic sketch before uploading the game:
#include <Wire.h>
void setup() {
Wire.begin();
Serial.begin(9600);
Serial.println("I2C scanner");
for (byte address = 1; address < 127; address++) {
Wire.beginTransmission(address);
byte error = Wire.endTransmission();
if (error == 0) {
Serial.print("Found I2C device at 0x");
if (address < 16) Serial.print("0");
Serial.println(address, HEX);
}
}
}
void loop() {}
Open Tools → Serial Monitor and set the baud rate to 9600. If the scanner reports 0x27, use:
LiquidCrystal_I2C lcd(0x27, 16, 2);
Replace 0x27 with the address reported by your own hardware. If nothing is detected, check power, ground, SDA, SCL, and the backpack solder joints.
Custom characters and the LCD’s limits
Each HD44780-style character is a 5×8 dot matrix. The game loads custom glyphs for the car, its jump pose, ground, and obstacle shapes. A standard character LCD has only a small number of custom-character slots—typically eight—so it cannot display unlimited unique graphics. That limitation explains the blocky art and requires the sketch to reuse glyphs carefully.
Custom characters must be created before the game draws them. Repeatedly calling lcd.clear() can produce visible flicker, so the sketch below redraws fixed-width rows instead. On a 16×2 display, the two rows also provide a convenient visual representation of ground and jumping.
Rank #4
- 【Complete Hardware】The kit includes LAFVIN R3 CH340 board, V5 expansion board, L298N motor driver, ultrasonic sensor, SG90 servo, DC motors, and more. All components are well-organized for quick assembly and easy use.
- 【Multiple Smart Functions】It supports ultrasonic obstacle avoidance and IR remote control, allowing the car to automatically detect and avoid obstacles or be controlled via the included remote.
- 【Easy Assembly】The modular design with standard connectors and clear wiring makes assembly simple for beginners. We provide tutorial and open source code libraries to help you build and program the car step by step.
- 【Educational STEM Learning】This kit is ideal for learning robotics, programming, and electronics. It helps users understand how microcontrollers work together, improving hands-on skills, logical thinking, and problem-solving abilities.
- 【Beginner Friendly】Compatible with the Arduino IDE, the kit allows for further customization and expansion. It’s perfect for classroom teaching, personal projects, and STEM competitions.
Upload this adapted game sketch
Set LCD_ADDRESS to the address found by the scanner. This is a compact, commented adaptation of the original project’s concept. It uses button polling rather than an interrupt because polling is easier for a first game and avoids interrupt bounce problems. The original sketch uses attachInterrupt(0, buttonPush, FALLING); on an Uno, interrupt 0 maps to digital pin 2.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
const byte LCD_ADDRESS = 0x27; // Change this after running the scanner
const byte PIN_BUTTON = 2;
const byte LCD_COLUMNS = 16;
const byte LCD_ROWS = 2;
const byte CAR_COLUMN = 2;
LiquidCrystal_I2C lcd(LCD_ADDRESS, LCD_COLUMNS, LCD_ROWS);
// Custom-character slots: 0 through 7
const byte SPRITE_CAR = 0;
const byte SPRITE_CAR_JUMP = 1;
const byte SPRITE_GROUND = 2;
const byte SPRITE_BLOCK = 3;
const unsigned long JUMP_TIME = 520;
const unsigned long START_SPEED = 420;
const unsigned long MIN_SPEED = 170;
byte terrain[LCD_COLUMNS];
bool running = false;
bool jumping = false;
unsigned long jumpStarted = 0;
unsigned long lastStep = 0;
unsigned long score = 0;
unsigned long speedMs = START_SPEED;
byte car[] = {
B00000, B00110, B01111, B11111,
B11111, B01110, B01010, B00000
};
byte carJump[] = {
B00000, B00110, B01111, B11111,
B11111, B01110, B00000, B01010
};
byte ground[] = {
B11111, B11111, B11111, B11111,
B11111, B11111, B11111, B11111
};
byte block[] = {
B00100, B01110, B01110, B11111,
B11111, B11111, B11111, B11111
};
void setup() {
pinMode(PIN_BUTTON, INPUT_PULLUP);
lcd.init();
lcd.backlight();
lcd.createChar(SPRITE_CAR, car);
lcd.createChar(SPRITE_CAR_JUMP, carJump);
lcd.createChar(SPRITE_GROUND, ground);
lcd.createChar(SPRITE_BLOCK, block);
resetTerrain();
showStartScreen();
}
void loop() {
if (!running) {
if (buttonPressed()) {
running = true;
score = 0;
speedMs = START_SPEED;
resetTerrain();
lastStep = millis();
drawGame();
}
return;
}
if (buttonPressed() && !jumping) {
jumping = true;
jumpStarted = millis();
}
if (jumping && millis() - jumpStarted >= JUMP_TIME) {
jumping = false;
}
if (millis() - lastStep >= speedMs) {
lastStep = millis();
shiftTerrain();
if (terrain[CAR_COLUMN] != 0 && !jumping) {
running = false;
showGameOver();
} else {
score++;
if (speedMs > MIN_SPEED && score % 12 == 0) speedMs -= 10;
drawGame();
}
}
}
bool buttonPressed() {
static bool previous = HIGH;
bool current = digitalRead(PIN_BUTTON);
bool pressed = previous == HIGH && current == LOW;
previous = current;
if (pressed) {
delay(25); // Simple debounce for a beginner build
return digitalRead(PIN_BUTTON) == LOW;
}
return false;
}
void resetTerrain() {
for (byte i = 0; i < LCD_COLUMNS; i++) terrain[i] = 0;
}
void shiftTerrain() {
for (byte i = 0; i < LCD_COLUMNS - 1; i++) terrain[i] = terrain[i + 1];
// Randomly add a block, while leaving enough space between obstacles.
terrain[LCD_COLUMNS - 1] = (random(0, 4) == 0) ? SPRITE_BLOCK : 0;
}
void drawGame() {
char top[LCD_COLUMNS + 1];
char bottom[LCD_COLUMNS + 1];
for (byte i = 0; i < LCD_COLUMNS; i++) {
top[i] = ' ';
bottom[i] = ' ';
}
top[LCD_COLUMNS] = ' ';
bottom[LCD_COLUMNS] = ' ';
for (byte i = 0; i < LCD_COLUMNS; i++) {
if (terrain[i] == SPRITE_BLOCK) bottom[i] = SPRITE_BLOCK;
}
if (jumping) top[CAR_COLUMN] = SPRITE_CAR_JUMP;
else bottom[CAR_COLUMN] = SPRITE_CAR;
lcd.setCursor(0, 0);
for (byte i = 0; i < LCD_COLUMNS; i++) lcd.write(top[i]);
lcd.setCursor(0, 1);
for (byte i = 0; i < LCD_COLUMNS; i++) lcd.write(bottom[i]);
lcd.setCursor(10, 0);
lcd.print("S");
lcd.print(score);
}
void showStartScreen() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("CAR RUNNER");
lcd.setCursor(0, 1);
lcd.print("Press button");
}
void showGameOver() {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("GAME OVER");
lcd.setCursor(0, 1);
lcd.print("Score: ");
lcd.print(score);
delay(1200);
showStartScreen();
}
The sketch uses lcd.init(), as does the original project. If your installed library requires a different initialization call, use that library’s documented API. The LCD address remains hardware-dependent.
Understanding the game logic
States
The game can be viewed as a small state machine:
- Waiting: the start screen is shown and the button begins a run.
- Running: terrain shifts, the score advances, and the car stays on the lower row unless jumping.
- Jumping: the car is drawn on the upper row for a timed interval.
- Collision: a ground-level obstacle at the car’s column ends the run.
- Restarting: the score and terrain are reset for the next attempt.
Terrain buffers
The terrain array represents the 16 visible LCD columns. Each update shifts every entry one position left and randomly creates a new block at the right edge. The drawing routine converts that buffer into two complete rows, which prevents stale characters from remaining on screen.
Collision detection
The collision rule is intentionally simple: if the terrain at the car’s fixed horizontal position is occupied and the car is not jumping, the game ends. This is the same underlying idea used by the original project’s terrain buffers: preserve the obstacle information, draw the car separately, and test whether the car’s position was already occupied.
Button handling
The original code uses an interrupt on digital pin 2. Interrupts are responsive and educational, but mechanical switches bounce and can generate several rapid triggers. The adapted sketch polls the button in loop() and applies a short debounce delay. For a more advanced interrupt version, shared flags should generally be declared volatile, the interrupt routine should remain short, and debounce timing should be treated as a tunable design choice.
Best Value
- 【FPV First-Person View】It provides real-time video streaming via Wi-Fi and enables remote control of the robot car's movements.
- 【Wireless transmission and control】The car with the built-in ESP32-S3 module, it supports WIFI connection. Users can receive real-time video streams through mobile devices and remotely control the movement of the vehicle and the angle of the pan-tilt unit.
- 【Five Intelligent Operation Modes】Includes Obstacle Avoidance, Infrared Remote Control, Line Following, Object Following, and FPV Video Transmission.
- 【DIY Assembly】Requires full self-assembly to cultivate hands-on skills, logical thinking, and focus; sensors have easy-to-connect interfaces, minimizing incorrect wiring and simplifying the building process for beginners.
- 【Open-Source Learning Platform】Based on an open-source ecosystem, it provides a wealth of free learning resources, project tutorials, and open-source code.
Troubleshooting
The backlight is on, but there is no text
- Run the I2C scanner and use the address it reports.
- Turn the small contrast potentiometer on the LCD backpack slowly.
- Verify that SDA goes to A4 and SCL goes to A5 on an Uno.
- Check VCC and GND.
- Test the display with a minimal LCD sketch before testing the game.
- Confirm that the backpack is soldered to the display correctly.
LiquidCrystal_I2C.h: No such file or directory
The library is missing, incorrectly installed, or duplicated by another library with the same header. Install a compatible LiquidCrystal_I2C library through the IDE Library Manager, confirm the include line exactly, and remove conflicting duplicate libraries if the IDE selects the wrong one.
The error mentions lcd.init() or the constructor
Different libraries with similar names expose different APIs. Identify the installed library and follow its documentation. Some variants use init(); others expect a begin() form or different constructor arguments.
The button does nothing
- Confirm that one electrical side of the switch connects to pin 2 and the other to GND.
- Check that the sketch contains
pinMode(PIN_BUTTON, INPUT_PULLUP). - Verify the switch orientation and its four-leg internal connections.
- Do not connect both wires to legs that are already internally joined.
The game starts randomly or the car jumps repeatedly
This usually indicates a floating input, incorrect pull-up wiring, or switch bounce. Wire the button to ground, use INPUT_PULLUP, and keep debounce handling short. If you restore the original interrupt approach, debounce the interrupt and keep the handler minimal.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Uploading fails
- Select the correct board under Tools → Board.
- Select the correct serial port.
- Use a USB data cable, not a charge-only cable.
- Disconnect anything interfering with reset or serial pins.
- For some Nano boards, try the appropriate processor or bootloader option.
The original sketch assigns an autoplay-related output to pin 1. On an Uno, pin 1 is serial TX, so using it can interfere with Serial debugging. Avoid that pin for debugging, or move the autoplay feature to another suitable pin.
The display flickers
Repeated lcd.clear(), slow redraws, excessive delay() calls, unstable power, and loose jumper wires can all contribute. The adapted game avoids clearing during normal play and writes both complete rows. A more polished version can update only changed characters and replace the remaining blocking delay with millis()-based timing.
The characters look wrong
Check that the custom characters are created before drawing begins and that the display really is a 16×2 HD44780-compatible module. If the library maps custom-character bytes differently, verify the library API and test a minimal createChar() example.
Useful modifications
- Increase difficulty: reduce
speedMsgradually, or increase the probability of a new obstacle. - Change the car’s position: modify
CAR_COLUMN, but keep collision testing and drawing aligned. - Add obstacle types: use additional custom-character slots, remembering that the LCD has limited custom memory.
- Add sound: connect a buzzer through an appropriate resistor or driver and play a tone on collision.
- Save a high score: use EEPROM, while avoiding unnecessary writes on every frame.
- Add restart control: use the same button after game over, or add a second button.
- Improve timing: remove the debounce
delay()and implement a fully non-blocking debounce withmillis(). - Upgrade the display: an OLED or TFT can show richer graphics, but requires different libraries, wiring, and rendering code.
A parallel LCD is another option. Arduino’s standard LiquidCrystal library supports HD44780 functions such as setCursor(), print(), and createChar(), but a parallel display consumes substantially more Arduino pins and needs more wiring than an I2C module.
Free tools Windows power users keep installed
One-click scans. No signup required.
Arduino LiquidCrystal library information
What you learn from this build
This small game combines several important Arduino concepts in one approachable project: I2C peripheral wiring, custom 5×8 LCD glyphs, button input, debouncing, timed animation, fixed-size buffers, collision rules, and state-machine design. Its limitations are part of the lesson: a 16×2 character LCD cannot provide the graphics or precision of a modern game display, but those constraints make the code and hardware easy to inspect.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

