Dino Run v2.1.1 Golden-Heart (Part 5): Evolution of a Legend on ESP32

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.

  • 1. Bonus System
    Bonus and Survival System. Three types of drop items have been integrated into the game: Standard Hearts (accumulation system), Star (temporary invincibility and magnet), and the Golden Heart (instant +1 HP refill).
  • 2. Boss Mode
    Boss Mode (Epic Boss Fight). Every 100 points, the game enters battle mode. A Mega-Cactus appears on the right, attacking the player with projectiles at varying heights.
  • 3. UI Optimization
    Optimized UI and Centering. The text output mechanism has been completely rewritten. Now, the game title, version, and authorship are perfectly centered horizontally.

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.

ComponentPin (Display/Button)ESP32 Pin (GPIO)Description
OLED DisplayVCC3.3VPower Supply
OLED DisplayGNDGNDCommon Ground
OLED DisplaySCLGPIO 22I2C Clock Line
OLED DisplaySDAGPIO 21I2C Data Line
ButtonPin 1GPIO 15Jump Signal
ButtonPin 2GNDGround Connection
BuzzerVia 100 Ohm resistorGPIO 25Audio Signal Output
BuzzerNegative (-)GNDCommon 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.

Dino Run v2.1.1 Golden-Heart

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!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *