В этом уроке мы создадим игру «Змейка». Это известная многим игра «Snake» впервые выпущенная фирмой Gremlin Industries в 1977 г. Первоначальная версия игры была написана для игрового автомата «Hustle», далее игра распространилась на компьютеры, игровые приставки и сотовые телефоны. Игра присутствовала и на игровых автоматах в CCCР, её переводили как: «Змейка», «Удав», «Питон» и т.д.
Правила игры «Змейка»:
На игровом поле появляется змейка и её еда, представленные набором фигур. В процессе игры змейка постоянно ползёт, игрок не может её остановить, но может направлять её голову влево, вправо, вверх и вниз, а хвост змейки движется следом. Задача игрока заключается в том, что бы змейка съела еду избегая столкновений со своим хвостом и с границами игрового поля. После каждого съеденного куска еды, змейка становится длиннее (что усложняет задачу игроку), а еда вновь появляется в случайном месте игрового поля.
Примечание: В некоторых версиях игра не имеет границ игрового поля, заползая за экран, змейка появляется с противоположной стороны экрана. Есть версии игры с дополнительными препятствиями и даже трехмерная версия игры.
Управление:
Традиционно игрок управляет змейкой используя кнопки или джойстик, но мы откажемся от этих деталей в пользу акселерометра, присутствующего в Trema-модуле IMU 9 DOF (Inertial Measurement Unit 9 Degrees Of Freedom). Таким образом управление змейкой будет осуществляться наклоном всего устройства влево, вправо, вперёд или назад.
Нам понадобится:
- Arduino Uno х 1шт.
- Trema OLED-дисплей 128x64 х 1шт.
- Trema-модуль IMU 9 DOF х 1шт.
- Trema Set Shield х 1шт.
И никаких проводов (кроме USB для загрузки скетча).
Для реализации проекта нам необходимо установить библиотеку:
- iarduino_OLED - графическая библиотека для работы с Trema OLED дисплеями.
О том как устанавливать библиотеки, Вы можете ознакомиться на странице Wiki - Установка библиотек в Arduino IDE.
Видео:
Схема подключения:
Перед подключением модулей, закрепите винтами нейлоновые стойки в отверстиях секций 3 и 4 Trema Set Shield.
- Установите Trema Set Shield на Arduino Uno.
- Установите Trema OLED-дисплей 128x64 в 3 посадочную площадку, в верхнюю I2C колодку.

- Установите Trema-модуль IMU 9 DOF в 4 посадочную площадку, в верхнюю I2C колодку.

- Полученный результат представлен на рисунке ниже.

После чего закрепите установленные модули вкрутив через их отверстия нейлоновые винты в установленные ранее нейлоновые стойки (нейлоновые стойки и винты входят в комплектацию Trema Set Shield).

Наличие всего двух колодок в секциях Trema Set Shield, не позволит Вам неправильно установить модули, т.к. при неправильном подключении модули будут смещены относительно разметки своей секции и Вы не сможете закрепить их винтами.
Код программы:
#include <Wire.h> // Подключаем библиотеку для работы с аппаратной шиной I2C, до подключения библиотеки iarduino_Position_BMX055.
#include <iarduino_Position_BMX055.h> // Подключаем библиотеку iarduino_Position_BMX055 для работы с Trema-модулем IMU 9 DOF.
iarduino_Position_BMX055 sensor(BMA); // Создаём объект sensor указывая что ему требуется работать только с акселерометром.
#define BMX055_DISABLE_BMG // Не использовать гироскоп.
#define BMX055_DISABLE_BMM // Не использовать магнитометр.
#define MAX_VALUE 1.50 // Максимальное значение по оси измерений.
#define MIN_VALUE -1.50 // Минимальное значение по оси измерений.
//
#include <iarduino_OLED.h> // Подключаем библиотеку iarduino_OLED.
iarduino_OLED myOLED(0x3C); // Объявляем объект myOLED, указывая адрес дисплея на шине I2C: 0x3C.
extern const uint8_t MediumFontRus[]; // Подключаем шрифт MediumFontRus.
extern const uint8_t SmallFontRus[]; // Подключаем шрифт SmallFontRus.
/* Объявление переменных */
int Tail[115]; // Массив тела змейки.
int Choice; // Переменная выбора движения.
int nTail; // Переменная количества элементов в змейке.
int CoreX; // Переменная X-координаты квадратика.
int CoreY; // Переменные Y-координаты квадратика.
int x; // Переменная X-координаты движения.
int y; // Переменная Y-координаты движения.
int EndX; // Переменная X-координаты последнего элемента массива.
int EndY; // Переменная Y-координаты последнего элемента массива.
int del; // Переменная задержки.
int ycor; // Дополнительная переменная Y-координаты с которой строится и выводится завершающаяся змейка.
int aTail; // Дополнительная переменная количества элементов в змейке.
/* Объявление функций */
void ShowSnake(); // Функция вывода всей змейки на экран после выигрыша или проигрыша.
void SplashScreen(); // Функция вывода заставки игры на экран.
void GameOver(); // Функция завершения игры.
void SpeedControl(); // Функция регулировки скорости.
void MoveControl(); // Функция движения змейки и выбора направления.
//
void setup() //
{ //
myOLED.begin(); // Инициируем работу с дисплеем.
SplashScreen(); // Функция вывода заставки игры на экран.
myOLED.autoUpdate(false); // Запрещаем автоматический вывод данных. Информация на дисплее будет обновляться только после обращения к функции update().
myOLED.clrScr(); // Чистим экран.
while(!Serial){} // Ждём готовность Serial к передаче данных в монитор последовательного порта.
sensor.begin(&Wire, true); // Инициируем работу с датчиками объекта sensor.
//
nTail = 3; // Назначаем количество элементов в змейке 3.
del = 200; // Присваиваем переменной значение задержки 200 мс.
y = random(3, 30); // Начальное положение Y-координаты.
x = random(3, 14); // Начальное положение X-координаты.
Choice = random(1, 4); // Начальное направление движения.
//
for (int i = 0; i <= nTail-1; i++) // Формируем змейку из трех элементов в зависимости от направления движения. Присваиваем координаты каждому элементу змейке.
{ //
Tail[i] = y; // Присваиваем Y-координату элементу массива.
Tail[i] = Tail[i] << 8; // Сдвигаем побитно на 8.
Tail[i] += x; // Присваиваем X-координату элементу массива.
switch (Choice) // В зависимости от направления движения формируем хвост в противоположную сторону.
{ //
case 1: // Движение вверх.
x++; // Формируем хвост змейки вниз.
break; //
case 2: // Движение вниз.
x--; // Формируем хвост змейки вверх.
break; //
case 3: // Движение влево.
y++; // Формируем хвост змейки вправо.
break; //
case 4: // Движение вправо.
y--; // Формируем хвост змейки влево.
break; //
} //
} //
//
for (int i = 0; i <= nTail-1; i++){ // Выводим змейку на экран с помощью цикла.
myOLED.drawRect(highByte(Tail[i])*4, lowByte(Tail[i])*4, highByte(Tail[i])*4+4, lowByte(Tail[i])*4+4, true, 1);}
//
CoreY = random(2, 32); // Задаем Y-координату положения квадратика.
CoreX = random(2, 15); // Задаем X-координату положения квадратика.
myOLED.drawRect (CoreY*4, CoreX*4, CoreY*4 + 4, CoreX*4 + 4, true , 1); // Выводим квадратик.
//
y = highByte(Tail[0]); // Y-координату первого элемента массива присваиваем начальной Y-координате движения.
x = lowByte(Tail[0]); // X-координату первого элемента массива присваиваем начальной X-координате движения.
} //
void loop() //
{ //
SpeedControl(); // Функция регулировки скорости.
sensor.read(); // Читаем данные датчика (акселерометра).
//
myOLED.drawRect (127, 63, 0, 0, false , 1); // Обновляем каждый шаг рамку игрового поля.
myOLED.drawRect (CoreY*4, CoreX*4, CoreY*4 + 4, CoreX*4 + 4, true , 1); // Обновляем каждый шаг квадратик.
//
EndY = highByte(Tail[nTail-1]); // Запоминаем Y-координаты последнего элемента массива.
EndX = lowByte(Tail[nTail-1]); // Запоминаем Y-координаты последнего элемента массива.
//
MoveControl(); // Функция движения змейки и выбора направления.
//
int PrevSY = highByte(Tail[0]); // Дополнительная переменная для переноса Y-координаты массива.
int PrevSX = lowByte(Tail[0]); // Дополнительная переменная для переноса X-координаты массива.
int Prev2SX, Prev2SY; // Вторые дополнительные переменные для переноса координат массива.
Tail[0] = y; // Присваиваем новую Y-координату первому элементу массива.
Tail[0] = Tail[0] << 8; // Сдвигаем побитно на 8.
Tail[0] += x; // Присваиваем новую Y-координату первому элементу массива.
for (int i = 1; i <= nTail-1; i++) // Переносим каждый элемент массива по очереди с помощью цикла.
{ //
Prev2SY = highByte(Tail[i]); // Присваиваем второй дополнительное переменной Y-координату массива.
Prev2SX = lowByte(Tail[i]); // Присваиваем второй дополнительное переменной X-координату массива.
Tail[i] = PrevSY; // Присваиваем новому элементу массива старую Y-координату предыдущего элемента массива.
Tail[i] = Tail[i] << 8; // Сдвигаем побитно на 8.
Tail[i] += PrevSX; // Присваиваем новому элементу массива старую X-координату предыдущего элемента массива.
PrevSY = Prev2SY; // Присваиваем дополнительным переменным с Y-координатой вторые дополнительные переменные с Y-координатой.
PrevSX = Prev2SX; // Присваиваем дополнительным переменным с X-координатой вторые дополнительные переменные с X-координатой.
} //
//
for (int i = 0; i <= nTail-1; i++){ // Выводим змейку на экран.
myOLED.drawRect (highByte(Tail[i])*4, lowByte(Tail[i])*4, highByte(Tail[i])*4 + 4, lowByte(Tail[i])*4 + 4, true , 1);}
//
myOLED.drawRect (EndY*4, EndX*4, EndY*4 + 4, EndX*4 + 4, true , 0); // Очищаем последний элемент змейки.
myOLED.update(); // Обновляем информацию на дисплее.
//
if (x == CoreX && y == CoreY) // Сравниваем координаты змейки с координатами квадратика.
{ //
myOLED.drawRect (CoreY*4, CoreX*4, CoreY*4 + 4, CoreX*4 + 4, true , 0); // Если они совпадают, очищаем старый квадратик.
CoreY = random(2, 32); // Задаем новую Y-координату квадратика.
CoreX = random(2, 15); // Задаем новую X-координату квадратика.
nTail++; // Увеличиваем количество элементов в змейке на один.
} //
//
if (x > 14 || x < 0 || y > 31 || y < 0){ // Проверка выхода змейки за пределы игрового поля. Если координат движения больше, чем игровое поле, то игра заканчивается.
GameOver(); // Функция завершения игры.
ShowSnake();} // Функция вывода всей змейки на экран после проигрыша.
//
for (int i = 1; i < nTail-1; i++){ // Проверки столкновения головы змейки с хвостом.
if (y == highByte(Tail[i]) && x == lowByte(Tail[i])){ // С помощью цикла проверяем каждый элемент массива с координатами движения. Если они совпадают, то голова столкнулась с хвостом.
GameOver(); // Функция завершения игры.
ShowSnake();}} // Функция вывода всей змейки на экран после проигрыша.
//
if (nTail == 115){ // Проверка количества элементов змейки на выигрыш.
myOLED.clrScr(); // Чистим экран.
myOLED.setFont(MediumFontRus); // Указываем шрифт который требуется использовать для вывода цифр и текста.
myOLED.print("ВЫИГРЫШ!", OLED_C, 20); // Выводим текст по центру 30 строки.
myOLED.autoUpdate(true); // Разрешаем автоматический вывод данных.
ShowSnake();} // Функция вывода всей змейки на экран после выигрыша.
} //
// Функция вывода всей змейки на экран после выигрыша или проигрыша.
void ShowSnake() //
{ //
ycor = 6; // Присваиваем дополнительное координате значение 6. С 6 Y-координаты строим змейку столбцами по 5 элементов.
for (int k = 1; k <= 100; k++) // Цикл вывода змейки на экран.
{ //
if (nTail > 5) // Сравниваем количество элементов с 5. Столбцы формируемые снизу вверх. Нечетные столбцы.
{ // Если больше 5.
aTail = 5; // Присваиваем дополнительной переменной значение 5.
for (int i = 56; i >= 56 - aTail*4; i = i - 5) // Цикл формирования и вывода змейки по координатам, начиная с X = 56 до X = 36 с шагом 5.
{ //
myOLED.drawRect (ycor, i, ycor+3, i+3, true, 1); // Вывод змейки квадратами начиная с координат Y = 6, X = 56, Y = 9, X = 59 до координат Y = 6, X = 36 Y = 9, X = 39. Значения Y-координаты указаны для первого столбца.
delay(200); // Задержка 200 мс.
} //
nTail = nTail - 5; // Вычитаем из количества элементов в змейке 5. Для определения количества выводимых столбцов.
} //
else // Если меньше 5.
{ //
aTail = nTail; // Присваиваем оставшееся количество элементов дополнительной переменной.
for (int i = 56; i >= 56 - aTail*4; i = i - 5) // Цикл формирования и вывода змейки по координатам с шагом 5, начиная с X = 56 до X-координаты соответствующая значению количества оставшихся элементов змейки.
{ //
myOLED.drawRect (ycor, i, ycor+3, i+3, true, 1); // Вывод змейки квадратами начиная с координат Y = 6, X = 56, Y = 9, X = 59 до координат Y = 6, X = 36 Y = 9, X = 39. Значения Y-координаты указаны для первого столбца.
delay(200); // Задержка 200 мс.
} //
delay (4000); // Задержка 4 секунды.
return setup(); // Возвращаемся в функцию Setup().
} //
if (nTail > 5) // Сравниваем количество элементов с 5. Столбцы формируемые сверху вниз. Четные столбцы.
{ // Если больше 5.
aTail = 5; // Присваиваем дополнительной переменной значение 5.
for (int i = 36; i <= 36 + aTail*4; i = i + 5) // Цикл формирования и вывода змейки по координатам, начиная с X = 36 до X = 56 с шагом 5.
{ //
myOLED.drawRect (ycor+5, i, ycor+8, i+3, true, 1); // Вывод змейки квадратами начиная с координат Y = 11, X = 36, Y = 14, X = 39 до координат Y = 11, X = 56 Y = 14, X = 59. Значения Y-координаты указаны для первого столбца.
delay(200); // Задержка 200 мс.
} //
nTail = nTail - 5; // Вычитаем из количества элементов в змейке 5. Для определения количества выводимых столбцов.
} //
else // Если меньше 5.
{ //
aTail = nTail; // Присваиваем оставшееся количество элементов дополнительной переменной.
for (int i = 36; i <= 36 + aTail*4; i = i + 5) // Цикл формирования и вывода змейки по координатам с шагом 5, начиная с X = 36 до X-координаты соответствующая значению количества оставшихся элементов змейки.
{ //
myOLED.drawRect (ycor+5, i, ycor+8, i+3, true, 1); // Вывод змейки квадратами начиная с координат Y = 11, X = 36, Y = 14, X = 39 до координат Y = 11, X = 56 Y = 14, X = 59. Значения Y-координаты указаны для первого столбца.
delay(200); // Задержка 200 мс.
} //
delay (4000); // Задержка 4 секунды.
return setup(); // Возвращаемся в функцию Setup().
} //
ycor = ycor + 10; // Увеличиваем дополнительную Y-координату на значение 10. Строим новый ряд столбцов.
} //
} //
// Функция вывода заставки игры на экран.
void SplashScreen() //
{ //
myOLED.setFont(MediumFontRus); // Указываем шрифт который требуется использовать для вывода цифр и текста.
myOLED.print("Змейка", OLED_C, 27); // Выводим текст по центру 30 строки.
myOLED.drawRect (127, 63, 0, 0, false , 1); // Выводим рамку игрового поля.
// Формируем рисунок змейки с помощью циклов и выводим на экран.
for (int i = 56; i >= 36; i = i - 5){ // Цикл формирования рисунка змейки. //
myOLED.drawRect (16, i, 19, i + 3, true , 1); // Выводим рисунок на экран. //
delay(300);} // Задержка 300 мс. //
for (int i = 21; i <= 41; i = i + 5){ // Цикл формирования рисунка змейки. //
myOLED.drawRect (i, 36, i + 3, 39, true , 1); // Выводим рисунок на экран. //
delay(300);} // Задержка 300 мс. //
for (int i = 41; i <= 51; i = i + 5){ // Цикл формирования рисунка змейки. // # # # # #
myOLED.drawRect (41, i, 44, i + 3, true , 1); // Выводим рисунок на экран. // # # # # #
delay(300);} // Задержка 300 мс. // # # # # #
for (int i = 46; i <= 56; i = i + 5){ // Цикл формирования рисунка змейки. // #
myOLED.drawRect (i, 51, i + 3, 54, true , 1); // Выводим рисунок на экран. // #
delay(300);} // Задержка 300 мс. //
myOLED.drawRect (56, 46, 59, 49, true , 1); // Выводим рисунок на экран. //
delay(300); // Задержка 300 мс. //
for (int i = 61; i <= 66; i = i + 5){ // Цикл формирования рисунка змейки. //
myOLED.drawRect (i, 46, i + 3, 49, true , 1); // Выводим рисунок на экран. //
delay(300);} // Задержка 300 мс. //
for (int i = 0; i <= 5; i++){ // Рисунок мигающего элемента. С помощью цикла формируем мигание.
myOLED.drawRect (91, 46, 94, 49, true , 0); // Гасим рисунок.
delay(300); // Задержка 300 мс.
myOLED.drawRect (91, 46, 94, 49, true , 1); // Выводим рисунок.
delay(300);} // Задержка 300 мс.
//
myOLED.clrScr(); // Чистим экран.
myOLED.print("От", OLED_C, 25); // Выводим текст по центру 20 строки.
myOLED.print("IARDUINO", OLED_C, 50); // Выводим текст по центру 20 строки.
delay (3000); // Задержка 3 секунды.
} //
// Функция завершения игры.
void GameOver() //
{ //
myOLED.clrScr(); // Чистим экран.
myOLED.setFont(SmallFontRus); // Указываем шрифт который требуется использовать для вывода цифр и текста.
myOLED.print("Конец игры", OLED_C, 14); // Выводим текст по центру 25 строки.
myOLED.print("Результат - ", 25, 25); // Выводим текст 25 столбца 20 строки.
myOLED.print(nTail-3, 95, 25); // Выводим переменную 95 столбца 40 строки.
myOLED.autoUpdate(true); // Разрешаем автоматический вывод данных.
} //
// Функция регулировки скорости.
void SpeedControl() //
{ //
delay (del); // Регулируем скорость змейки. Первая скорость - задержка 200 мс.
if (nTail == 5){del = 100;} // Вторая скорость. Задержка 100 мс. Количество элементов змейки = 5.
if (nTail == 10){del = 50;} // Третья скорость. Задержка 50 мс. Количество элементов змейки = 10.
if (nTail == 15){del = 0;} // Четвертая скорость. Задержки нет. Количество элементов змейки = 15.
} //
// Функция движения змейки и выбора направления.
void MoveControl() //
{ //
switch (Choice) //
{ //
case 3: // Движение влево.
if (sensor.axisX >= -2 && sensor.axisY >= MAX_VALUE){Choice = 1;} // Проверяем данные по оси Y. Поворот вверх.
if (sensor.axisX >= -2 && sensor.axisY <= MIN_VALUE){Choice = 2;} // Проверяем данные по оси Y. Поворот вниз.
y--; // Координаты формирования змейки при движении влево. Уменьшаем координату Y на количество пройденных квадратиков.
break; //
//
case 4: // Движение вправо.
if (sensor.axisX <= 2 && sensor.axisY >= MAX_VALUE){Choice = 1;} // Проверяем данные по оси Y. Поворот вверх.
if (sensor.axisX <= 2 && sensor.axisY <= MIN_VALUE){Choice = 2;} // Проверяем данные по оси Y. Поворот вниз.
y++; // Координаты формирования змейки при движении влево. Увеличиваем координату Y на количество пройденных квадратиков.
break; //
//
case 2: // Движение вниз.
if (sensor.axisY >= -2 && sensor.axisX >= MAX_VALUE){Choice = 4;} // Проверяем данные по оси X. Поворот вправо.
if (sensor.axisY >= -2 && sensor.axisX <= MIN_VALUE){Choice = 3;} // Проверяем данные по оси X. Поворот влево.
x++; // Координаты формирования змейки при движении влево. Увеличиваем координату X на количество пройденных квадратиков.
break; //
//
case 1: // Движение вверх.
if (sensor.axisY <= 2 && sensor.axisX >= MAX_VALUE){Choice = 4;} // Проверяем данные по оси X. Поворот вправо.
if (sensor.axisY <= 2 && sensor.axisX <= MIN_VALUE){Choice = 3;} // Проверяем данные по оси X. Поворот влево.
x--; // Координаты формирования змейки при движении влево. Уменьшаем координату X на количество пройденных квадратиков.
break; //
} //
} //
Алгоритм работы:
- В начале скетча (до кода setup) выполняются следующие действия:
- Подключаем библиотеку iarduino_Position_BMX055 для работы с датчиком Trema IMU 9 DOF.
- Объявляем объект sensor, указывая работу только с акселерометром.
- Подключаем графическую библиотеку iarduino_OLED для работы с Trema OLED дисплеем.
- Объявляем объект myOLED указывая адрес дисплея на шине I2C, он должен совпадать с адресом установленным переключателем на обратной стороне платы OLED дисплея.
- Подключаем шрифты предустановленные в библиотеке myOLED.
- Объявляем массив и переменные участвующие в работе скетча.
- Объявляем функции используемые в скетче.
- В коде setup выполняются следующие действия:
- Инициируем работу с Trema OLED дисплеем и запрещаем автоматический вывод данных.
- Выводим анимированную заставку (текст «Змейка» с появляющимися фигурами) и очищаем экран.
- Инициируем работу с датчиком.
- Определяем начальные параметры игры (Координаты головы змейки, координаты хвоста змейки, координаты квадратика, количество элементов хвоста змейки, выбор направления движения)
- В коде loop выполняются следующие действия:
- Управление скоростью змейки.
- Чтение данных с датчика.
- Обновление рамки и квадратика дисплея.
- Выбор поворота змейки.
- Перемещение по частям каждого элемента массива.
- Вывод змеи с новыми координатами и очищение последнего элемента.
- Разрешаем автоматический вывод данных.
- Проверка на увеличение количества элементов в змейке.
- Проверка проигрышных и выигрышных ситуаций (Выход змейки за пределы игрового поля, столкновение головы змейки с хвостом, набор максимального количества элементов змейки).
Все строки скетча (кода программы) прокомментированы, так что Вы можете подробнее ознакомиться с кодом прочитав комментарии строк.

Обсуждение