Разработка встраиваемых игр на платформе ESP32 с использованием OLED-дисплеев — это отличный способ изучить основы компьютерной графики, физики и оптимизации кода. В этой статье мы разберем ключевые изменения, внесенные в последнюю версию игры Dino Run (v1.9.5)
Проект представляет собой классическую аркаду, адаптированную под микроконтроллер ESP32 и OLED-дисплей. В этой версии основной упор был сделан на динамику препятствий и улучшение визуального стиля.
Пропустили предыдущие части?
Система здоровья и «милосердия»
В ранних версиях игра была беспощадной: любое касание кактуса означало мгновенный проигрыш.
Что изменилось:
Редизайн и оптимизация спрайтов
В версии v1.9.5 мы отошли от крупных объектов в пользу «пиксельной точности»:
Вертикальный геймплей: Летающие препятствия в ESP32 Dino Run v1.9.5
Самое значимое изменение в механике — отказ от статичной линии препятствий.
Разная высота: Начиная со второго уровня, кактусы перестают быть только наземными. Они начинают появляться на трех разных уровнях высоты:
Экономика игры: Система «Сердец»
Вместо обычных очков мы внедрили бонусную систему сбора предметов:
Визуальные эффекты и UI
Для придания игре «сочности» (game juice) были добавлены следующие элементы в ESP32 Dino Run v1.9.5:
Симуляция в Wokwi: Тестируем без железа
Перед тем как брать в руки паяльник, я рекомендую проверить всё в онлайн-симуляторе. Это сэкономит время на отладку.
Попробовать игру в браузере:
Технический стек:
Для придания игре «сочности» (game juice) были добавлены следующие элементы:
Краткое напоминание распиновки. Для звуковых эффектов обязательно используйте пассивный зуммер.
Важно: резистор на 100–220 Ом между GPIO 25 и плюсом зуммера нужен, чтобы ограничить ток и не перегружать порт ESP32
Тип зуммера: код использует функцию tone(), поэтому лучше всего подойдет пассивный зуммер (он позволяет менять тональность). Активный зуммер будет просто пищать на одной ноте.
| Компонент | Пин дисплея / кнопки | Пин ESP32 (GPIO) | Описание |
|---|---|---|---|
| OLED Display | VCC | 3.3V | Питание дисплея |
| OLED Display | GND | GND | Общий минус |
| OLED Display | SCL | GPIO 22 | Тактовая линия I2C |
| OLED Display | SDA | GPIO 21 | Линия данных I2C |
| Кнопка | Pin 1 | GPIO 15 | Сигнал прыжка |
| Кнопка | Pin 2 | GND | Замыкание на землю |
| Buzzer | Через резистор 100 Ом | GPIO 25 | Выход звукового сигнала |
| Buzzer | Negative (-) | GND | Общая земля |
Создание и оптимизация Bitmap-графики
Одной из самых сложных задач было перерисовать графику, чтобы она выглядела как в оригинале, но помещалась в микроскопические размеры.
Как создавались спрайты:
Разбор ключевых частей кода
Определение графики (PROGMEM)
Здесь хранятся «чертежи» наших объектов. Например, новый уменьшенный кактус теперь занимает всего 10 байт памяти:
const unsigned char cactus_mini_bmp[] PROGMEM = {
0x18, 0x18, 0x18, 0x98, 0xdb, 0xff, 0xdb, 0x18, 0x18, 0x18
};Логика переменной высоты препятствий
В этой версии мы изменили логику loop(). Теперь кактус получает случайную координату Y при каждом появлении, что создает эффект полета:
if (level >= 2) {
int r = random(0, 10);
if (r < 3) obstacleY = 25; // Высокий полет (можно пробежать под ним)
else if (r < 5) obstacleY = 35; // Средний полет
else obstacleY = 45; // Наземный кактус
}Система коллизий (Collision Detection)
Мы используем метод AABB (Axis-Aligned Bounding Box). Программа проверяет, пересекаются ли прямоугольники Динозавра и препятствия, учитывая их текущие координаты $X$ и $Y$.
Код программы ESP32 Dino Run v1.9.5
Полный исходный код проекта (v1.9.5)
#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
// Данные проекта
const String version = "v1.9.5";
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 };
// Игровые переменные
int dinoY = 39, velocity = 0, gravity = 2;
bool isJumping = false, legToggle = false;
int score = 0, highScore = 0, lives = 3;
unsigned long invincibilityUntil = 0;
int obstacleX = 128, obstacleY = 45;
int bonusX = -20, bonusY = 30, heartsCollected = 0;
int level = 1, gameSpeed = 7;
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(BUZZER_PIN, OUTPUT);
randomSeed(analogRead(0));
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
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 { legToggle = !legToggle; }
level = (score / 10) + 1;
gameSpeed = 6 + level;
obstacleX -= gameSpeed;
bonusX -= gameSpeed;
if (obstacleX < -20) {
score++;
obstacleX = 128 + random(0, 40);
if (level >= 2) {
int r = random(0, 10);
if (r < 3) obstacleY = 25; else if (r < 5) obstacleY = 35; else obstacleY = 45;
} else { obstacleY = 45; }
if (score % 5 == 0 && bonusX < -10) { bonusX = obstacleX + 60; bonusY = random(20, 40); }
}
if (bonusX > 15 && bonusX < 35 && dinoY < bonusY + 7 && dinoY + 16 > bonusY) {
bonusX = -20; heartsCollected++; tone(BUZZER_PIN, 1500, 50);
if (heartsCollected >= 5) { heartsCollected = 0; if (lives < 3) { lives++; tone(BUZZER_PIN, 2000, 150); } }
}
if (millis() > invincibilityUntil && 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 drawGame() {
display.clearDisplay();
display.setTextColor(WHITE);
display.setCursor(0, 0); display.print("HP:"); display.print(lives);
display.setCursor(35, 0); display.print("H:"); display.print(heartsCollected);
display.setCursor(65, 0); display.print("S:"); display.print(score);
display.setCursor(100,0); display.print("HI:"); display.print(highScore);
display.drawLine(0, 55, 128, 55, WHITE);
if (bonusX > -10) display.drawBitmap(bonusX, bonusY, heart_bonus_bmp, 8, 7, WHITE);
if (millis() > invincibilityUntil || (millis() / 100) % 2 == 0) {
display.drawBitmap(20, dinoY, (isJumping ? dino_frame1 : (legToggle ? dino_frame1 : dino_frame2)), 16, 16, WHITE);
}
display.drawBitmap(obstacleX, obstacleY, cactus_mini_bmp, 8, 10, WHITE);
display.display();
}
void handleGameOver() {
for(int i = 0; i < 10; i++) { display.setRotation(random(0, 4)); display.display(); delay(20); }
display.setRotation(0);
if (score > highScore) { highScore = score; preferences.putInt("highscore", score); }
display.clearDisplay();
display.setTextSize(2); display.setCursor(10, 10); display.print("GAME OVER");
display.setTextSize(1); display.setCursor(25, 35); display.print("Score: "); display.print(score);
display.setCursor(25, 45); display.print("Version: "); display.print(version);
display.setCursor(25, 55); display.print("by "); display.print(author);
display.display();
delay(3000);
lives = 3; score = 0; heartsCollected = 0; obstacleX = 128; dinoY = 39;
showStartScreen();
}
void showStartScreen() {
display.clearDisplay();
display.setCursor(15, 10); display.print("DINO RUN "); display.print(version);
display.setCursor(15, 25); display.print("Author: "); display.print(author);
display.setCursor(15, 40); display.print("HI-SCORE: "); display.print(highScore);
display.setCursor(15, 55); display.print("Press to Start");
display.display();
while(digitalRead(BUTTON_PIN) == HIGH) delay(10);
tone(BUZZER_PIN, 800, 100); delay(200);
}Итог
ESP32 Dino Run v1.9.5 превратила простую демонстрацию кода в сбалансированную игру с прогрессирующей сложностью. Уменьшение размера кактусов и введение разной высоты полета сделало геймплей более динамичным и глубоким.

AndiBond.com
Поддержите проект AndiBond
Создание качественных гайдов, поиск рабочих решений и отладка кода занимают много времени. Все мои проекты остаются открытыми и бесплатными, чтобы каждый мог войти в мир электроники с минимальным порогом входа.
Если этот туториал сэкономил ваше время или помог запустить вашу первую игру на ESP32, вы можете поддержать развитие блога. Ваша поддержка помогает мне покупать новые датчики, дисплеи и контроллеры для будущих обзоров.
Каждый донат — это топливо для новых статей и видео. Спасибо, что вы со мной!»

