Arduino UNO R3 (Игра в пинг-понг делаем сами). Arduino UNO R3 (Игра в пинг-понг делаем сами) Проверка столкновения игрока и объектов-цифр



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

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

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

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

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

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

  • FIGURA[i] – числовое значение объекта-цифры (0 – пустой объект);
  • FIGURA[i] – текущая координата x объекта-цифры;
  • FIGURA[i] – текущая координата 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={{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. Программа ищет первый пустой индекс в массиве (FIGURA=0), и помещает в него новый объект-цифру. Цифровое значение и горизонтальная координата нового объекта генерируются функцией random, а вертикальная координата устанавливается равной нулю. Символ, отображающий новый объект-цифру, выводится на экран.

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

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

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

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

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

  • уничтожается объект-цифра из массива FIGURA (запись 0 в FIGURA[[i]);
  • уменьшается на 1 счетчик количества объектов-цифр (GAME.count_objects)
  • производится изменение счетчика количества баллов игрока (вызов функции change_balls()).

//****************** проверка столкновения void collision() { for(int i=0;i;i++)<> { if(FIGURA[i]>0) { if(FIGURA[i]==GAME1.xk && FIGURA[i]==GAME1.yk) { TV.tone(740,200); if(FIGURA[i]==GAME1.tekCursor) change_balls(GAME1.tekCursor*GAME1.tekCursor); else if(FIGURA[i]>GAME1.tekCursor && GAME1.level>3) change_balls(FIGURA[i]*(-1)); else change_balls(FIGURA[i]+GAME1.tekCursor); FIGURA[i]=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; }

При свете дня, а затем и во сне, возникла у меня идея создания собственной регламентированной тв-приставки. Собственно, тут-то открылся передо мной богатый и насыщенный мир радиотехники. Так как ранее я не имел дела с серьезной разработкой электроники, мой выбор пал на более простой вариант - Arduino и ее самая распространенная модель - Uno .

План работы

1. Разобраться с библиотекой
2. Спаять плату видео вывода
3. Написать код
4. Вырезать корпус

Финальная внешняя составляющая не особо важна в случае с подобными проектами.

Шаг 1. Разбираемся, что к чему

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

На сайте, посвященному проектам на Ардуино и вообще радиоэлектронике в целом (не реклама) нашел статью о подобной затее. Было решено использовать библиотеку TVout , так как приставка тв-шная. Для ее установки и работы пришлось немного пошаманить.

Необходимые функции библиотеки

Функции установки режима

Функция begin() инициализирует вывод видеосигнала (разрешение экрана по умолчанию 128x96).
Синтаксис:
TVOut.begin(mode);
TVOut.begin(mode, x, y);

Параметры:
mode – стандарт видеосигнала:
_PAL – режим PAL;
_NTSC – режим NTSC.
Возвращаемое значение:
0 – в случае удачного соединения, 4 – в случае неудачи (недостаточно памяти для буфера вывода).

Функции задержки

Функция delay() осуществляет задержку выведенного изображения.
Синтаксис:

TVOut.delay(ms);
Параметры:

ms – задержка в мс с точностью: 20 мс для PAL и 16 мс для NTSC.

Функция delay_frame() осуществляет задержку выведенного изображения.
Синтаксис:

TVOut.delay_frame(frames);
Параметры:

frames – количество кадров для задержки…
Функция полезна для сведения к минимуму или устранения на мерцание экрана, вызванные обновлением экрана.

Функции получения параметров

Функция hres() возвращает горизонтальное разрешение экрана.
Синтаксис:

TVOut.hres();
Параметры:

нет.
Возвращаемое значение:

unsigned char – горизонтальное разрешение экрана.

Функция vres() возвращает вертикальное разрешение экрана.
Синтаксис:

TVOut.vres();
Параметры:

нет.
Возвращаемое значение:

unsigned char – вертикальное разрешение экрана.

Функция char_line() возвращает максимально возможное количество символов в одной строке при выводе текстовой информации.
Синтаксис:

TVOut. char_line();
Параметры:

нет.
Возвращаемое значение:

unsigned char – количество символов.

Основные графические функции

Функция set_pixel() устанавливает цвет пикселя экрана в точке с заданными координатами.
Синтаксис:

TVOut.set_pixel(x,y,color);
Параметры:

x,y – координаты пикселя;
color – цвет пикселя:
0 – черный;
1 – белый;
2 – инвертировать цвет.
Функция get_pixel() получает цвет пикселя экрана из точки с заданными координатами.
Синтаксис:

TVOut.get_pixel(x,y);
Параметры:

x,y – координаты пикселя.
Возвращаемое значение:

color – цвет пикселя:
0 – черный;
1 – белый;
2 – инвертировать цвет.
Функция fill() заполняет экран заданным цветом.
Синтаксис:

TVOut.fill(color);
Параметры:

color – цвет заполнения:
0 – черный;
1 – белый;
2 – инвертировать цвет.
Функция clear_screen() очищает экран, заполняя заданным цветом.
Синтаксис:

TVOut.clear_screen(color);
Параметры:

color – цвет заполнения:
0 – черный;
1 – белый;
2 – инвертировать цвет.

Функция invert() инвертирует содержимое экрана.
Синтаксис:

TVOut.invert();
Параметры:

нет.
Функция shift_direction() сдвигает содержимое экрана.
Синтаксис:

TVOut.shift_direction(distance, direction);
Параметры:

distance – расстояние для сдвига содержимого экрана.
direction – направление сдвига:
UP=0 – вверх;
DOWN=1 – вниз;
LEFT=2 – влево;
RIGHT=3 – вправо.

Функция draw_line() соединяет на экране линией две точки.
Синтаксис:

TVOut.draw_line(x0,y0,x1,y1,color);
Параметры:

x0,y0 – координаты первой точки;
x1,y1 – координаты второй точки;
color – цвет заполнения:
0 – черный;
1 – белый;
2 – инвертировать цвет.
Функция draw_row() заполняет строку указанным цветом между двумя точками строки.
Синтаксис:

TVOut.draw_row(row,x0,x1,color);
Параметры:

row – вертикальная координата строки;
x1,x2 – горизонтальный координаты точек строки;
color – цвет заполнения:
0 – черный;
1 – белый;
2 – инвертировать цвет.
Функция draw_column() заполняет строку указанным цветом между двумя точками столбца.
Синтаксис:

TVOut.draw_column(column,y0,y1,color);
Параметры:

column – горизонтальная координата столбца;
y1,y2 – вертикальные координаты точек столбца;
color – цвет заполнения:
0 – черный;
1 – белый;
2 – инвертировать цвет.
Функция draw_rect() рисует на экране прямоугольник.
Синтаксис:

TVOut.draw_rect(x,y,w,h,color);
TVOut.draw_rect(x,y,w,h,color,fillcolor);

Параметры:

x,y – координаты левой верхней точки;
w,h – ширина и высота рисуемого прямоугольника;
color – цвет границ прямоугольника:
0 – черный;
1 – белый;
2 – инвертировать цвет.
fillcolor – цвет заполнения прямоугольника:
0 – черный;
1 – белый;
2 – инвертировать цвет.
Функция draw_circle() рисует на экране круг.
Синтаксис:

TVOut.draw_ circle(x,y,r,color);
TVOut.draw_ circle(x,y,r,color,fillcolor);

Параметры:

x,y – координаты центра круга;
r – радиус круга;
color – цвет границ круга:
0 – черный;
1 – белый;
2 – инвертировать цвет.
fillcolor – цвет заполнения круга:
0 – черный;
1 – белый;
2 – инвертировать цвет.
Функция bitmap() выводит на экран растровое изображение.
Синтаксис:

TVOut.bitmap(x,y,bmp,w,h);
Параметры:

x,y – координаты левого верхнего угла точки вывода;
bmp – указатель на массив памяти, где хранится картинка;
w,h – ширина, высота выводимого изображения;
Ниже рассмотрим процесс создания кода выводимых растровых изображений.

Функции вывода текстовой информации

Для применения функций вывода текстовой информации требуетя подключение файлов с включенными в библиотеку или пользовательскими шрифтами. Для подключения пользовательского набора шрифтов необходимо в скетче подключить заголовочный файл:
#include
В состав библиотеки включены следующие наборы шрифтов:

font4x6;
font6x8;
font8x8;
font8x8ext.
Функция select_font() выбирает шрифт для вывода текстовой информации.
Синтаксис:

TVOut.select_font(font);
Параметры:

font – шрифт, подключенный в скетче.

Функция print_char() выводит символ на экран.
Синтаксис:

TVOut.print_char(x,y,char);
Параметры:

x,y – позиция на экране для вывода символа;
char – символ из текущего шрифта.

Функция set_cursor() устанавливает позицию курсора для вывода текстовой информации на экран.
Синтаксис:

TVOut.set_cursor(x,y);
Параметры:

x,y – координаты для курсора.
Функция print() выводит на экран строку, символ или число.
Синтаксис:

TVOut.print(x,y,string);
TVOut.print(x,y,char,base);
TVOut.print(x,y,int,base).

Параметры:

x,y – координаты курсора.
base – формат вывода:
BYTE = 0;
DEC = 10 (default);
HEX = 16.

Функция println() выводит на экран строку, символ или число и в конце символ перевода строки:
Синтаксис:

TVOut.println(x,y,string);
TVOut.println(x,y,char,base);
TVOut.println(x,y,int,base).

Параметры:

x,y – координаты курсора.
base – формат вывода:
BYTE = 0;
DEC = 10 (default);
HEX = 16.

Функции вывода аудио

Функции вывода звука позволяют отправлять на телевизор через аудиовыход сигнал определенной частоты.
Функция tone() выдает аудиосигнал определенной частоты.
Синтаксис:

TVOut.tone(frequency,duration);
TVOut.tone(frequency).

Параметры:

frequency – частота аудиосигнала;
duration – длительность сигнала.
Функция noTone() прекращает выдачу аудиосигнала.
Синтаксис:

TVOut.noTone().

Шаг 2. Паяем видеовывод

В первую очередь нам нужно спаять некую плату для вывода видеосигнала через композитный av-выход (RCA). Паяем по следующей схеме:


Расположим два резистора номиналом 470 ом и 1к ом параллельно друг другу и припаяем к ним «плюс» от кабеля-тюльпана. Далее отведем от резистора в 470 ом провод в седьмой пин на Arduino, т.к. он отвечает за вывод видео (video ), а от резистора в 1к ом отведем провод в девятый пин, так как он отвечает за синхронизацию (sync ). А «минус» от кабеля-тюльпана в «землю» на Arduino. Подробнее (англ. )

Шаг 3. Пишем код (игру)

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

Начинаем с экрана приветствия , куда без него. Но тут встревает важный вопрос, как назвать сие чудо? Я пораскинул мозгами и придумал - Shimo . Звучит неплохо, даже технологично, по-китайски, конечно, но это не беда.

Начинаем. Чертим линию через середину экрана с помощью TV.draw_line(60,0,60,96,1); . Появляется шарик ровно в центре экрана. Напишем функцию его движения void ballmove(int vel, int angle) . Устанавливаем с помощью TV.set_pixel(x,y,1); , переменные я так и назвал.

Далее перед манипуляциями с шариком прописываем обновление экрана, а точнее, чтобы шарик не «наследил» на экране, поэтому при переходе на следующую позицию нужно закрашивать черным предыдущую. Для этого нам нужно прописать перед всем остальным TV.set_pixel(x,y,0); . После всех изменений переменных координат нужно прописать уже установку позиции и небольшую задержку - TV.delay(50); . Примерно вот так должно получиться:

Void ballmove(int vel, int angle) { TV.set_pixel(x,y,0); //Манипуляции с координатами TV.set_pixel(x,y,1); }

Теперь о самих изменениях координат. Всего восемь направлений (1-8), переменная int angle . А там уже просто, в зависимости от поворота, отнимаем или прибавляем к переменным какую-либо часть от int velocity . Я сделал так:

If(angle == 1) { y -= vel; } if(angle == 3) { x += vel; } if(angle == 5) { y += vel; } if(angle == 7) { x -= vel; } if(angle == 2) { x += round(vel/2); y -= round(vel/2); } if(angle == 4) { x += round(vel/2); y += round(vel/2); } if(angle == 6) { x -= round(vel/2); y += round(vel/2); } if(angle == 8) { x -= round(vel/2); y -= round(vel/2); }

Теперь движения ракеток. Здесь важное уточнение - я использовал только координаты по y , так как позиции ракеток по x не изменяются. Прописываем следующую функцию void racketsmove() . Далее рисуем ракетки, переменные int yb1 , int yb2 , TV.draw_line(10, yb1+8, 10, yb1-8, 1); и TV.draw_line(110, yb2+8, 110, yb2-8, 1); . Обновление экрана, то есть «без следа», аналогично случаю с шариком.

Управление ракетками производится с кнопок. Подключаем кнопки, пины 2 и 3 - первая ракетка, 4 и 5 - вторая ракетка. Проверяем нажатие кнопок и изменяем координаты.

Вот такая функция:

Void racketsmove() { TV.draw_line(10, yb1+8, 10, yb1-8, 0); TV.draw_line(110, yb2+8, 110, yb2-8, 0); if((yb1 - 8) > 1) { if(digitalRead(2) == HIGH) { yb1 -= 2;} } if((yb1 + 8) < 95) { if(digitalRead(3) == HIGH) {yb1 += 2;} } if((yb2 - 8) > 1) { if(digitalRead(4) == HIGH) {yb2 -= 2; } } if((yb2 + 8) < 95) { if(digitalRead(5) == HIGH) {yb2 += 2;} } TV.draw_line(10, yb1+8, 10, yb1-8, 1); TV.draw_line(110, yb2+8, 110, yb2-8, 1); }

Сейчас снова вернемся к ball . Теперь пропишем его коллизию и отталкивание от стен и ракеток. Функция - void ballcol() . Для этого просто проверяем его местонахождение относительно объектов, а потом и его угол. Затем этот угол изменяем на другой. С углом легко угадать.

Угол отражения равен углу падения

Можно сделать некоторые физические исключения для определенных зон ракеток.

Void ballcol() { if(x == 1 || x == 119 || (x == 10 && y < (yb1 + 3) && y > < (yb2 + 3) && y > < (yb1 - 3) && y > (yb1 - 8)) { a = 2; } if(x == 10 && y > (yb1 + 3) && y < (yb1 + 8)) { a = 4; } if(x == 110 && y < (yb2 - 3) && y > (yb2 - 8)) { a = 8; } if(x == 110 && y > (yb2 + 3) && y < (yb2 + 8)) { a = 6; } if(y == 95 || y == 1) { if(a==1){a=5;}else if(a==2){a=4;}else if(a==3){a=7;}else if(a==4){a=2;}else if(a==5){a=1;}else if(a==6){a=8;}else if(a==7){a=3;}else if(a==8){a=6;} } }

Самое сложное позади, можете успешно вздохнуть.

На данный момент нам остается только сделать систему подсчета баллов, таймер и рестарт.

Начнем с таймера. Есть переменная секунд float ts (в ней хранится абсолютно все время), переменная int tm (количество минут, которые мы получаем из ts ). Задаем значение tm операцией tm = ts/60; . И выводим значения на экран, TV.print(81,1,tm); TV.print(97,1,"."); TV.print(100,1,int(ts-(tm*60))); .

Продолжим. Функция рестарта, называем void restart() . Здесь мы возвращаем изначальные значения переменных.

Void restart() { TV.clear_screen(); x = 60; y = 48; yb1 = 48; yb2 = 48; a = 8; ts = 900.0; c1 = 0; c2 = 0; }

Финал, система подсчета баллов, она чересчур проста. Открываем гугл и вбиваем «Правила игры в настольные теннис». Ищем, за что очки даются. Находим часть про штрафы, а дальше мы успешно находим следующее: «Очко считается выигранным, если противник не успеет отразить правильно посланный ему мяч после первого отскока». Назревает вопрос, как отсчитывать удары и прочее?.. А удары и не нужно отсчитывать, ведь наш пинг-понг с двухмерной графикой.

Мы спокойно находим выход из положения и, как всегда, просто проверяем координаты относительно боковых стенок. Если происходит столкновение, то начисляем балл игроку на противоположной стороне поля. Функция - void ballscount() . Когда выйдет таймер - мы сравниваем баллы первого игрока (переменная int c1 ) и второго игрока (переменная int c2 ), объявляем победителя, делаем задержку и вызываем рестарт.

Void ballscount() { if(x == 1) { c2++; } if(x == 119) { c1++; } if(c1 > < c2 && ts == 0) { TV.println(10, 45, "Player 2 won!"); delay(10000); restart(); } else if(c1 == c2 && ts == 0) { TV.println(10, 45, "You are equal"); delay(10000); restart(); }

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


Для ленивых я просто напишу весь код.

Полный скрипт

Всего 218 строк. #include #include TVout TV; int x, y, a, c1, c2, yb1, yb2, tm, tsh, s; float ts; boolean paused = false; void setup () { TV.begin(NTSC, 120, 96); TV.clear_screen(); TV.select_font(font6x8); TV.println(0, 50, "Welcome to Shimo"); TV.delay (5000); TV.clear_screen(); x = 60; y = 48; yb1 = 48; yb2 = 48; a = 8; ts = 900.0; s = 2; } void loop () { if(!paused) { TV.draw_line(60,0,60,96,1); TV.select_font(font8x8); racketsmove(); ballscount(); TV.print(1,1,c1); TV.print(18,1,":"); TV.print(26,1,c2); tm = ts / 60; ts -= 0.04; if(ts < 0) { ts = 0; } TV.draw_rect(81,1,38,10,0,0); TV.print(81,1,tm); TV.print(97,1,"."); TV.print(100,1,int(ts-(tm*60))); ballcol(); /*if(ts < 600) { s = 4; } if(ts < 300) { s = 6; }*/ ballmove(s, a); TV.delay(50); if(digitalRead(6) == HIGH) { paused = true; delay(1000); } } else { TV.println(40,4,"pause"); if(digitalRead(6) == HIGH) { paused = false; delay(1000); TV.clear_screen(); } } } void ballscount() { if(x == 1) { c2++; } if(x == 119) { c1++; } if(c1 > c2 && ts == 0) { TV.println(10, 45, "Player 1 won!"); delay(10000); restart(); } else if(c1 < c2 && ts == 0) { TV.println(10, 45, "Player 2 won!"); delay(10000); restart(); } else if(c1 == c2 && ts == 0) { TV.println(10, 45, "You are equal"); delay(10000); restart(); } } void ballcol() { if(x == 1 || x == 119 || (x == 10 && y < (yb1 + 3) && y > (yb1 - 3)) || (x == 110 && y < (yb2 + 3) && y > (yb2 - 3))) { if(a==1){a=5;}else if(a==2){a=8;}else if(a==3){a=7;}else if(a==4){a=6;}else if(a==5){a=1;}else if(a==6){a=4;}else if(a==7){a=3;}else if(a==8){a=2;} } if(x == 10 && y < (yb1 - 3) && y > (yb1 - 8)) { a = 2; } if(x == 10 && y > (yb1 + 3) && y < (yb1 + 8)) { a = 4; } if(x == 110 && y < (yb2 - 3) && y > (yb2 - 8)) { a = 8; } if(x == 110 && y > (yb2 + 3) && y < (yb2 + 8)) { a = 6; } if(y == 95 || y == 1) { if(a==1){a=5;}else if(a==2){a=4;}else if(a==3){a=7;}else if(a==4){a=2;}else if(a==5){a=1;}else if(a==6){a=8;}else if(a==7){a=3;}else if(a==8){a=6;} } } void racketsmove() { TV.draw_line(10, yb1+8, 10, yb1-8, 0); TV.draw_line(110, yb2+8, 110, yb2-8, 0); if((yb1 - 8) > 1) { if(digitalRead(2) == HIGH) { yb1 -= 2; } } if((yb1 + 8) < 95) { if(digitalRead(3) == HIGH) { yb1 += 2; } } if((yb2 - 8) > 1) { if(digitalRead(4) == HIGH) { yb2 -= 2; } } if((yb2 + 8) < 95) { if(digitalRead(5) == HIGH) { yb2 += 2; } } TV.draw_line(10, yb1+8, 10, yb1-8, 1); TV.draw_line(110, yb2+8, 110, yb2-8, 1); } void ballmove(int vel, int angle) { TV.set_pixel(x,y,0); if(angle == 1) { y -= vel; } if(angle == 3) { x += vel; } if(angle == 5) { y += vel; } if(angle == 7) { x -= vel; } if(angle == 2) { x += round(vel/2); y -= round(vel/2); } if(angle == 4) { x += round(vel/2); y += round(vel/2); } if(angle == 6) { x -= round(vel/2); y += round(vel/2); } if(angle == 8) { x -= round(vel/2); y -= round(vel/2); } TV.set_pixel(x,y,1); } void restart() { TV.clear_screen(); x = 60; y = 48; yb1 = 48; yb2 = 48; a = 8; ts = 900.0; c1 = 0; c2 = 0; }

Шаг 4. Вырезаем корпус

Решил вырезать корпус на лазерном резаке (или фрезеровщике, я точно не знаю) из фанеры в 4mm. Нарисовал в InkScape, немного пошаманил и перевел в формат фрезеровщика.


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

Вывод

В процессе работы была создана простая игровая телевизионная игровая приставка на Arduino со стандартной игрой Ping Pong, с двумя геймпадами, в которую мы можем поиграть и даже залипать.

Привет, Гик Таймс!
Сегодня я поведаю вам одну не очень интересную историю о том, как создал простую игровую консоль на базе arduino и сделал несложную игру для нее в моем любимом игровом движке - Unity.

Игры

Вот уже почти четыре года я занимаюсь разработкой игр на популярном игровом движке Unity (ранее Unity3D). За это время я успел создать несколько небольших игр для мобильных устройств, а также объемный многопользовательский проект.
Это область для меня очень интересна и доставляет огромное удовольствие работать в ней.

Девайсы

Но иногда появляется желание попробовать что-то новое, и в один из таких дней я решил попробовать arduino.
Мне стало очень интересно, как создавать свои собственные устройства и как их программировать. Почему arduino? В интернете и даже на том-же хабре и гик таймс море постов о том, почему стоит брать arduino. Но отмечу, что для меня решающим фактором в выборе arduino является простота в использовании.

А как это объединить?

Однажды на просторах интернета я наткнулся на запись о том, как один из пользователей собрал свою простенькую игровую консоль на базе arduino, снабдив ее маленьким жк дисплеем 84x48 пикселей и написал на нее пару простых игр: понг и арканоид.
Эта разработка меня очень заинтересовала, и я решил создать свою версию игровой консоли на базе микроконтроллера atmega328.

Сама консоль

Первым делом я спроектировал и сделал ЛУТ-ом печатную плату для портативной консоли. Это было ошибкой - сначала надо было протестировать программу на отладочной плате, например arduino uno, так как я не предусмотрел возможность быстро и удобно заливать программы в контроллер. А еще я ошибся в самой схеме, это можно было исправить проводами, но все же обидно.
После того, как я понял, что ошибся, я подключил кнопки через плату к arduino uno, а экран я подключил напрямую.
Вот что вышло:



А теперь к играм

Для создания игр я решил использовать игровой движок Unity. Писать компилятор из Mono C# в программу для arduino я не стал, но решил написать набор скриптов с помощью которых можно легко собирать игры.
Все скрипты я разделил на 3 группы - действия, условия и комбайнеры.
Я думаю что назначение действий и условий объяснять не надо, а вот для чего нужен комбайнер я объясню. Комбайнер проверяет выполняется условие, а затем выполняет действие.
Из скриптов, а точнее их наличия и комбинаций алгоритм создает программу для arduino.

А что за игра?

Начать я решил с чего нибудь простого. А какая сама простая игра которую вы знаете? Правильно - понг. Но я решил сделать не совсем понг, а понг на одного - есть одна ракетка, мяч и стена, вместо второй ракетки.
Я собрал ее из написанного конструктора, скомпилировал и залил в контроллер. Работает!

А теперь приведем все в порядок

Когда игра запустилась, и я в нее наигрался, я решил, что стоит переделать плату, предать ей красивый вид, использовать smd компоненты и убрать все лишнее. Я переделал схему и сделал плату.
Вот что вышло



Исходники

Исходный код - очень простой.
Что делает Unity - в редакторе пользователь собирает игру из ui объектов, вешает на них скрипты действий, условий и комбайнеры.
Что делает компилятор (скрипт parser.cs) - он пробегает по всем объектам, смотрит на их скрипты и добавляет в текст файла build.ino куски кода, отвечающие за исполнение аналогов этих скриптов в микроконтроллере.

Ссылка на исходники - drive.google.com/open?id=0B5INc3_98cSJMEMxZmlWUTh1Ukk
Компиляция производится при запуске игры. и сохраняется в папку Assets/build/text/builded.ino
Этот скетч и надо заливать в контроллер.

В заключении

Хочу сказать, что это было очень интересно.
Я получил возможность совместить 2 своих любых занятия - разработка игр и создание девайсов.
Надеюсь вас это тоже заинтересовало, и теперь вы тоже сделаете свой крутой девайс =)

И так, друзья мои, если у вас уже есть набор конструктор Arduino UNO R3 (стартовый набор) или вам стало скучно, с точки зрения техники — пожалуйста. У нас есть целая серия «уроков» для Ардуино. Хочу напомнить, что из этого набора можно сделать если не всё, то почти всё что будет работать или чем-то управлять. Игровая консоль, ну считайте джойстик для игры в пинг-понг и те только. У кого-то остался старый компьютер со старым не работающим джойстиком, теперь, всё это логика, которая управляется через Ардуино Кит (http://arduinokit.ru/).

С чего начать делать проект?

Я создал простую игровую приставку, которая может играть в пинг-понг и другие игры, помните как на стареньком компе Atari 2600, которой сейчас легко может эмулировать Arduino R3. Ардуино стал бы как тот старый компьютер, считывая входы и выходы данных с джойстика подключенного к телевизору через стандартную библиотеку TVout (Для Ардуино). Я создал этот проект только для того, что бы продемонстрировать, что любой (без особых навыков) может создавать свои собственные миниатюрные консоли и играть. Также я хотел бы что все кто прочитал эту статью смогут пользоваться этим советом совершенно бесплатно.

Что нужно для игровой приставки на Arduino?

5. Теперь, для игры в пинг-понг, нам нужно только 4-контактный разъём: Вперед (1) , Назад (2) , Кнопка (6) , и Земля (8). А для 2-го джойстика, нам нужно только контакты 1, 2 и 8 (Кнопка «главный» используется только, чтобы «начать игру» от имени Администратора, так вот почему только 1 игрок должен иметь эту кнопку — управлять) .

6. Если посмотреть на контакты сбоку, то я подключил эти 4 запайки с простыми проводами, которые идут к Arduino («Вперед» к выводу 2 , «Назад» к контакту 3, и в «Управление» к контакту 4). Я проделал то же самое для других 3 контактов для игрока 2 (на второй Джойстик): «Вперед» с контактом 7 и «Назад» к контакту 10). Не забудьте подключить оба джойстиков Контакт 8 на землю (экранный провод)!

7. Теперь, загрузите игру Ping-pong и проверить входы джойстика. Вставьте Arduino UNO R3 и все провода в необходимые разъёмы (я использовал сломанный внешний корпус старого жесткого диска) .

Когда я впервые попробовать сам сделать эту задачу, я сильно переживал, чтобы начать работу над этим проектом. Фуф было не легко, хотел завершить этот проект как можно скорее. Изначально моя главная проблема, то что я пытался реализовать старые джойстики от Атари к Arduino. Из-за этого некоторые интернет-блоггеры не правильно написали на своих форумах, что мол типа входы невозможно прочитать или припаять изнутри джойстика, однако глядя на это было весело использовать их стандартные контроллеры подсоединившись к ним из-вне, я почесал репу и много выслушал высказываний в свой адрес, мол «руки у меня не оттуда растут» или я паяльник не умею держать в руках. Тем не менее — мой проект работает! А вам слабо?

PS: Я думаю, что единственное, что я бы пересмотрел теперь по другому — хм. Ну например бы взял картридж от Сеги или Денди и сделал бы некий карт-ридер. Вот вам и телевизионная игровая приставка своими руками. Главное, что мой проект масштабирован, можно делать если не летающих роботов, так игровые приставки своими руками. Кстати говоря, в у наших друзей есть различные компоненты для Arduino UNO R3 (Конструкторы Ардуино). Если сильно захотеть, то всё можно сделать своими руками. И это работает!