В этом уроке мы создадим простую игру Змейка на базе Arduino Uno,
используя дисплей OLED SSD1306 (128x64, I2C) и модуль джойстика HW-504.
OLED SSD1306 (I2C):
VCC → 5VGND → GNDSDA → A4SCL → A5Джойстик HW-504:
VRx → A0VRy → A1SW → D2+5V → 5VGND → GND
Через Arduino IDE → Менеджер библиотек установите:
4x4 пикселя.#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// ---------- Дисплей ----------
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_ADDR 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// ---------- Джойстик ----------
#define JOY_X A0
#define JOY_Y A1
#define JOY_SW 2
// Центровка джойстика (будет измерена при включении)
int JOY_X_CENTER = 512, JOY_Y_CENTER = 512;
const int DEADZONE = 180; // мёртвая зона (подавление дрожания стика)
const bool JOY_INVERT_X = false;
const bool JOY_INVERT_Y = false;
// Длина змейки для подсчета очков
const uint8_t INITIAL_LEN = 4;
// ---------- Игровое поле ----------
#define CELL 4 // размер клетки (4x4 пикселя)
#define COLS (SCREEN_WIDTH / CELL) // 128/4 = 32 клеток по X
#define ROWS (SCREEN_HEIGHT / CELL) // 64/4 = 16 клеток по Y
#define MAX_LEN 80 // ограничение длины змейки
enum Dir : uint8_t { UP=0, RIGHT=1, DOWN=2, LEFT=3 };
// ---------- Состояние игры ----------
uint8_t snakeX[MAX_LEN];
uint8_t snakeY[MAX_LEN];
uint8_t lengthSnake = INITIAL_LEN;
Dir dir = RIGHT; // текущее направление
Dir desiredDir = RIGHT; // желаемое направление (читается с джойстика)
uint8_t appleX = 10, appleY = 5; // координаты яблока
bool paused = false;
bool gameOver = false;
unsigned long stepDelay = 130; // скорость (мс на шаг)
const unsigned long stepDelayMin = 60;
unsigned long lastStep = 0;
// Дебаунс кнопки джойстика
bool lastBtn = false;
unsigned long lastBtnTime = 0;
const unsigned long debounceMs = 150;
// ---------- Вспомогательные ----------
bool isOpposite(Dir a, Dir b) {
return (a == UP && b == DOWN) || (a == DOWN && b == UP) ||
(a == LEFT && b == RIGHT) || (a == RIGHT && b == LEFT);
}
// Проверка, занимает ли змейка клетку
bool snakeOccupies(uint8_t x, uint8_t y) {
for (uint8_t i = 0; i < lengthSnake; i++) {
if (snakeX[i] == x && snakeY[i] == y) return true;
}
return false;
}
// Калибровка центра стика (читаем в покое)
void calibrateJoystick() {
long sx = 0, sy = 0;
for (int i = 0; i < 32; ++i) {
sx += analogRead(JOY_X);
sy += analogRead(JOY_Y);
delay(2);
}
JOY_X_CENTER = sx / 32;
JOY_Y_CENTER = sy / 32;
}
// Генерация нового яблока
void spawnApple() {
for (uint16_t tries = 0; tries < 500; tries++) {
uint8_t x = random(COLS);
uint8_t y = random(ROWS);
if (!snakeOccupies(x, y)) {
appleX = x; appleY = y;
return;
}
}
appleX = 0; appleY = 0; // fallback
}
// Сброс игры
void resetGame() {
lengthSnake = INITIAL_LEN;
dir = RIGHT;
desiredDir = RIGHT;
uint8_t startX = COLS / 2 - 2;
uint8_t startY = ROWS / 2;
// snakeX[0] — голова справа, тело влево
for (uint8_t i = 0; i < lengthSnake; i++) {
snakeX[i] = startX + (lengthSnake - 1 - i);
snakeY[i] = startY;
}
spawnApple();
stepDelay = 130;
paused = false;
gameOver = false;
}
// ---------- Ввод с джойстика ----------
void readJoystick() {
int dxraw = analogRead(JOY_X) - JOY_X_CENTER;
int dyraw = analogRead(JOY_Y) - JOY_Y_CENTER;
int x = JOY_INVERT_X ? -dxraw : dxraw;
int y = JOY_INVERT_Y ? -dyraw : dyraw;
Dir newDir = dir;
if (abs(x) > abs(y)) {
if (x > DEADZONE) newDir = RIGHT;
else if (x < -DEADZONE) newDir = LEFT;
} else {
if (y > DEADZONE) newDir = DOWN;
else if (y < -DEADZONE) newDir = UP;
}
if (!isOpposite(dir, newDir) && newDir != dir) {
desiredDir = newDir;
}
bool pressed = (digitalRead(JOY_SW) == LOW);
unsigned long now = millis();
if (pressed && !lastBtn && (now - lastBtnTime > debounceMs)) {
lastBtnTime = now;
if (gameOver) resetGame();
else paused = !paused;
}
lastBtn = pressed;
}
// ---------- Логика игры ----------
void stepGame() {
if (paused || gameOver) return;
dir = desiredDir;
// следующая позиция головы
int8_t nx = snakeX[0];
int8_t ny = snakeY[0];
if (dir == UP) ny--;
else if (dir == DOWN) ny++;
else if (dir == LEFT) nx--;
else if (dir == RIGHT) nx++;
// столкновение со стеной
if (nx < 0 || ny < 0 || nx >= COLS || ny >= ROWS) {
gameOver = true;
return;
}
// сдвиг тела
uint8_t tailX = snakeX[lengthSnake - 1];
uint8_t tailY = snakeY[lengthSnake - 1];
for (int i = lengthSnake - 1; i > 0; --i) {
snakeX[i] = snakeX[i - 1];
snakeY[i] = snakeY[i - 1];
}
// новая голова
snakeX[0] = (uint8_t)nx;
snakeY[0] = (uint8_t)ny;
// яблоко?
if (snakeX[0] == appleX && snakeY[0] == appleY) {
if (lengthSnake < MAX_LEN) {
snakeX[lengthSnake] = tailX;
snakeY[lengthSnake] = tailY;
lengthSnake++;
}
spawnApple();
if (stepDelay > stepDelayMin) stepDelay -= 5;
}
// самопересечение
for (uint8_t i = 1; i < lengthSnake; i++) {
if (snakeX[i] == snakeX[0] && snakeY[i] == snakeY[0]) {
gameOver = true;
return;
}
}
}
// ---------- Рендер ----------
void drawCell(uint8_t cx, uint8_t cy, bool filled = true) {
int px = cx * CELL;
int py = cy * CELL;
if (filled) display.fillRect(px, py, CELL, CELL, SSD1306_WHITE);
else display.drawRect(px, py, CELL, CELL, SSD1306_WHITE);
}
void render() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
if (gameOver) {
// --- режим GAME OVER ---
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(18, 14);
display.print(F("Total score: "));
display.print((int)(lengthSnake - INITIAL_LEN));
display.setTextSize(2);
display.setCursor(10, 28);
display.print(F("GAME OVER"));
display.setTextSize(1);
display.setCursor(5, 50);
display.print(F("Press btn to restart"));
}
else {
// игровое поле
display.drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, SSD1306_WHITE);
// яблоко
int ax = appleX * CELL;
int ay = appleY * CELL;
display.fillRect(ax + 1, ay + 1, CELL - 2, CELL - 2, SSD1306_WHITE);
// змейка
for (uint8_t i = 0; i < lengthSnake; i++) {
drawCell(snakeX[i], snakeY[i], true);
}
if (paused) {
display.setTextSize(1);
display.setCursor(48, 2);
display.print(F("PAUSE"));
}
}
display.display();
}
// ---------- Setup / Loop ----------
void setup() {
pinMode(JOY_SW, INPUT_PULLUP);
analogReference(DEFAULT);
randomSeed(analogRead(A2));
delay(10);
calibrateJoystick(); // держим стик в покое при включении
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
for (;;) {} // если дисплей не найден — зависаем
}
display.clearDisplay();
display.display();
resetGame();
}
void loop() {
readJoystick();
unsigned long now = millis();
if (now - lastStep >= stepDelay) {
lastStep = now;
stepGame();
}
render();
}
🔄 Режим «телепорт»
Сделайте так, чтобы при выходе за край экрана змейка появлялась с противоположной стороны.
📊 Очки в реальном времени
Добавьте счётчик очков в верхней строке экрана, который будет увеличиваться при каждом съеденном яблоке.
🎨 Измените графику
Попробуйте рисовать голову змейки другим символом (например, квадрат с точкой или стрелку в направлении движения).
⚡ Изменение скорости
Сделайте так, чтобы скорость змейки увеличивалась каждые 5 очков.
🍏 Несколько яблок
Реализуйте появление сразу двух яблок на поле, чтобы усложнить задачу.
🏆 Рекордный результат
Добавьте хранение рекорда в EEPROM, чтобы лучший результат сохранялся даже после перезапуска Arduino.
👾 Препятствия
Реализуйте появление случайных «блоков-препятствий», в которые нельзя врезаться.
🐍 Разные уровни
Сделайте уровни сложности: лёгкий (медленно), средний, сложный (быстро и с препятствиями).