В этом уроке мы создадим игру «Змейка». Это известная многим игра «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 выполняются следующие действия:
- Управление скоростью змейки.
- Чтение данных с датчика.
- Обновление рамки и квадратика дисплея.
- Выбор поворота змейки.
- Перемещение по частям каждого элемента массива.
- Вывод змеи с новыми координатами и очищение последнего элемента.
- Разрешаем автоматический вывод данных.
- Проверка на увеличение количества элементов в змейке.
- Проверка проигрышных и выигрышных ситуаций (Выход змейки за пределы игрового поля, столкновение головы змейки с хвостом, набор максимального количества элементов змейки).
Все строки скетча (кода программы) прокомментированы, так что Вы можете подробнее ознакомиться с кодом прочитав комментарии строк.
Обсуждение