#include <Adafruit_GFX.h> // Библиотека для графики
#include <Adafruit_SSD1306.h> // Библиотека для OLED-дисплея SSD1306
#include <Wire.h> // Библиотека для I2C
#define SCREEN_WIDTH 128 // Ширина дисплея в пикселях
#define SCREEN_HEIGHT 64 // Высота дисплея в пикселях
#define OLED_RESET -1 // Пин сброса дисплея (не используется)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); // Объект дисплея
// Пины для кнопок управления (влево и вправо)
const int buttonLeftPin = 3;
const int buttonRightPin = 2;
#define HORIZON_Y 20 // Позиция горизонта по вертикали на экране
#define ROAD_BOTTOM_WIDTH 100 // Ширина дороги у нижнего края экрана (ближе к игроку)
#define ROAD_TOP_WIDTH 10 // Ширина дороги у горизонта (дальняя точка)
#define ROAD_CENTER_X (SCREEN_WIDTH / 2) // Центр дороги по горизонтали
int playerLane = 0; // Текущая полоса игрока (0 - левая, 1 - правая)
const int laneCount = 2; // Количество полос на дороге
int obstacleLane = 0; // Полоса препятствия
int obstacleY = 0; // Вертикальная позиция препятствия
bool obstacleVisible = false; // Видимость препятствия
unsigned long lastObstacleMove = 0; // Время последнего перемещения препятствия
unsigned long obstacleInterval = 60; // Интервал между перемещениями препятствия (мс)
bool gameOver = false; // Флаг окончания игры
int score = 0; // Счёт игрока
// Битовый массив для спрайта машинки (7 строк, 11 пикселей ширина)
const uint16_t carSprite[7] = {
0b00111111100,
0b01111111110,
0b10001110001,
0b11111111111,
0b11111111111,
0b00010000100,
0b00010000100
};
unsigned long lastBlinkTime = 0; // Время последнего мигания линий разметки
bool blinkOn = false; // Текущий статус мигания (включено/выключено)
void setup() {
// Настройка пинов кнопок как входы с подтяжкой к питанию
pinMode(buttonLeftPin, INPUT_PULLUP);
pinMode(buttonRightPin, INPUT_PULLUP);
// Инициализация OLED дисплея, если не получилось — зацикливаем программу
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
while (true);
}
// Показываем приветственный экран
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setCursor(10, 10);
display.setTextSize(1);
display.println("RETRO RACER");
display.display();
delay(2000); // Пауза 2 секунды
}
void loop() {
if (!gameOver) {
handleInput(); // Обработка нажатий кнопок
updateObstacle(); // Обновление позиции препятствия
checkCollision(); // Проверка столкновения с препятствием
drawFrame(); // Отрисовка текущего кадра
} else {
// Экран "Game Over" с результатом
display.clearDisplay();
display.setCursor(20, 20);
display.setTextSize(1);
display.println("GAME OVER");
display.setCursor(20, 35);
display.print("Score: ");
display.println(score);
display.display();
delay(3000); // Пауза 3 секунды перед рестартом
restartGame(); // Перезапуск игры
}
}
void handleInput() {
// Если нажата левая кнопка и игрок не в крайней левой полосе — сдвинуть влево
if (digitalRead(buttonLeftPin) == LOW && playerLane > 0) {
playerLane--;
delay(150); // Задержка для избежания слишком быстрого повторного срабатывания
}
// Если нажата правая кнопка и игрок не в крайней правой полосе — сдвинуть вправо
if (digitalRead(buttonRightPin) == LOW && playerLane < laneCount - 1) {
playerLane++;
delay(150);
}
}
void updateObstacle() {
// Проверяем прошло ли время для обновления положения препятствия
if (millis() - lastObstacleMove > obstacleInterval) {
if (obstacleVisible) {
obstacleY += 4; // Опускаем препятствие вниз
// Если препятствие вышло за экран — скрываем его и увеличиваем счёт
if (obstacleY > SCREEN_HEIGHT) {
obstacleVisible = false;
score++;
}
} else {
// Если препятствие не видно — создаём новое в случайной полосе у горизонта
obstacleY = HORIZON_Y + 1;
obstacleLane = random(0, laneCount);
obstacleVisible = true;
}
lastObstacleMove = millis();
}
}
void checkCollision() {
// Проверяем столкновение — если препятствие в зоне машины игрока по вертикали и в той же полосе
if (obstacleVisible && obstacleY >= 55 && obstacleY <= 60 && obstacleLane == playerLane) {
gameOver = true;
}
}
void restartGame() {
// Сброс игровых параметров для новой игры
playerLane = 0;
obstacleLane = 0;
obstacleY = 0;
obstacleVisible = false;
score = 0;
gameOver = false;
blinkOn = false;
lastBlinkTime = 0;
}
void drawFrame() {
display.clearDisplay(); // Очищаем экран перед рисованием
drawBackground(); // Рисуем фон (горизонт, солнце, горы)
drawRoad(); // Рисуем дорогу
drawLaneLines(); // Рисуем мигающую разметку
drawCar(playerLane, 60, true); // Рисуем машину игрока (на фиксированной высоте)
if (obstacleVisible && obstacleY > HORIZON_Y)
drawCar(obstacleLane, obstacleY, false); // Рисуем препятствие
// Отрисовка счёта в левом верхнем углу
display.setCursor(0, 0);
display.setTextSize(1);
display.print("Score: ");
display.println(score);
display.display(); // Выводим всё на экран
}
void drawBackground() {
// Горизонтальная линия горизонта
display.drawLine(0, HORIZON_Y, SCREEN_WIDTH, HORIZON_Y, SSD1306_WHITE);
int sunX = 110; // Позиция солнца по X
int sunY = 8; // Позиция солнца по Y
int radius = 6; // Радиус солнца
// Рисуем закрашенный круг солнца (псевдо-окружность с заполнением)
for (int y = -radius; y <= radius; y++) {
int width = (int)sqrt(radius * radius - y * y);
display.drawFastHLine(sunX - width, sunY + y, width * 2 + 1, SSD1306_WHITE);
}
// Рисуем горы — два треугольника у горизонта
display.drawTriangle(20, HORIZON_Y, 35, 5, 50, HORIZON_Y, SSD1306_WHITE);
display.drawTriangle(60, HORIZON_Y, 75, 10, 90, HORIZON_Y, SSD1306_WHITE);
}
void drawRoad() {
int halfBottom = ROAD_BOTTOM_WIDTH / 2;
int halfTop = ROAD_TOP_WIDTH / 2;
// Рисуем две боковые линии дороги — расширяющиеся к игроку
display.drawLine(ROAD_CENTER_X - halfBottom, SCREEN_HEIGHT, ROAD_CENTER_X - halfTop, HORIZON_Y, SSD1306_WHITE);
display.drawLine(ROAD_CENTER_X + halfBottom, SCREEN_HEIGHT, ROAD_CENTER_X + halfTop, HORIZON_Y, SSD1306_WHITE);
}
void drawLaneLines() {
unsigned long now = millis();
// Переключаем мигание каждые 250 мс
if (now - lastBlinkTime > 250) {
blinkOn = !blinkOn;
lastBlinkTime = now;
}
// Рисуем полосы разметки на дороге
for (int i = 0; i < 10; i++) {
int y = SCREEN_HEIGHT - i * 10; // Позиция линии снизу вверх
if (y < HORIZON_Y) continue; // Не рисуем за горизонтом
// Линейная интерполяция положения левой и правой границ дороги на текущей высоте y
int xStart = map(y, HORIZON_Y, SCREEN_HEIGHT, ROAD_CENTER_X - ROAD_TOP_WIDTH / 2, ROAD_CENTER_X - ROAD_BOTTOM_WIDTH / 2);
int xEnd = map(y, HORIZON_Y, SCREEN_HEIGHT, ROAD_CENTER_X + ROAD_TOP_WIDTH / 2, ROAD_CENTER_X + ROAD_BOTTOM_WIDTH / 2);
int midX = (xStart + xEnd) / 2; // Центр дороги в этой точке по X
// Рисуем мигающие линии разметки с шагом 2 и миганием
if ((i % 2 == 0 && blinkOn) || (i % 2 == 1 && !blinkOn)) {
display.drawLine(midX, y, midX, y - 5, SSD1306_WHITE);
}
}
}
void drawCar(int lane, int y, bool isPlayer) {
int x = laneToX(lane, y); // Получаем X позицию машины в зависимости от полосы и высоты
int scale = obstacleScale(y); // Масштабируем машину в зависимости от позиции (перспектива)
int spriteWidth = 11;
int spriteHeight = 7;
// Рисуем спрайт машины пиксель за пикселем с масштабированием
for (int row = 0; row < spriteHeight; row++) {
for (int col = 0; col < spriteWidth; col++) {
// Проверяем, установлен ли бит для текущего пикселя
if (carSprite[row] & (1 << (spriteWidth - 1 - col))) {
// Рисуем масштабированный квадрат пикселей
for (int dx = 0; dx < scale; dx++) {
for (int dy = 0; dy < scale; dy++) {
display.drawPixel(
x - (spriteWidth / 2) * scale + col * scale + dx,
y - (spriteHeight / 2) * scale + row * scale + dy,
SSD1306_WHITE
);
}
}
}
}
}
}
// Функция вычисляет X координату центра машины для заданной полосы и позиции по Y
int laneToX(int lane, int y) {
int left = map(y, HORIZON_Y, SCREEN_HEIGHT, ROAD_CENTER_X - ROAD_TOP_WIDTH / 2, ROAD_CENTER_X - ROAD_BOTTOM_WIDTH / 2);
int right = map(y, HORIZON_Y, SCREEN_HEIGHT, ROAD_CENTER_X + ROAD_TOP_WIDTH / 2, ROAD_CENTER_X + ROAD_BOTTOM_WIDTH / 2);
int laneWidth = (right - left) / laneCount;
return left + laneWidth * lane + laneWidth / 2;
}
// Функция возвращает масштаб машины в зависимости от позиции по Y для эффекта перспективы
int obstacleScale(int y) {
return map(y, HORIZON_Y, SCREEN_HEIGHT, 1, 3);
}