Игра “Ковбой”
Простая игра в стиле “перепрыгни препятствие” и “уклонись от врагов” с подсчётом полученных очков. В этот раз управление реализовано с помощью пульта ДУ. Пьезодинамик служит для примитивного звукового сопровождения.
СБОРКА СХЕМЫ НА МАКЕТНОЙ ПЛАТЕ
Нам понадобятся следующие компоненты:
- Arduino Uno;
- USB-кабель для подключения к компьютеру;
- 1 LCD дисплей с I2C модулем;
- 1 пьезодинамик;
- 1 тактовая кнопка;
- 1 резистор номиналом 10 кОм;
- 1 ИК приёмник;
- 1 ИК пульт ДУ;
- 4 соединительных проводов типа папа-мама различных цветов;
- 9 соединительных проводов типа папа-папа различных цветов.
Соберите устройство по следующей схеме:
Вначале, нам нужно определить: какие кнопки пульта ДУ мы будем использовать для управления нашим персонажем. Выберите 4 кнопки, которые будут перемещать персонаж влево и вправо, заставлять его подпрыгивать и служить выходом из игры. Далее, прочитайте коды этих кнопок в мониторе порта, загрузив в Arduino следующий скетч:
#include <IRremote.h> // подключаем библиотеку для ИК-приемника IRrecv irrecv(A0); // указываем пин, к которому подключен ИК-приемник decode_results results; // объявляем переменную для хранения полученных кодов void setup() { irrecv.enableIRIn(); // инициализируем прием инфракрасного сигнала Serial.begin(9600); // инициализируем монитор последовательного порта } void loop() // процедура loop { if (irrecv.decode(&results)) // если данные пришли выполняем команды { Serial.println(results.value); // отправляем полученные данные в порт irrecv.resume(); // принимаем следующий сигнал на ИК приемнике } }
Внимание! Если этот код не хочет работать, то у вас скорее всего отсутствует библиотека для работы с ИК-приёмником. Перейдите к занятию “Дистанционное управление” и установите библиотеку согласно инструкции.
БАЗОВЫЙ КОД ПРОГРАММЫ
#include <IRremote.h> // ИК модуль #include <Wire.h> // i2P шина #include <LiquidCrystal_I2C.h> // дисплей 1602 LiquidCrystal_I2C lcd(0x27,16,2); // Устанавливаем дисплей int AnimPlayer = 1; // анимация героя int GGpozX = 8; // позиция героя ось Х int GGpozY = 1; // позиция героя ось У int Emeny_check_1 = 0; // проверка злодея int Emeny_control = 0; // рандомим это число что бы были задержки между призывом кактуса int E1pozX = 16; // позиция злодея Х int E1pozY = 1; // позиция злодея У int HeartControl = 0; // контроль анимации сердечка int LifeCheck = 3; // количество жизней long Timer_z = 0; // счетчик очков long AnimatedTime = 300; // время обновления анимации героя long AnimatedTimeCheck = 0; // проверка вышеуказанного long HeartHitBig = 800; // время задержки сердца большое long HeartHitLight = 250; // время задержки сердца малое long HeartHitBigCheck = 0; // проверка большого удара сердца long HeartHitLightCheck = 0; // проверка малого удара сердца long BatteyBlink = 200; // время бликов батарейка когда осталось 1 ХП long BatteyBlinkCheck = 0; // проверка вышеуказанного long JumpUP = 800; // время нахождения в прыжке long JumpUPCheck = 0; // проверка вышеуказанного long DHTTimeR = 1000; //обновление секунда long DHTTimeRCheck = 0; //проверка обновление секунд long TimeBlink = 500; // обновление пол секунды long TimeBlinkCheck = 0; //проверка мигание настроек long currentMillis = 0; // главный счетчик времени bool JumpB = false; // прыжок или нет bool BatteryB = false; // батарейка пуста или почти bool hitON = false; // удар или нет decode_results results; // вывод результата данных с ИК IRrecv irrecv(A0); // порт аналоговый под ИК модуль enum { SYMBOL_HEIGHT = 8 }; byte Player_1[SYMBOL_HEIGHT] = {B01110,B01110,B00100,B01110,B10101,B00100,B01110,B01010,}; byte Player_2[SYMBOL_HEIGHT] = {B00000,B01110,B01110,B00100,B11111,B00100,B01110,B01010,}; byte Enemy_1[SYMBOL_HEIGHT] = {B00010,B00110,B10111,B10110,B11111,B00110,B00110,B11111,}; byte Heart_L[SYMBOL_HEIGHT] = {B00000,B01010,B11111,B11111,B11111,B01110,B00100,B00000,}; byte Heart_R[SYMBOL_HEIGHT] = {B00000,B00000,B01010,B11111,B01110,B00100,B00000,B00000,}; byte Battery1[SYMBOL_HEIGHT] = {B01110,B11111,B11111,B11111,B11111,B11111,B11111,B11111,}; byte Battery2[SYMBOL_HEIGHT] = {B01110,B10001,B10011,B10111,B11111,B11111,B11111,B11111,}; byte Battery3[SYMBOL_HEIGHT] = {B01110,B10001,B10001,B10001,B10011,B10111,B11111,B11111,}; byte Battery4[SYMBOL_HEIGHT] = {B01110,B10001,B10001,B10001,B10001,B10001,B10001,B11111,}; void setup() { Serial.begin(9600); // скорость порта irrecv.enableIRIn(); // запускаем прием lcd.init(); // запуск экрана Wire.begin(); // запуск ИК lcd.backlight();// Включаем подсветку дисплея } void loop() { currentMillis = millis(); // присваиваем миллисекунды IRCheck (); //запускаем функцию ИК модуля if (AnimPlayer < 3){ // если герой анимируется, значит он жив, значит работаем дальше if (LifeCheck == 3) {lcd.createChar(3, Battery1);} //показываем жизнь полная if (LifeCheck == 2) {lcd.createChar(3, Battery2);} //показываем жизнь средняя if (LifeCheck == 1) {//начинаем блинькать жизнью, когда 1 хп остался if (BatteryB == false){lcd.createChar(3, Battery3);} //показываем жизнь почти пусто if (BatteryB == true){lcd.createChar(3, Battery4);} //показываем жизнь пусто if (currentMillis - BatteyBlinkCheck >= BatteyBlink) {BatteyBlinkCheck = currentMillis; //проверка времени if (BatteryB == false) {BatteryB = true;} else {BatteryB = false;}} //меняем положение } timer(); //запуск таймера check_hit_gg_1 (); //запуск поверки урона PlAn(); //запуск анимации ГГ HeartHit (); // запуск анимации сердца enemy_go(); // запуск злодея } } void IRCheck () // функция ИК порта { if ( irrecv.decode( &results )) // если данные пришли, то { if (results.value == 0xFF18E7 && GGpozY == 1){ // если нажали кнопку « 2 » и позиция на «Земле» GGclear (); // очищаем прошлое местонахождение героя GGpozY = 0; // выставляем позицию на 2ю полосу (типа прыжок) JumpB = true; // переменная обозначающая что мы в фазе прыжка JumpUPCheck = currentMillis; // обнуляем счетчик проверки времянахождения в воздухе } // 2 if (results.value == 0xFF10EF && GGpozX >= 0){ // Если нажали на четверку и вы не упираетесь в левый край экрана GGclear (); // тоже что и до этого GGpozX -= 1; // перемещаем главного героя влево } // 4 if (results.value == 0xFF5AA5 && GGpozX <= 15){ //если нажали шесть и не упираемся в правую сторону экрана GGclear (); //должны были уже запомнить GGpozX += 1; // перемещаем вправо } // 6 if (results.value == 0xFF6897){ // 0 //если нажать ноль, то игра делает рестарт… lcd.clear(); // очищаем экран AnimPlayer = 1; //возвращаем переменную запуска анимации LifeCheck = 3; // восстанавливаем ХП Timer_z = 0; // обнуляем счетчик GGpozX = 8; // \/ \/ \/ GGpozY = 1; // возвращаем главного героя на центр экрана Emeny_check_1 = 0; // возвращаем данные генератора кактусов E1pozX = 16; // \/ \/ \/ E1pozY = 1; // выставляем стартовую позицию кактуса. } irrecv.resume(); // обнуляем данные датчика } } void timer () // функция очков { if (currentMillis - DHTTimeRCheck >= DHTTimeR) // таймер который срабатывает раз в секунду { DHTTimeRCheck = currentMillis; // обнуление таймера Timer_z ++; // плюсуем единицу к общей сумме lcd.setCursor(0, 0); // выставляем курсор на первую верхнюю точку lcd.print(Timer_z); // выводим данные на экран } } // я переписал этот кусок кода и он будет отличаться от первой части статьи(чуток). Это для меня нормально =) На работоспособность, это никак не влияет но лучше использовать новый вариант. void PlAn () // функция анимации главного героя { if (JumpB == true && GGpozY == 0){ // это контроль прыжка (активация прыжка будет в другой части кода) Если прыжок = правда И позиция нахождения наверху. if (currentMillis - JumpUPCheck >= JumpUP) { // проверка времени в полете 0.8f JumpB = false; GGclear (); GGpozY = 1; // когда время выходит, возвращаем нашего героя на землю и ждем следующего прыжка. Прыжок = лож; активация функции очищения местонахождения героя (); позиция вертикали главного героя = нижняя полоса; } } if (AnimPlayer == 1){lcd.createChar(0, Player_1);} //Если переменная контроля анимации один, записываем в ячейку памяти первый спрайт героя if (AnimPlayer == 2){lcd.createChar(0, Player_2);} //Если переменная контроля анимации два, записываем в ячейку памяти второй спрайт героя if (AnimPlayer < 2) // если переменная до двух, выставляем курсор на то место, где находиться наш главный герой и рисуем из памяти спрайт героя { lcd.setCursor(GGpozX, GGpozY); // выставили на позицию lcd.write(0); // отрисовали } if (currentMillis - AnimatedTimeCheck >= AnimatedTime) { // проверка времени AnimatedTimeCheck = currentMillis; // обнуление времен if (AnimPlayer == 2){AnimPlayer = 1;} // если переменная контроля анимации два то один else{AnimPlayer = 2;} // а если один, то два. } } void GGclear () // функция обновления героя { lcd.setCursor(GGpozX, GGpozY); // позиция героя lcd.print(" "); //очищаем его } void enemy_go () // функция злодея { if (Emeny_check_1 == 0) // что бы враг появлялся с случайно задержкой, нам надо сделать так что бы пока не было злодея на экране, у нас срабатывал рандом и при правильном числе, призывался злодей а этот рандом ждал пока он снова не понадобиться { Emeny_control = random (100); // я подумал, если мы задействуем разные методы, то это будет интереснее, по этому, высчитать шанс появления злодея, можно будет рандомом. if (Emeny_control == 1) { // если рандом = 1 из 100 то запускаем злодея. Emeny_check_1 = 1; // запускаем персонажа, по сути, тут можно использовать bool так как у нас два состояния, или да или нет, а мне сейчас переделывать было лень hitON = false; // эта функция проверяет был ли нанесен урон главному герою } } if (Emeny_check_1 == 1) // когда рандом одобрен, злодей пошел в бой { if (currentMillis - TimeBlinkCheck >= TimeBlink) //проверка временем 0.5f { TimeBlinkCheck = currentMillis; //обнуление проверки время lcd.createChar(2, Enemy_1); //присваиваем ячейке памяти 2 спрайт кактуса lcd.setCursor(E1pozX, E1pozY); //выбрали 1ю точку кактуса lcd.print(" "); //обнуляем ее E1pozX--; //перемещаем влево на один шаг lcd.setCursor(E1pozX, E1pozY); //выставляем 2ю позицию lcd.write(2); //отрисовываем кактус if (E1pozX <= 0) //если кактус дошел до края экрана { lcd.setCursor(0,1); //выставляем курсор на край экрана lcd.print(" "); //обнуляем его Emeny_control = 0; //обнуляем функцию рандома Emeny_check_1 = 0; //закрываем доступ к этой части скрипта E1pozX = 16; // - \/ \/ \/ E1pozY = 1; // - выставляем катус обратно на позицию Х и У } } } } void check_hit_gg_1 () //функция получения урона { if (E1pozX == GGpozX && E1pozY == GGpozY && hitON == false){ //проверка что координаты Х и Y совпали, что у героя, что у кактуса LifeCheck -= 1; // минусуем од ХП hitON = true; // возвращаем проверку и теперь наш герой снова может получать урон if (LifeCheck <= 0){ // если жизни меньше чем ноль AnimPlayer = 50; //отключаем срабатывание loop () Emeny_check_1 = 50; // отключаем случайное срабатывание кактуса lcd.clear(); //чистим экран lcd.setCursor(3, 0); //устанавливаем курсор lcd.print("GAME OVER"); // пишем заветное слово } } else { // НО! Если герой и кактус не сошлись в соитие то… lcd.setCursor(13, 0); // устанавливаем курсор и … lcd.write(1); // отображение сердечка lcd.setCursor(14, 0); lcd.print("="); // это просто равно lcd.setCursor(15, 0); lcd.write(3); // отображение батарейки } } void HeartHit () // функция анимации сердечка { if (HeartControl == 0 || HeartControl == 2){lcd.createChar(1, Heart_L);} //если наша переменная покажет ноль или два, мы в ячейку один записываем большое сердце if (HeartControl == 1 || HeartControl == 3){lcd.createChar(1, Heart_R);} //если наша переменная покажет один или два, мы в ячейку один записываем малое сердце if (currentMillis - HeartHitBigCheck >= HeartHitBig) { // время большого зависания удара if (currentMillis - HeartHitLightCheck >= HeartHitLight) { // время коротких ударов HeartHitLightCheck = currentMillis; // обнуление контроля времени коротких ударов if (HeartControl<3){HeartControl++;} // если переменная контроля отрисовки менее трех, то при каждом срабатывании скрипта мы плюсуем один к сумме else {HeartControl = 0; HeartHitBigCheck = currentMillis;} //но если сумма переменной превысила три, то обнуляем ее и обнуляем счетчик длительного залипания сердца } } }
ЗАДАНИЕ ДЛЯ САМОСТОЯТЕЛЬНОГО ВЫПОЛНЕНИЯ
Задание 1:
Дополните программу таким образом, чтобы вся статистика игры (счёт, жизни и т.д.) выводилась не только на экран, но в том числе и в последовательный порт.
Задание 2:
Добавьте в игру свои варианты препятствий и замените модельку игрового персонажа на собственную.
Задание 3*:
Добавьте в игру звуковое сопровождение (на нажатие каждой кнопки, на потерю жизни, на получение очков и т.д.). Если вы забыли, как работать с пьезодинамиком, посмотрите занятие “Музыкальная шкатулка“.
Задание 4*:
Объедините 2 игры: эту и предыдущую (Игра “Дракончик” в одном устройстве). Выбор игры может производиться с помощью кнопок на пульте ДУ, а запуск игры должен осуществляться тактовой кнопкой, которая есть на вашей макетной плате.