ESP32 Dino Run (Часть 3): Случайные препятствия, уровни и инверсия экрана

Прокачайте свою DIY-консоль: оживляем игру звуковыми эффектами и сохраняем лучшие результаты в памяти ESP32.

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

Что нового в версии v1.3?

Главная цель этого обновления — непредсказуемость. Теперь игроки не смогут просто заучить ритм; им придется реагировать на ситуацию в реальном времени.

  • Настоящая случайная дистанция: Мы внедрили генератор случайных чисел. Расстояние между кактусами теперь всегда разное.
  • Прогрессивная плотность: Начиная со 2-го уровня, максимальная задержка появления препятствий сокращается — кактусы летят на вас гораздо быстрее и кучнее.
  • Переменная высота: Препятствия теперь имеют случайную высоту (от 8 до 15 пикселей), что заставляет игрока точнее рассчитывать время прыжка.
  • Визуальный шок (Инверсия): При достижении 3-го уровня (30 очков) экран мгновенно инвертируется, создавая эффект «Ночного режима» и повышая сложность.

Схема подключения

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

Важно: резистор на 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Общая земля

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

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

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

Разбор ключевых механик v1.3

Истинная случайность

Чтобы функция random() выдавала разные значения при каждом включении устройства, мы инициализируем её шумом с неиспользуемого аналогового пина:

randomSeed(analogRead(0));

Логика появления препятствий по уровням

Мы реализовали сокращение «окна ожидания» по мере роста уровня. Это создает ощущение, что препятствий становится больше:

int maxSpawnDelay = (level >= 2) ? 40 : 90;
obstacleX = 128 + random(0, maxSpawnDelay);

Полный исходный код: ESP32 Dino Run v1.3 (от AndiBond)

Скопируйте и вставьте этот код в ваш Arduino IDE.

#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

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
Preferences preferences;

// Игровые параметры
int dinoY = 45;
int velocity = 0;
int gravity = 2;
bool isJumping = false;
int obstacleX = 128;
int score = 0;
int highScore = 0;

// Параметры сложности
int gameSpeed = 7;      
int obstacleWidth = 6;  
int obstacleHeight = 10; 
int level = 1;
bool inverted = false; 

// Прототипы функций
void showStartScreen();
void handleGameOver();
void drawGame();
void playSound(int freq, int duration);

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 playSound(int freq, int duration) {
  tone(BUZZER_PIN, freq, duration);
}

void loop() {
  // 1. ВВОД
  if (digitalRead(BUTTON_PIN) == LOW && !isJumping) {
    velocity = -12;
    isJumping = true;
    playSound(600, 80);
  }

  // 2. ФИЗИКА
  if (isJumping) {
    dinoY += velocity;
    velocity += gravity;
    if (dinoY >= 45) {
      dinoY = 45;
      isJumping = false;
    }
  }

  // ЛОГИКА УРОВНЕЙ
  level = (score / 10) + 1;
  gameSpeed = 6 + level; 

  // Инверсия на 3-м уровне (30 очков)
  if (level >= 3 && !inverted) {
    inverted = true;
    display.invertDisplay(true); 
    playSound(400, 200); delay(100); playSound(800, 200); 
  }

  obstacleX -= gameSpeed; 

  // Появление нового препятствия
  if (obstacleX < -20) {
    score++;
    
    // Случайная дистанция: плотнее на уровне 2+
    int maxSpawnDelay = (level >= 2) ? 40 : 90;
    obstacleX = 128 + random(0, maxSpawnDelay); 
    
    // Случайная высота
    obstacleHeight = random(8, 16);
    
    if (score % 10 == 0) playSound(1200, 50); 
  }

  // Проверка столкновений
  if (obstacleX > 15 && obstacleX < 25 && dinoY > (55 - obstacleHeight)) {
    handleGameOver();
  }

  drawGame();
  delay(30); 
}

void showStartScreen() {
  inverted = false;
  display.invertDisplay(false); 
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setTextSize(1);
  display.setCursor(25, 5);
  display.print("DINO RUN ESP32");
  display.setCursor(18, 18);
  display.print("v1.3 by AndiBond");
  display.setCursor(28, 38);
  display.print("HI-SCORE: ");
  display.print(highScore);
  display.setCursor(20, 54);
  display.print("Press to Start");
  display.display();

  while(digitalRead(BUTTON_PIN) == HIGH) { delay(10); }
  playSound(800, 100);
  delay(200); 
}

void handleGameOver() {
  playSound(150, 600);
  if (score > highScore) {
    highScore = score;
    preferences.putInt("highscore", highScore);
  }
  
  display.clearDisplay();
  display.setCursor(10, 10);
  display.setTextSize(2);
  display.print("GAME OVER");
  display.setTextSize(1);
  display.setCursor(15, 35);
  display.print("Score: "); display.print(score);
  display.print("  Lv: "); display.print(level);
  display.setCursor(30, 50);
  display.print("HI-Score: "); display.print(highScore);
  display.setCursor(30, 57);
  display.print("andibond.com");
  display.display();
  
  delay(2500);
  score = 0;
  obstacleX = 128;
  dinoY = 45;
  showStartScreen(); 
}

void drawGame() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setCursor(0, 0);
  display.print("L:"); display.print(level);
  display.setCursor(35, 0);
  display.print("S:"); display.print(score);
  display.setCursor(80, 0);
  display.print("HI:"); display.print(highScore);
  
  display.drawLine(0, 55, 128, 55, WHITE);
  display.fillRect(20, dinoY, 10, 10, WHITE); 
  display.fillRect(obstacleX, 55 - obstacleHeight, obstacleWidth, obstacleHeight, WHITE);
  
  display.display();
}
ESP32 Dino Run

AndiBond.com

Заключение

Мы прошли долгий путь от рисования одной точки на экране до создания полноценной игры с:

  • Физикой прыжка и гравитацией.
  • Системой рекордов в энергонезависимой памяти (NVS).
  • Звуковой обратной связью.
  • Случайной генерацией уровней.

Проект v1.3 от AndiBond — это отличная база для ваших собственных модификаций. Попробуйте добавить летающих врагов-птиц или бонусы для ускорения!

Делитесь вашими рекордами в комментариях и подписывайтесь на новые DIY-гайды!


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

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

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

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

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

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