Rise and Rubble: the DIY Video Game You Can't Put Down

by Vanilars Kitchen in Circuits > Arduino

50 Views, 0 Favorites, 0 Comments

Rise and Rubble: the DIY Video Game You Can't Put Down

20260331_164636.jpg

I built this game using countless hours of research and an absurd amount of AI. When I say absurd, I really mean it! The game, Rise and Rubble, is simple to play yet surprisingly challenging. From personal experience, I know it’ll hook you the moment you lay eyes on it—I once played for an hour straight without even noticing the time slipping by.

Supplies

20260331_170033.jpg
20260331_165945.jpg
20260331_165918.jpg
20260331_165845.jpg
20260331_165751.jpg
  1. 1 220 ohm resistor
  2. 1 16×2 LCD
  3. 1 arduino uno r3
  4. LOTS of wires
  5. 5 pushbuttons
  6. 1 piezo buzzer
  7. 1 breadboard

Wiring Up the LCD

20260331_164603.jpg

I first prototyped this circuit in Tinkercad to make sure everything worked perfectly. Instead of walking you through every single wire—which is about as exciting as watching paint dry—I’ve set up this interactive example so you can jump straight into wiring your own project. Quick note: the circuit simulator is running in milliseconds which can be a real pain if you want to play the game on it.

Coding

#include <LiquidCrystal.h>
#include <EEPROM.h>

LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

const int btnLeft = 8,
const int btnRight = 9,
const int btnSpeed = 10,
const int btnReset = 6,
const int btnSaveToggle = 1,
const int buzzer = 7;

// Custom Sprites
byte arrowChar[] = { B00000, B10000, B11000, B11100, B11110, B11100, B11000, B10000 };
byte shield1Player[] = { B00001, B10001, B11001, B11101, B11111, B11101, B11001, B10001 };
byte rockChar[] = { B01110, B11111, B11111, B11111, B11111, B11111, B11111, B01110 };
byte balloonChar[] = { B01110, B10001, B10001, B01110, B00100, B00100, B00010, B00100 };
byte shieldItem[] = { B00100, B00100, B00100, B00100, B00100, B00100, B00100, B00100 };
byte shield2Player[] = { B00011, B10011, B11011, B11111, B11111, B11111, B11011, B10011 };

enum Type { ROCK, BALLOON, SHIELD_UP };
struct Entity { int row, col, oldRow, oldCol; Type type; bool active; };
const int MAX_ENTITIES = 4;
Entity entities[MAX_ENTITIES];

int score = 0, highScore = 0, playerPos = 0, oldPlayerPos = 0, gameSpeed = 220, shieldCount = 0, speedClicks = 0;
bool gameOver = true, frenzyMode = false, frenzyUsed = false, allowSave = true, showBest = false, isMuted = false;
unsigned long lastUpdate = 0, frenzyStartTime = 0, lastEndScreenSwap = 0, lastNoteTime = 0, musicMuteUntil = 0;

// I didn't want this part to be repeating over and over so I made it a four-part symphony.
// The notes were made with AI because I'm not very musically oriented.
int longSymphony[] = {
294, 330, 349, 392, 440, 392, 349, 330,
294, 440, 523, 587, 523, 440, 392, 349,
440, 440, 392, 349, 330, 294, 262, 294,
587, 523, 440, 392, 440, 349, 330, 294
};
int currentNote = 0;

void setup() {
pinMode(btnLeft, INPUT_PULLUP); pinMode(btnRight, INPUT_PULLUP);
pinMode(btnSpeed, INPUT_PULLUP); pinMode(btnReset, INPUT_PULLUP);
pinMode(btnSaveToggle, INPUT_PULLUP); pinMode(buzzer, OUTPUT);
highScore = EEPROM.read(0);
lcd.begin(16, 2);
lcd.createChar(0, arrowChar); lcd.createChar(1, shield1Player);
lcd.createChar(2, rockChar); lcd.createChar(3, balloonChar);
lcd.createChar(4, shieldItem); lcd.createChar(5, shield2Player);
showIntro();
}

void safeTone(int pin, int freq, int dur) {
if (!isMuted) tone(pin, freq, dur);
}

void showIntro() {
lcd.clear(); lcd.setCursor(1, 0); lcd.print("RISE & RUBBLE");
for (int i = 0; i < 16; i++) {
safeTone(buzzer, longSymphony[i % 32], 120);
lcd.setCursor(0, 1); for(int c=0; c<16; c++) lcd.print(" ");
lcd.setCursor(0, 1); lcd.write(byte(0));
int rockPos = 15 - i;
if (rockPos >= 0) { lcd.setCursor(rockPos, 1); lcd.write(byte(2)); }
delay(180);
}
noTone(buzzer);
lcd.clear();
while (digitalRead(btnReset) == HIGH) {
checkSaveAndMuteButtons();
if ((millis() / 500) % 2 == 0) { lcd.setCursor(2, 0); lcd.print("PRESS START"); }
else { lcd.setCursor(2, 0); lcd.print(" "); }
delay(10);
}
safeTone(buzzer, 440, 400);
resetGame();
}

void checkSaveAndMuteButtons() {
if (digitalRead(btnSaveToggle) == LOW) {
unsigned long pressStart = millis();
while (digitalRead(btnSaveToggle) == LOW) {
if (millis() - pressStart > 1000) {
isMuted = !isMuted;
lcd.clear(); lcd.setCursor(5, 0); lcd.print(isMuted ? "SOUND OFF" : "SOUND ON");
delay(800); lcd.clear(); return;
}
}
allowSave = !allowSave;
lcd.clear(); lcd.setCursor(3, 0); lcd.print(allowSave ? "SAVE ON" : "SAVE OFF");
delay(800); lcd.clear();
}
}

void playMusic() {
if (millis() < musicMuteUntil || isMuted) return;
int noteDuration = 140; // Fast Irish Jig tempo
if (millis() - lastNoteTime > noteDuration + 20) {
lastNoteTime = millis();
safeTone(buzzer, longSymphony[currentNote], noteDuration);
currentNote = (currentNote + 1) % 32;
}
}

void loop() {
if (digitalRead(btnReset) == LOW) { safeTone(buzzer, 294, 150); delay(250); resetGame(); }
checkSaveAndMuteButtons();
if (gameOver) {
if (millis() - lastEndScreenSwap > 2000) {
lastEndScreenSwap = millis(); showBest = !showBest; lcd.clear();
if (showBest) { lcd.setCursor(3, 0); lcd.print("BEST SCORE"); lcd.setCursor(7, 1); lcd.print(highScore); }
else { lcd.setCursor(3, 0); lcd.print("YOUR SCORE"); lcd.setCursor(7, 1); lcd.print(score); }
}
return;
}
playMusic();
if (digitalRead(btnSpeed) == LOW) {
musicMuteUntil = millis() + 500; safeTone(buzzer, 880, 100);
speedClicks = (speedClicks + 1) % 5; gameSpeed = 220 - (speedClicks * 35);
lcd.clear(); lcd.setCursor(4, 0); lcd.print("LEVEL "); lcd.print(speedClicks + 1);
delay(400); lcd.clear();
}
if (digitalRead(btnLeft) == LOW && playerPos != 0) { musicMuteUntil = millis() + 80; safeTone(buzzer, 1800, 20); playerPos = 0; }
if (digitalRead(btnRight) == LOW && playerPos != 1) { musicMuteUntil = millis() + 80; safeTone(buzzer, 1800, 20); playerPos = 1; }
if (millis() - lastUpdate > gameSpeed) { lastUpdate = millis(); updateGame(); drawGame(); }
}

void updateGame() {
if (!frenzyUsed && score >= 100) {
frenzyMode = true; frenzyUsed = true; frenzyStartTime = millis(); clearEntities();
musicMuteUntil = millis() + 1000;
for(int i=0; i<4; i++) { safeTone(buzzer, 880 + (i*200), 100); delay(150); }
}
if (frenzyMode && (millis() - frenzyStartTime > 10000)) { frenzyMode = false; clearEntities(); }
for (int i = 0; i < MAX_ENTITIES; i++) {
if (entities[i].active) {
entities[i].oldRow = entities[i].row; entities[i].oldCol = entities[i].col; entities[i].row--;
if (entities[i].row == 0 && entities[i].col == playerPos) handleCollision(i);
else if (entities[i].row < 0) { entities[i].active = false; if (entities[i].type == ROCK) score++; }
} else if (random(0, 100) < (frenzyMode ? 60 : 15)) spawnEntity(i);
}
}

void drawGame() {
if (playerPos != oldPlayerPos) { lcd.setCursor(0, oldPlayerPos); lcd.print(" "); oldPlayerPos = playerPos; }
for (int i = 0; i < MAX_ENTITIES; i++) {
if (entities[i].oldRow >= 0) { lcd.setCursor(entities[i].oldRow, entities[i].oldCol); lcd.print(" "); entities[i].oldRow = -1; }
}
lcd.setCursor(0, playerPos); lcd.write(byte(shieldCount == 0 ? 0 : (shieldCount == 1 ? 1 : 5)));
for (int i = 0; i < MAX_ENTITIES; i++) {
if (entities[i].active) { lcd.setCursor(entities[i].row, entities[i].col); lcd.write(entities[i].type == ROCK ? 2 : (entities[i].type == BALLOON ? 3 : 4)); }
}
lcd.setCursor(14, 0); if (score < 100) lcd.print(" "); if (score < 10) lcd.print(" "); lcd.print(score);
}

void handleCollision(int i) {
musicMuteUntil = millis() + 500;
if (entities[i].type == ROCK) {
if (shieldCount > 0) { shieldCount--; score = max(0, score - 5); safeTone(buzzer, 200, 200); }
else {
noTone(buzzer);
for(int f=400; f>100; f-=20) { safeTone(buzzer, f, 80); delay(90); }
gameOver = true;
if (score > highScore) { highScore = score; if (allowSave) EEPROM.update(0, highScore); }
lcd.setCursor(0, playerPos); lcd.write(byte(2));
delay(1000);
lcd.clear();
}
} else {
safeTone(buzzer, (entities[i].type == BALLOON ? 2200 : 2800), 80);
if (entities[i].type == BALLOON) score += 5; else shieldCount++;
}
entities[i].active = false;
}

void spawnEntity(int i) {
for (int j = 0; j < MAX_ENTITIES; j++) { if (entities[j].active && entities[j].row > 13) return; }
entities[i].active = true; entities[i].row = 15; entities[i].col = random(0, 2); entities[i].oldRow = -1;
entities[i].type = frenzyMode ? BALLOON : (random(100) < 80 ? ROCK : (random(100) < 70 ? BALLOON : SHIELD_UP));
}

void clearEntities() { for (int i = 0; i < MAX_ENTITIES; i++) { entities[i].active = false; entities[i].oldRow = -1; } lcd.clear(); }

void resetGame() {
score = 0; speedClicks = 0; gameSpeed = 220; shieldCount = 0;
gameOver = false; frenzyMode = false; frenzyUsed = false; currentNote = 0; musicMuteUntil = 0;
clearEntities();
}

Play!

This is by far the easiest step. The rules are simple: avoid rocks while popping balloons (+5 points) and collecting shields to defend yourself with. The goal: get as many points as possible while staying alive. Happy gaming!

Your Turn: Questions & Customizations

The code and wiring is completely open source and I can't wait to hear your feedback, questions and modifications!