Игровая ТВ-приставка на Arduino. Часть 2

Приступим к созданию игры с условным названием «Арифметический сборщик». Игрок управляется джойстиком с возможностью перемещения по полю размером 128×90. При нулевых отклонениях джойстика игрок находится в центре. Максимальное отклонение джойстика соответствует максимальному перемещению игрока. С определенным интервалом времени генерируются объекты-цифры, которые движутся сверху вниз. По достижении нижнего положения экрана объект-цифра исчезает, уменьшая количество баллов игрока на величину данной цифры. Если игрок перехватывает объект-цифру на экране, то это приводит к исчезновению объекта-цифры и увеличению счетчика баллов игрока. С определенной периодичностью значок игрока меняет свое значение с цифры 0 до 9. При перехвате объекта-цифры счетчик баллов игрока увеличивается на сумму, равную сумме объекта-цифры и цифры игрока, если цифра игрока равняется цифре объекта-цифры, то счетчик баллов игрока увеличивается на произведение цифр. По достижении определенных порогов баллов, происходит переход на более высокий уровень игры, что приводит к увеличению скорости движения и скорости генерации объектов-цифр. Кроме того с 4 уровня игры столкновение игрока с объектом-цифрой приводит не только к увеличению счетчика игрока, но и уменьшению (если цифра игрока меньше цифры объекта-цифры). Если количество баллов игрока становится меньше 0, игра начинается сначала.

Вот видео того, что получилось

Скачать архив со скетчем и файлами библиотеки TVOut можно по ссылке

И сам процесс создания игры

Создание переменных игры

Для управления игрой создадим объекты для хранения текущего положения игры. Символ, отображающий игрока, и символы, отображающие объекты-цифры, выводятся как текстовая информация, поэтому поделим все поле игры на строки и все перемещения объектов будем производить как вывод символа в знакоместо на поле. Переменные MAX_X=31 и MAX_Y=14 определяют размер поля игры по количеству знакомест по горизонтали и вертикали. Переменная MAX_OBJ=30 определяет максимальное количество одновременно находящихся на поле игры объектов-цифр. Массив int FIGURA[30][3] хранит информации об объектах-цифрах, находящихся на поле игры следующим образом:

  • FIGURA[i][0]  – числовое значение объекта-цифры (0 – пустой объект);
  • FIGURA[i][1] – текущая координата x объекта-цифры;
  • FIGURA[i][2] – текущая координата y объекта-цифры.

Для хранения прочих переменных, описывающих текущее состояние игры, создадим структуру GAME. Список полей структуры:

  • xk – координата x(/4) игрока;
  • yk – координата y(/6) игрока;
  • tekCursor – текущее значение курсора;
  • blinkCursor – текущее состояние блинка курсора;
  • vblink – скорость blink в vk ;
  • vk – скорость движения игрока — проверка входов A0,A1;
  • vo_10 – скорость изменения цифры игрока;
  • vo_11 – скорость появления объектов-цифр;
  • vo_12 – скорость движения объектов-цифр;
  • count_objects – кол-во объектов-цифр на поле;
  • level – уровень игры;
  • balls – кол-во баллов.
 int MAX_X=31; int MAX_Y=14; // структура данных игры struct GAME // структура для данных игры {  int xk;   // координата x(/4) игрока  int yk;   // координата y(/6) игрока  int tekCursor; // текущее значение курсора  int blinkCursor; // текущее состояние блинка курсора  int vblink; // скорость blink в vk  long vk;   // скорость движения игрока - проверка входов A0,A1  long vo_10;   // скорость изменения цифры игрока  long vo_11;   // скорость появления цифр  long vo_12;   // скорость движения цифр  int count_objects; //кол-во объектов на поле  int level;   // уровень игры   int balls;   // кол-во баллов };  int MAX_OBJ=30; int FIGURA[30][3]={{0,0,0},{0,0,0},{0,0,0},{0,0,0},{0,0,0},    {0,0,0},{0,0,0},{0,0,0},{0,0,0},{0,0,0},    {0,0,0},{0,0,0},{0,0,0},{0,0,0},{0,0,0},    {0,0,0},{0,0,0},{0,0,0},{0,0,0},{0,0,0},    {0,0,0},{0,0,0},{0,0,0},{0,0,0},{0,0,0},    {0,0,0},{0,0,0},{0,0,0},{0,0,0},{0,0,0}    }; 

Управления положением игрока с помощью джойстика.

Положение игрока на экране определяется отклонением джойстика. Выводы джойстика подключены к аналоговым портам A0, A1 платы Arduino. Опрос портов происходит через время, определенное параметром GAME.vk, эти данные  обрабатываются функцией map(), которая пропорционально переносит значение из текущего диапазона 0-124 в новый диапазон (значения ширины и высоты экрана). Затем это значение переводится в координаты знакоместа. В это знакоместо необходимо переместить изображение символа игрока, предварительно поместив в предыдущее положение игрока символ пробела. Для отображения игрока используется мигающий символ – цифра и пробел. 

 //****************** установка нового положения игрока void set_iam()  {  TV.set_cursor(min(123,GAME1.xk*4),min(84,GAME1.yk*6));  TV.print(" ");  GAME1.xk=map(analogRead(A0), 0, 1024, 0, 128);  GAME1.yk=map(analogRead(A1), 0, 1024, 0, 96);  GAME1.xk=GAME1.xk/4;  GAME1.yk=GAME1.yk/6;  GAME1.vblink--;    if(GAME1.vblink<0)    {    GAME1.blinkCursor=1-GAME1.blinkCursor;    GAME1.vblink=5+GAME1.blinkCursor*5;    }  TV.set_cursor(min(123,GAME1.xk*4),min(84,GAME1.yk*6));  if(GAME1.blinkCursor==1)     TV.print(GAME1.tekCursor);  else     TV.print(" ");     } 

Символ, отображающий игрока меняется через время, определенное параметром GAME.vo_10, вызовом функции set_new_cursor(), изменение происходит случайным образом с помощью функции random(). 

 //****************** установка нового вида символа игрока void set_new_cursor()  {  GAME1.tekCursor=random(0,10);  } 

Генерация и перемещение объектов-цифр.

Объекты-цифры генерируются через время, определенное параметром GAME.vo11. Вызывается функция set_new_object(). Информация обо всех объектах-цифрах хранится в массиве FIGURA[30][3]. Программа ищет первый пустой индекс в массиве (FIGURA[30][3]=0), и помещает в него новый объект-цифру.  Цифровое значение и горизонтальная координата нового объекта генерируются функцией random, а вертикальная координата устанавливается равной нулю. Символ, отображающий новый объект-цифру, выводится на экран. 

 //****************** появление нового объекта-цифры void set_new_object()  {  int null_index=0;  if(GAME1.count_objects)<>    {    for(int i=0;i;i++)<>    {    if(FIGURA[i][0]==0)      {null_index=i;break;}    }    FIGURA[null_index][0]=random(1,9);    FIGURA[null_index][1]=random(0,MAX_X);    FIGURA[null_index][2]=0;    // вывод на доску    TV.set_cursor(FIGURA[null_index][1]*4,0);    TV.print(FIGURA[null_index][0]);       GAME1.count_objects++;    }  } 

Функция движении объектов-цифр (go_object()) вызывается через время, определенное параметром GAME.vo12. Визуализация движения объектов-цифр происходит путем стирания символа из предыдущего положения объекта(запись символа пробела), и записью символа объекта в новое положение (вертикальная координата знакоместа объекта-цифры увеличивается на единицу). По достижении низа экрана происходит удаление объекта-цифры (запись 0 в элемент массива FIGURA[i][0]), а также уменьшение количества баллов игрока. 

 //****************** движение объекта-цифры void go_object()  {  for(int i=0;i;i++)<>   {    if(FIGURA[i][0]>0)    {    TV.set_cursor(FIGURA[i][1]*4,FIGURA[i][2]*6);    TV.print(" ");       if(FIGURA[i][2])<>     {     FIGURA[i][2]++;     TV.set_cursor(FIGURA[i][1]*4,FIGURA[i][2]*6);     TV.print(FIGURA[i][0]);     }    else     {     TV.tone(294,200);     change_balls(FIGURA[i][0]*(-1));     FIGURA[i][0]=0;     GAME1.count_objects--;     }    }   }  }

Проверка столкновения игрока и объектов-цифр.

При перемещении символа игрока по экрану необходимо проверять столкновения игрока с объектами-цифрами. Для этого используем функцию collision(). После установки символа, соответствующего игроку, проверяем элементы массива FIGURA на соответствие координат объектов-цифр координатам символа игрока. При совпадении координат выполняем следующие действия:

  • уничтожается объект-цифра из массива FIGURA (запись 0 в FIGURA[[i][0]);
  • уменьшается на 1 счетчик количества объектов-цифр (GAME.count_objects)
  • производится изменение счетчика количества баллов игрока (вызов функции change_balls()).
 //****************** проверка столкновения void collision()  {  for(int i=0;i;i++)<>    {    if(FIGURA[i][0]>0)      {      if(FIGURA[i][1]==GAME1.xk && FIGURA[i][2]==GAME1.yk)        {        TV.tone(740,200);        if(FIGURA[i][0]==GAME1.tekCursor)          change_balls(GAME1.tekCursor*GAME1.tekCursor);        else if(FIGURA[i][0]>GAME1.tekCursor && GAME1.level>3)          change_balls(FIGURA[i][0]*(-1));        else          change_balls(FIGURA[i][0]+GAME1.tekCursor);           FIGURA[i][0]=0;        GAME1.count_objects--;        }      }    }  }

В функцию изменение счетчика количества баллов игрока (change_balls()) в качестве аргумента передается число, на которое следует увеличить (уменьшить) счетчик баллов игрока. Это число равно сумме объекта-цифры и цифры игрока. Если цифра игрока равняется цифре объекта-цифры, то передается значение, равное произведению цифр. Для повышения сложности игры с 4 уровня столкновение игрока с объектом-цифрой приводит не только к увеличению счетчика игрока, но и уменьшению (если цифра игрока меньше цифры объекта-цифры).

Счетчик баллов игрока.

Функция change_balls() производит изменение счетчика баллов игрока на величину входящего аргумента. Если счетчик количества баллов становится меньше 0, игра заканчивается, экран очищается и происходит перехов на начало игры. Кроме того, при достижении счетчиком определенных значений, происходит переход игры на новый, более высокий, уровень. Установку значений для нового уровня выполняет функция new_level(). 

 //****************** изменение набранных балов void change_balls(int ball)  {  GAME1.balls=GAME1.balls+ball;  if(GAME1.balls<0)    start_game();  if(GAME1.balls>(GAME1.level+1)*100)    new_level(GAME1.level);  set_data_tablo();   } 

Переход на новый уровень.

При переходе на новый уровень игры происходит установка новых значений для скорости генерации и скорости перемещения объектов-цифр. Первоначально предполагалось менять и значение скорости изменения цифры игрока, скорости движения игрока, но в ходе испытания игры решил от этого отказаться. Действия установки значений переменных для нового уровня игры производятся в функции new_level()
 

 //****************** изменение уровня игры void new_level(int tek_level)  {  GAME1.level++;  GAME1.vo_10=5000;  GAME1.vo_11=2000-(GAME1.level-1)*100;   GAME1.vo_12=1000-(GAME1.level-1)*100;     } 

Отображение данных игры на табло.

Табло с данными игрока находится внизу экрана. Здесь отображается текущие значения количество набранных баллов и уровень игры. При изменении счетчика баллов игрока происходит вызов функции set_data_tablo(), которое изменяет значение количества набранных баллов и уровень игры в переменных структуры GAME и на табло.
 

 //****************** вывод данных на табло void set_data_tablo()  {  TV.print(20,91," balls=   ");  TV.print(70,91," level=   ");  TV.print(48,91,GAME1.balls);  TV.print(98,91,GAME1.level);  } 

Звуковое сопровождение игры.

Для звукового оформления игры будем использовать функцию tone(frequency, duration) библиотеки TVOut. При достижении объектом-цифрой низа игрового поля – TV.tone(294,200) (функция go_object()), при столкновении игрока и объекта-цифры – TV.tone(740,200) (функция collision()). Если вы захотите в проект вставить фрагмент из последовательного воспроизведения нескольких нот, после вывода каждой ноты командой tone(frequency, duration) нобходимо вставлять командой delay() задержку длительностью не меньшей, чем параметр duration,6 – последовательное воспроизведении е двух но с паузой.

 TV.tone(294,200); TV.delay(400); TV.tone(740,200);

Основной цикл игры.

Основной цикл программы состоит из вызова рассмотренных нами функций set_iam(), collision(), go_object(), set_new_object(), set_new_cursor() через промежуток времени установленный в переменных GAME.vk, GAME.vo_10, GAME.vo_11, GAME.vo_12. 

 int game1()  {  while(GAME1.balls>0 && GAME1.level<6)    {     long time2=millis();    if(time2-time11>GAME1.vk)      {set_iam();      collision();      time11=time2;      }    if(time2-time12>GAME1.vo_12)      {go_object();time12=time2;}    if(time2-time13>GAME1.vo_11)      {set_new_object();time13=time2;}    if(time2-time14>GAME1.vo_10)      {set_new_cursor();time14=time2;}    TV.delay_frame(10);     }  if(GAME1.balls<0)    return 0;  else if(GAME1.level>5)    return 0;  else    return 1;     }

Добавляем меню для выбора игр.

Добавим в скетч меню для вывода трех игр , которые вы можно дописать самостоятельно. 

 void loop() {    switch(menu(pmenu))    {    case 1:start_game();    while(game1()>0);    break;    default:    break;    }  }  //***** меню для выбора игры int menu(int poz)  {    TV.clear_screen();    pmenu=max(poz,1);    int pmenu1=pmenu;    TV.println(60,30,"Game 1");    TV.println(60,50,"Game 2");    TV.println(60,70,"Game 3");    TV.draw_rect(50,5+20*pmenu,40,10,WHITE,INVERT);    TV.delay(500);    while(digitalRead(12)==LOW)    {    if(analogRead(A1)<100)      pmenu=max(pmenu-1,1);    else if(analogRead(A1)>900)      pmenu=min(pmenu+1,3);    else       ;    if(pmenu1!=pmenu)     {     TV.delay(500);     TV.draw_rect(50,5+20*pmenu1,40,10,BLACK,INVERT);     TV.draw_rect(50,5+20*pmenu,40,10,WHITE,INVERT);     pmenu1=pmenu;     }    }    return pmenu;   }