ЧАСТЬ #2 от начала до конца


Продолжаем делать игру на arduino и в дальнейшем всунем эту игру в программу, которую я делаю для машины и на наших полученных знаниях создадим вторую игру, для забавы ради и сделаем правильную музыку для нее.

Что бы понять, что нам сейчас нужно писать, мы должны составить план, что же мы будем иметь и в каком виде. У нас есть главный герой, у него два кадра анимации и это мы уже в прошлом уроке уже разобрали. Делать ему изменение спрайта прыжка я не стал, не потому что мне лень, просто не вижу пока что в этом смысла. Далее у нас должно быть обозначение жизни, делать просто сердечки в ряд не интересно, и цифры тоже банально, давайте сделаем три очка жизни и обозначим их, например уровнем заряда батарейки. И для радости глаза сердечко, которое будет биться возле этой батарейки. Количество очков за раунд, установим слева экрана, чисто в цифровом виде и враг у нас будет злой кактус, для начала пойдет.

С объектами мы определились и давайте их отрисуем, сразу все спрайты и запишем их.





Рабочие спрайты у нас есть и мы уже можем представить как будет выглядеть это на экране, следуя первому уроку мы выпишем их в двоичной системе, помним, где ноль там пусто, а где единица, у нас горит пиксель. Приступим:

"--------------------------------------------------------------------------"

//Главный герой первый спрайт:
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,};
"--------------------------------------------------------------------------"

Теперь у нас есть спрайты, и пришло время это все оживлять. Для начала подумаем какие дополнительные функции нам нужны. Зная, что arduino работает не идеально и местами даже очень капризно, мы начинаем стараться максимально упростить этому микроконтроллеру жизнь. Не перегружать и одновременно требовать полной отдачи. Так что выведем такие дополнительные функции, которые будут жить своей жизнью и делать все что нам надо:

— анимация сердца
— анимация героя
— проверка урона по герою
— движение злого кактуса
— начисление очков (пока что ежесекундно +1, дальше поменяем)
— обновление экрана (но это не точно, скорее всего, мы уберем эту функцию и добавим другую, мне не понравилось, что экран моргает, хочется стабильности). В последствии заменим эту функцию на удаление прошлого местоположения героя, это уберет пративное мерцание экрана, а обнуления злодея, будет внутри злого скрипта, думаю там одна или две строчки максимум будет.
— управление пультом
— настройки loop и setup

Мы хотим, что бы у нас была анимация биения сердца. Выводя ее в отдельную функцию и заставив жить своей отдельной жизнью нам проще будет отслеживать работу и в дальнейшем проще будет редактировать, так как у нас все, ну или почти все находиться в одном месте. Этот код, можно было бы вывести в loop () и закомментировать, но я, лично, привык разбивать на отдельные функции, ты не ищешь код во всем списке и знаешь, что отдельная функция, контролирует отдельные элементы нашей игры.

Сейчас, я буду писать отрезки кода, мы их соединим в конце и получиться полноценный скрипт, я вам сейчас объясню суть да идею, а потом соберем пазл и будем наслаждаться результатом.
Перед началом самого кода, объясню парочку моментов.

lcd.createChar // это команда обращения к экрану и задействования одной из ячеек памяти для записи новых символов. В скобочках записывается номер ячейки и через запятую, название переменной с информацией.

Контроль за отрисовкой будем вести через цифровую переменную, используя четыре цифры, для правильности анимации, если бы мы хотели сделать так что бы сердце просто билось туда-сюда то нам и подошла бы обычная переменная bool. Но у меня мысль другая, один большой удар и два коротких, так будет смотреться интереснее.

"--------------------------------------------------------------------------"
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;} //но если сумма переменной превысила три, то обнуляем ее и обнуляем счетчик длительного залипания сердца
    } 
  }
}
"--------------------------------------------------------------------------"

Еще раз, хочу сконцентрировать ваше внимание на этом коде:
lcd.createChar(x, y); присваивание в ячейку пямяти « x » от (0…7) данные для отображения на экране « y »

Идем дальше =)

Теперь, у нас есть код, который создает эффект интересного биения сердечка, ничего полезного оно не делает, только понты =)

Далее основываясь на этом, создадим анимацию нашего героя, есть огромные плюсы, чем дальше мы погружаемся в эту статью, тем больше вы узнаете мой стиль мышления, а мне, уже меньше надо вам объяснять и больше акцентировать внимание на коде и построении идеи. Я буду стараться больше объяснять в комментариях скрипта, что бы меньше писать вне скрипта.
Так что приступим к главному герою и создадим еще одну функцию для этого:

"--------------------------------------------------------------------------"
// я переписал этот кусок кода и он будет отличаться от первой части статьи(чуток). Это для меня нормально =) На работоспособность, это никак не влияет но лучше использовать новый вариант.
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 timer () // функция очков
{
  if (currentMillis - DHTTimeRCheck >= DHTTimeR)  // таймер который срабатывает раз в секунду
  {
    DHTTimeRCheck = currentMillis; // обнуление таймера
    Timer_z ++; // плюсуем единицу к общей сумме
    lcd.setCursor(0, 0);  // выставляем курсор на первую верхнюю точку
    lcd.print(Timer_z); // выводим данные на экран
  }
}
--------------------------------------------------------------------------"

Вот и все. Чем дальше тем легче.

Теперь, осталось отработать нашего кактуса.

Его задача простая, появиться, прошагать весь путь справа на левое и попытаться соприкоснутся с героем, что бы нанести урон. С уроном все проще, one touch – one hit. Пока что увеличения сложности, делать мы не будем. Пусть кактус двигается со скоростью 0.5f (сложность это уже будет ваше домашнее задание =)) ну или русским языком пол секунды – один шаг.

Давайте глянем, как будет смотреться, этот кусок кода:

"--------------------------------------------------------------------------"
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);  // отображение батарейки
  }
}
"--------------------------------------------------------------------------"

Этот код очень простой, основная его функция, это проверять, и ждать пока условия все будут выполнены, что бы остановить игру и сообщить нам, что мы проиграли.

Теперь последняя функция, это управление. По сути, если мы разобрали код выше, то для нас он покажется проще простого. С первого урока, мы вытянули коды пульта, у меня выписаны они все, выглядят они вот так:

* CH- 0xFFA25D
* CH 0xFF629D
* CH+ 0xFFE21D
* << 0xFF22DD
* >> 0xFF02FD
* >|| 0xFFC23D
* - 0xFFE01F
* + 0xFFA857
* EQ 0xFF906F
* 0 0xFF6897
* 100+ 0xFF9867
* 200+ 0xFFB04F
* 1 0xFF30CF
* 2 0xFF18E7
* 3 0xFF7A85
* 4 0xFF10EF
* 5 0xFF38C7
* 6 0xFF5AA5
* 7 0xFF42BD
* 8 0xFF4AB5
* 9 0xFF52AD


Кнопка _ код (внимание!) (коды на пультах могут отличаться)

Кто не читал, советую прочитать 1ю часть.

Что-то подобное, потом будет и у вас, и вы сможете без проблем настраивать все, что вам нужно.

Теперь мы создадим простейший алгоритм, основываясь на том, что уже знаем и наша игра, оживет.

"--------------------------------------------------------------------------"
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(); // обнуляем данные датчика
}
"--------------------------------------------------------------------------"

Вывод от выше написанного кода:
кнопка 2 это прыжок
кнопка 4 это шаг влево
кнопка 6 это шаг вправо
кнопка 0 обнуляет игру и перезапускает ее

Вот, теперь осталось нам настроить setup & loop, уже все идет к концу. Мы создали все дополнительные функции и нам осталось только все это склеить и добавить библиотеки. Я думаю, мы рассмотрим переменные и главные настройки setup\loop уже в общем коде, так что приступим, а потом от вас требуется ctrl + c & ctrl + v и все =) и дальнейшее самостоятельное развитие в этом направлении, если вам конечно это все понравилось.

"--------------------------------------------------------------------------"

#include <IRremote.h> // ИК модуль
#include <Wire.h>  // i2P шина
#include <LiquidCrystal_I2C.h> // дисплей 1602
LiquidCrystal_I2C lcd(0x3F,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;} //но если сумма переменной превысила три, то обнуляем ее и обнуляем счетчик длительного залипания сердца
    } 
  }
}
"--------------------------------------------------------------------------"

По подключению. Код та мы сделали, а вот как собрать проводочки и что да куда втыкать, вот это я вам не объяснил. Хотя я и так уверен что более 80% тех, кто решит почитать эту статейку и так уже это знают, но как по мне, статья будет не полная, если я не предоставлю максимум информации.



A5 Дисплей 1602 — SCL
A4 Дисплей 1602 — SDA
A0 ИК датчик

Сейчас я занимаюсь над созданием радио ключа для машины на основе arduino, а на экране 1602 хочу вывести данные датчика влаги и температуры снаружи машины (так как я скоро буду в Москве, искать себе новую работу, мне надо будет знать что твориться за бортом машины так как там очень холодно), часы, вольтметр аккумулятора и туда-же поставить игры (у меня осталось 4 пина свободных, а мне надо еще стооолько туда впихнуть), что бы можно было на светофоре потыкать в эту игру или во вторую, которую я планирую сделать как гоночки старых добрых карманных приставок из 2000х где суть была просто объезжать препятствие и музыку поставить на фон из Rock n Roll Racing. Ухх классика =) и всунуть оптимально это все в одну arduino не используя внешние хранилища данных(чисто челендж), но я уже задействовал ~60 % и если код игры займет максимум 15% — 20%, то музыка… ох…там с этим будут проблемы, весит много, надо оптимизировать, я уже начал собирать трек и умудрился уменьшить вес почти в 10 раз, но я допустил ошибки в построении нот и тонов и теперь мне придется делать все заново. Я скорее всего потом напишу сюда полный вариант с описанием проекта, над которым я сейчас работаю. Надеюсь кому-то что-то новое для размышления я предоставил, если будет все хорошо и время, я продолжу писать статьи. Будут вопросы? Пишите в комментариях.

Все, подписывайтесь, ставьте лайки, следите за новыми выходами статей.

Всем спасибо за внимание, чао-какао!

P.S.: работоспособность игры снять на видео я пока что не могу, умерла камера на телефоне и это очень печально. Но я что-то придумаю и добавлю чуть позже видос в статью.

Первая часть статейки ->

Комментарии (9)


  1. shiru8bit
    04.10.2018 22:37
    +1

    Как будет воспроизводиться музыка, насколько сложный синтезатор в планах? Полифония? Просто квадратная волна без громкости, или что-нибудь поинтереснее?


    1. Asmodroid Автор
      04.10.2018 23:46

      На данный момент времени, у меня в голове только один выход как всунуть большой трек не съедая почти всю память микроконтроллера, это сделать группы массивов для полифонии и правильное обращение к ним. Я так уже уменьшил размер музыки до очень малого размера, НО мне было скучно и я решил вручную переписать тон и задержку, и получил в результате ошибочные данные. Так что я пропишу код, что бы ардуино сам создал в логах правильные группы массивов для дальнейшего их использования. Вот такие у меня планы, если у вас есть предложение получше, я с удовольствием его выслушаю и опробую.


      1. shiru8bit
        05.10.2018 00:02
        +2

        Потенциально предложений масса, но пока не понятно, что именно в планах. На Arduino по звуку можно сделать очень много чего, я бы сказал — в рамках чиптюна возможности безграничны.

        Если у вас главная сложность не в синтезе интересного звука, а в сжатии данных — очень хорошо себя зарекомендовала схема, где каналы хранятся отдельно группами по 8 нот, и для каждой следующей группы в выходные данные или добавляется фрагмент (паттерн) с новой уникальной комбинацией 8 нот, или находится в уже добавленных данных полностью совпадающий фрагмент, и на него делается ссылка (одноуровневый CALL). По сути очень примитивный вариант LZ.

        Размер группы в 8 нот (четверть такта, если вы считаете в музыкальных длительностях, а не кадрах) выявился статистически, иногда выгоднее 16, но разница не очень значительна, а другие размеры как правило заметно проигрывают. Я обычно делаю кодировщики так, чтобы они сами перебирали все возможные размеры блоков и находили самый выгодный для каждого конкретного трека брутфорсом, благо на современных компьютерах это занимает секунду.


        1. Asmodroid Автор
          05.10.2018 00:17

          Интересно, очень хорошее предложение, я как-то даже и не подумал насчет этого, а ведь треки старой доброй «Rock n Roll Racing», действительно имеют много повторений и зачем мне их дублировать, если я могу просто воспроизвести их нужное мне количество раз. Вы действительно помогли, и как только я дойду до этого этапа, надо будет обязательно попробовать. Супер! Спасибо :)


        1. StroboNights
          05.10.2018 03:26
          +1

          Asmodroid, «как всунуть большой трек не съедая почти всю память микроконтроллера» — у AVR на борту есть встроенная EEPROM (у Atmega8 это целых 512 байт). Библиотека Arduino для работы со встроенной EEPROM наверняка существует, попробуйте поискать. Ну и как самый простой вариант, Вы можете попробовать написать свой «секвенсор». Например, количество нот и, соответственно, частоту каждой из нот определить заранее; в то время как «секвенсор» будет принимать на вход номер ноты, длительность ноты и длительность паузы — при желании и грамотном подходе эту информацию можно уместить в байт. Прием этот древний, но эффективный. Успехов Вам!
          Здравствуйте, shiru8bit. Уже не один раз с интересом просматривал я Вашу потрясающую статью (AONDEMO), а также всегда с удовольствием читаю Ваши комментарии — профессиональные и всегда по делу. Не теряю надежду на новую статью от Вас. С уважением.


        1. Asmodroid Автор
          06.10.2018 10:35

          Ахаха, рассказал другу про идею разбить музыку по тактам и повторять одинаковые отрезки для оптимизации, меня прям пристыдили, сказав что «Почему ты сам до этого не догадался? У тебя же музыкальное образование» вот так, отучился, получил диплом и забыл всю теорию )))


          1. shiru8bit
            06.10.2018 10:44

            Разбиение на повторяющиеся фрагменты вообще типично для игровых музыкальных форматов. Но для большей эффективности важны несколько упомянутых мной моментов, раскрою их подробнее:

            — Разбивать именно на такты, т.к. если просто взять поток нот, типа MIDI, где нет информации о тактах, и пробовать кодировать в блоки произвольного размера, повторяемость в них будет меньше, а в тактах это происходит натуральным образом.

            — Кодировать поканально, а не все каналы одновременно, т.к. в пределах канала повторяемость гораздо выше, чем у комбинации каналов. Вроде очевидная идея, однако же в таких популярных форматах, как MOD/XM и им подобных кодируют каналы одновременно (сначала не подумали, потом традиции).

            — Кодировать не двумя структурами типа 'линейный список тактов и данные тактов' (так называемый ордер-лист), а обратными ссылками в данных тактов. Это упрощает код плеера, и эффективно, т.к. повторения в среднем бывают реже, нет смысла хранить список типа 0 1 2 3 4 5 6 7.


  1. ay8910
    04.10.2018 22:53
    +2

    Схемы ардуинщиков, состоящие из нарезанных фотографий бредбордов, плат, и разноцветных проводов, нарисованных с машиностроительной точностью просто не перестают удивлять… )


    1. Asmodroid Автор
      04.10.2018 23:39

      Та эти схемы делает программа (ее название внизу справа изображения) которая имеет в себе все эти нарезанные фотографии, ты их только выставляешь и соединяешь там же проводками и все :) я мог бы сделать стандартную схему, но смысла не вижу. В проекте для авто, там уже не прокатит эта прога так как много внешних устройств, автономные блоки питания, реле, стабилизаторы напряжения и т.д.