ESP32 Dino Run (Часть 4): От простого прыжка до полноценной аркады

Разработка встраиваемых игр на платформе ESP32 с использованием OLED-дисплеев — это отличный способ изучить основы компьютерной графики, физики и оптимизации кода. В этой статье мы разберем ключевые изменения, внесенные в последнюю версию игры Dino Run (v1.9.5)

Проект представляет собой классическую аркаду, адаптированную под микроконтроллер ESP32 и OLED-дисплей. В этой версии основной упор был сделан на динамику препятствий и улучшение визуального стиля.

Пропустили предыдущие части?

Система здоровья и «милосердия»

В ранних версиях игра была беспощадной: любое касание кактуса означало мгновенный проигрыш.

Что изменилось:
  • Система здоровья и выживания (Health System)
    Три жизни вместо одной: Раньше любая ошибка приводила к Game Over. Теперь у игрока есть 3 единицы здоровья (HP), что делает геймплей более глубоким и менее раздражающим.
    Механика «I-Frames» (Invincibility Frames): После столкновения игрок получает 1000 мс неуязвимости. Это предотвращает «мгновенную смерть», когда одно препятствие отнимает все жизни сразу.
    Визуальный отклик: Во время неуязвимости спрайт Дино мигает, сигнализируя игроку о временной защите.
  • Сбор бонусов и «Лечение» (Heart Mechanics)
    Бонусные сердечки: На игровое поле добавлен новый тип объектов — маленькие сердечки (заменили монетки).
    Механика восстановления: Собрав 5 сердечек, игрок восстанавливает 1 HP (но не более 3-х). Это создает дополнительный стимул рисковать ради сбора бонусов.
    Звуковая индикация: Добавлены уникальные звуковые тона (Buzzer) для сбора сердечка и для момента восстановления жизни.
  • Динамические препятствия и высота (Multi-Height Obstacles)
    Переменная высота Y: Кактусы больше не привязаны только к земле. Со второго уровня они начинают появляться на разных высотах:
    Ground: Обычный кактус на земле.
    Low Fly: Низко летящий кактус (нужен точный прыжок).
    High Fly: Высоко летящий кактус (можно пробежать под ним, не прыгая).
  • Полный редизайн графики (Pixel Art Redesign)
    Стиль Chrome Dino: Спрайты Динозавра перерисованы с нуля в 16-битном стиле оригинальной игры Google. Улучшена детализация морды, хвоста и анимация лап.
    Мини-кактусы (8×10): Размер препятствий был уменьшен. Это визуально освободило экран и сделало коллизии (столкновения) более честными и предсказуемыми.
    Обновленный Птеродактиль: Птица заменена на более детализированный спрайт с размахом крыльев 16×12.
  • Улучшения движка и UI Тряска экрана (Screen Shake): Добавлен эффект визуального «шока» при потере последней жизни — экран хаотично вращается перед выводом Game Over.
    Новая панель статуса: Интерфейс переработан для отображения всех параметров одновременно: HP, количество собранных сердечек (H), текущие очки (S) и рекорд (HI).
    Очистка кадра: Убраны декоративные облака, чтобы повысить частоту кадров (FPS) и сосредоточить внимание игрока на препятствиях.
  • Работа с памятью
    Энергонезависимый High Score:
  • Используется библиотека Preferences.h для хранения рекорда во Flash-памяти ESP32. Рекорд не сбрасывается при нажатии кнопки Reset.
    Оптимизация Bitmap: Все изображения переведены в массивы PROGMEM, что позволяет запускать игру даже на контроллерах с очень малым объемом оперативной памяти.

Редизайн и оптимизация спрайтов

В версии v1.9.5 мы отошли от крупных объектов в пользу «пиксельной точности»:

  • Мини-кактусы: Размер препятствия уменьшен до 8×10 пикселей. Это сделало игру визуально чище и дало больше пространства для маневра на маленьком экране 128×64.
  • Chrome Style: Спрайты Динозавра были перерисованы с нуля. Теперь они максимально приближены к оригинальному стилю Google Chrome — с характерной мордой и детализированными лапками.

Вертикальный геймплей: Летающие препятствия в ESP32 Dino Run v1.9.5

Самое значимое изменение в механике — отказ от статичной линии препятствий.

Разная высота: Начиная со второго уровня, кактусы перестают быть только наземными. Они начинают появляться на трех разных уровнях высоты:

  • Земля (Y=45): Классический прыжок.
  • Низкий полет (Y=35): Требует точного расчета времени прыжка.
  • Высокий полет (Y=25): Кактус пролетает над головой Дино, если тот стоит на земле, но становится опасным в момент прыжка.

Экономика игры: Система «Сердец»

Вместо обычных очков мы внедрили бонусную систему сбора предметов:

  • Бонус-сердечки: Каждые 5 набранных очков на поле появляется сердечко.
  • Механика лечения: Сбор 5 таких бонусов восстанавливает одну потерянную жизнь (до максимума в 3 HP). Это заставляет игрока рисковать и прыгать за бонусом даже в опасных ситуациях.

Визуальные эффекты и UI

Для придания игре «сочности» (game juice) были добавлены следующие элементы в ESP32 Dino Run v1.9.5:

  • Эффект тряски (Screen Shake): При проигрыше экран хаотично меняет ориентацию и мигает, создавая физическое ощущение удара.
  • Информативный статус-бар: В верхней части экрана теперь отображаются сразу четыре параметра: жизни, собранные бонусы, текущий счет и лучший результат (High Score).
  • Сохранение прогресса: Благодаря библиотеке Preferences.h, рекорд игрока сохраняется в энергонезависимую память ESP32 и не сбрасывается даже после выключения питания.

Симуляция в Wokwi: Тестируем без железа

Перед тем как брать в руки паяльник, я рекомендую проверить всё в онлайн-симуляторе. Это сэкономит время на отладку.

Попробовать игру в браузере:

Технический стек:

Для придания игре «сочности» (game juice) были добавлены следующие элементы:

  • Контроллер: ESP32 (поддержка Wi-Fi не используется для экономии ресурсов).
  • Дисплей: SSD1306 (I2C, 128×64).
  • Звук: Пьезо-пищалка (Buzzer) для обратной связи.
  • Память: Использование макроса PROGMEM для хранения графики в Flash-памяти, освобождая оперативную память.

Краткое напоминание распиновки. Для звуковых эффектов обязательно используйте пассивный зуммер.

Важно: резистор на 100–220 Ом между GPIO 25 и плюсом зуммера нужен, чтобы ограничить ток и не перегружать порт ESP32

Тип зуммера: код использует функцию tone(), поэтому лучше всего подойдет пассивный зуммер (он позволяет менять тональность). Активный зуммер будет просто пищать на одной ноте.

КомпонентПин дисплея / кнопкиПин ESP32 (GPIO)Описание
OLED DisplayVCC3.3VПитание дисплея
OLED DisplayGNDGNDОбщий минус
OLED DisplaySCLGPIO 22Тактовая линия I2C
OLED DisplaySDAGPIO 21Линия данных I2C
КнопкаPin 1GPIO 15Сигнал прыжка
КнопкаPin 2GNDЗамыкание на землю
BuzzerЧерез резистор 100 ОмGPIO 25Выход звукового сигнала
BuzzerNegative (-)GNDОбщая земля

Создание и оптимизация Bitmap-графики

Одной из самых сложных задач было перерисовать графику, чтобы она выглядела как в оригинале, но помещалась в микроскопические размеры.

Как создавались спрайты:

  • Проектирование: Спрайты рисовались в пиксельных редакторах (например, Piskel или Photoshop) на сетках 16×16 (для Дино) и 8×10 (для кактуса).
  • Конвертация: Полученные PNG изображения переводились в массив байтов (HEX-код) через онлайн-конвертеры (например, image2cpp).
  • Хранение: Чтобы сэкономить оперативную память (RAM), все массивы помечены модификатором PROGMEM. Это заставляет микроконтроллер читать графику напрямую из Flash-памяти.

Разбор ключевых частей кода

Определение графики (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 превратила простую демонстрацию кода в сбалансированную игру с прогрессирующей сложностью. Уменьшение размера кактусов и введение разной высоты полета сделало геймплей более динамичным и глубоким.

ESP32 Dino Run v1.9.5

AndiBond.com


Поддержите проект AndiBond

Создание качественных гайдов, поиск рабочих решений и отладка кода занимают много времени. Все мои проекты остаются открытыми и бесплатными, чтобы каждый мог войти в мир электроники с минимальным порогом входа.

Если этот туториал сэкономил ваше время или помог запустить вашу первую игру на ESP32, вы можете поддержать развитие блога. Ваша поддержка помогает мне покупать новые датчики, дисплеи и контроллеры для будущих обзоров.

Каждый донат — это топливо для новых статей и видео. Спасибо, что вы со мной!»

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

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