Developing portable games on microcontrollers is an excellent way to explore graphics, hardware resources, and optimization. In this article, we will analyze the final build of the Dino Run v2.1.1 project by developer AndiBond. This is a full-fledged arcade game with advanced mechanics rarely found in amateur projects.
What’s New Dino Run v2.1.1 Golden-Heart?
What’s new in version v2.1.1 Golden-Heart? The latest update has transformed a simple «runner» into an arcade game with deep gameplay. The main changes focus on balance and the user interface.
Missed the previous parts?
Technical Features
Technical features of version v2.1.1. Includes: an intelligent bonus system, Power-Up mechanics, boss battles, and non-volatile memory (Preferences).
Wokwi Simulation: Testing without hardware.
Before picking up a soldering iron, I recommend testing everything in the online simulator.
Try the Game in your Browser:
Connection Scheme
Connection Scheme. Important: a 100–220 Ohm resistor is required to limit current. Use a passive buzzer for the tone() function.
| Component | Pin (Display/Button) | ESP32 Pin (GPIO) | Description |
|---|---|---|---|
| OLED Display | VCC | 3.3V | Power Supply |
| OLED Display | GND | GND | Common Ground |
| OLED Display | SCL | GPIO 22 | I2C Clock Line |
| OLED Display | SDA | GPIO 21 | I2C Data Line |
| Button | Pin 1 | GPIO 15 | Jump Signal |
| Button | Pin 2 | GND | Ground Connection |
| Buzzer | Via 100 Ohm resistor | GPIO 25 | Audio Signal Output |
| Buzzer | Negative (-) | GND | Common Ground |
Project Code (v2.1.1)
Full source code for the project (v2.1.1)
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Preferences.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define BUZZER_PIN 25
#define BUTTON_PIN 15
// PROJECT DATA
const String version = "v2.1.1 Golden-Heart";
const String author = "AndiBond";
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
Preferences preferences;
// --- BITMAPS ---
const unsigned char dino_frame1[] PROGMEM = { 0x00, 0x00, 0x07, 0xf0, 0x07, 0xf8, 0x07, 0xfc, 0x07, 0xfc, 0x07, 0xf0, 0x07, 0xf0, 0x47, 0xe0, 0x47, 0xe0, 0xc7, 0xe0, 0xff, 0xe0, 0xff, 0xe0, 0x7f, 0xe0, 0x3f, 0xe0, 0x1c, 0x20, 0x18, 0x00 };
const unsigned char dino_frame2[] PROGMEM = { 0x00, 0x00, 0x07, 0xf0, 0x07, 0xf8, 0x07, 0xfc, 0x07, 0xfc, 0x07, 0xf0, 0x07, 0xf0, 0x47, 0xe0, 0x47, 0xe0, 0xc7, 0xe0, 0xff, 0xe0, 0xff, 0xe0, 0x7f, 0xe0, 0x3f, 0xe0, 0x08, 0x70, 0x00, 0x30 };
const unsigned char cactus_mini_bmp[] PROGMEM = { 0x18, 0x18, 0x18, 0x98, 0xdb, 0xff, 0xdb, 0x18, 0x18, 0x18 };
const unsigned char heart_bonus_bmp[] PROGMEM = { 0x66, 0xff, 0xff, 0xff, 0x7e, 0x3c, 0x18 };
const unsigned char star_bmp[] PROGMEM = { 0x18, 0x3c, 0x7e, 0xff, 0xff, 0x7e, 0x3c, 0x18 };
const unsigned char ptero_bmp[] PROGMEM = { 0x20, 0x00, 0x70, 0x00, 0xf8, 0x00, 0xff, 0x80, 0xff, 0xe0, 0x1f, 0x00, 0x0e, 0x00, 0x04, 0x00 };
// Parameters
int dinoY = 39, velocity = 0, gravity = 2;
bool isJumping = false, legToggle = false, starActive = false, isBossMode = false;
int score = 0, highScore = 0, lives = 3, heartsCollected = 0, level = 1, gameSpeed = 7;
unsigned long invincibilityUntil = 0, starEndTime = 0;
int obstacleX = 128, obstacleY = 45, obsType = 0, bonusX = -20, bonusY = 30, bonusType = 0;
int bossTimer = 0, bulletX = -20, bulletY = 45;
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
randomSeed(analogRead(0));
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { for(;;); }
preferences.begin("dino-game", false);
highScore = preferences.getInt("highscore", 0);
showStartScreen();
}
void loop() {
if (digitalRead(BUTTON_PIN) == LOW && !isJumping) { velocity = -12; isJumping = true; tone(BUZZER_PIN, 600, 80); }
if (isJumping) { dinoY += velocity; velocity += gravity; if (dinoY >= 39) { dinoY = 39; isJumping = false; } }
else { if(millis() % 200 < 100) legToggle = !legToggle; }
if (!isBossMode) {
level = (score / 10) + 1;
gameSpeed = constrain(6 + level, 7, 14);
obstacleX -= gameSpeed;
if (starActive && bonusX > 20 && bonusX < 110) {
if (bonusY > dinoY + 4) bonusY--; else if (bonusY < dinoY + 4) bonusY++;
bonusX -= (gameSpeed + 2);
} else { bonusX -= gameSpeed; }
if (score > 0 && score % 100 == 0) { isBossMode = true; bossTimer = 500; bulletX = 120; tone(BUZZER_PIN, 100, 500); }
} else {
bossTimer--; bulletX -= (gameSpeed + 2);
if (bulletX < -10) { bulletX = 110; bulletY = random(0, 10) > 5 ? 45 : 30; }
if (bulletX < 32 && bulletX + 8 > 20 && !starActive && millis() > invincibilityUntil) {
if (dinoY < bulletY + 10 && dinoY + 16 > bulletY) { lives--; tone(BUZZER_PIN, 150, 300); invincibilityUntil = millis() + 1000; bulletX = -10; if (lives <= 0) handleGameOver(); }
}
if (bossTimer <= 0) { isBossMode = false; score += 10; tone(BUZZER_PIN, 1000, 500); obstacleX = 140; }
}
if (starActive && millis() > starEndTime) starActive = false;
if (!isBossMode && obstacleX < -20) {
score++; obstacleX = 128 + random(0, 40);
obsType = (level >= 2 && random(0, 10) > 7) ? 1 : 0;
obstacleY = (obsType == 1) ? random(25, 40) : 45;
if (score % 5 == 0 && bonusX < -10) {
bonusX = 128; bonusY = random(20, 40);
int r = random(0, 100);
bonusType = (r > 90) ? 2 : (r > 70 ? 1 : 0);
}
}
if (bonusX > 15 && bonusX < 35 && dinoY < bonusY + 8 && dinoY + 16 > bonusY) {
if (bonusType == 0) { heartsCollected++; if (heartsCollected >= 5) { heartsCollected = 0; if (lives < 3) lives++; } }
else if (bonusType == 1) { starActive = true; starEndTime = millis() + 7000; }
else if (bonusType == 2) { if (lives < 3) lives++; }
tone(BUZZER_PIN, 2000, 100); bonusX = -30;
}
if (!isBossMode && millis() > invincibilityUntil && !starActive && obstacleX < 32 && obstacleX + 8 > 20) {
if (dinoY < obstacleY + 10 && dinoY + 16 > obstacleY) { lives--; tone(BUZZER_PIN, 150, 300); invincibilityUntil = millis() + 1000; obstacleX = 140; if (lives <= 0) handleGameOver(); }
}
drawGame();
delay(35);
}
void printCenter(String text, int y) {
int16_t x1, y1; uint16_t w, h;
display.getTextBounds(text, 0, 0, &x1, &y1, &w, &h);
display.setCursor((128 - w) / 2, y);
display.print(text);
}
void drawGame() {
display.clearDisplay();
display.setTextColor(WHITE);
display.setCursor(0, 0); display.print("HP:"); display.print(lives);
display.setCursor(32, 0); display.print("H:"); display.print(heartsCollected);
if (isBossMode) { display.setCursor(55, 0); display.print("BOSS:"); display.print(bossTimer/10); }
else if (starActive) { display.setCursor(58, 0); display.print("*PWR*"); }
else { display.setCursor(62, 0); display.print("S:"); display.print(score); }
display.setCursor(98, 0); display.print("HI:"); display.print(highScore);
display.drawLine(0, 10, 128, 10, WHITE); display.drawLine(0, 55, 128, 55, WHITE);
if (isBossMode) {
display.drawBitmap(110, 35, cactus_mini_bmp, 8, 10, WHITE);
display.drawBitmap(bulletX, bulletY, cactus_mini_bmp, 8, 10, WHITE);
} else if (bonusX > -10) {
if (bonusType == 0) display.drawBitmap(bonusX, bonusY, heart_bonus_bmp, 8, 7, WHITE);
else if (bonusType == 1) display.drawBitmap(bonusX, bonusY, star_bmp, 8, 8, WHITE);
else { if ((millis() / 100) % 2 == 0) display.drawBitmap(bonusX, bonusY, heart_bonus_bmp, 8, 7, WHITE); display.drawRect(bonusX-1, bonusY-1, 10, 9, WHITE); }
}
if (millis() > invincibilityUntil || (millis() / 100) % 2 == 0) {
display.drawBitmap(20, dinoY, (isJumping ? dino_frame1 : (legToggle ? dino_frame1 : dino_frame2)), 16, 16, WHITE);
if (starActive) display.drawCircle(27, dinoY + 7, 11, WHITE);
}
if (!isBossMode) {
if (obsType == 0) display.drawBitmap(obstacleX, obstacleY, cactus_mini_bmp, 8, 10, WHITE);
else display.drawBitmap(obstacleX, obstacleY, ptero_bmp, 12, 8, WHITE);
}
display.display();
}
void handleGameOver() {
if (score > highScore) { highScore = score; preferences.putInt("highscore", score); }
display.clearDisplay(); display.setTextSize(1);
printCenter("GAME OVER", 10); printCenter(version, 22); printCenter(author, 34);
printCenter("Score: " + String(score) + " HI: " + String(highScore), 46);
display.display(); delay(3000); lives = 3; score = 0; heartsCollected = 0; isBossMode = false; showStartScreen();
}
void showStartScreen() {
display.clearDisplay(); display.setTextSize(1);
printCenter("DINO RUN", 10); printCenter(version, 25); printCenter(author, 40);
printCenter("Record: " + String(highScore), 55); display.display();
while(digitalRead(BUTTON_PIN) == HIGH) delay(10);
tone(BUZZER_PIN, 800, 100); delay(200);
}Developer’s Insights
Data persistence (NVS) via Preferences.h. Interface mathematics using the printCenter function. Boss Fight logic implemented through the isBossMode state.

AndiBond.com
Conclusion
Conclusion. Version v2.1.1 Golden-Heart is the definitive release. Support the AndiBond project. Every donation is fuel for new articles.
Support the AndiBond Project
Creating high-quality guides, finding working solutions, and debugging code takes a lot of time. All my projects remain open-source and free so that everyone can enter the world of electronics with the lowest possible barrier to entry.
If this tutorial saved you time or helped you launch your first game on ESP32, you can support the blog’s development. Your support helps me buy new sensors, displays, and controllers for future reviews.
Every donation is fuel for new articles and videos. Thank you for being with me!

