Проста ігрова тв-приставка на Arduino

Вступ
При світлі дня, а потім і у сні, виникла у мене ідея створення власної регламентованої тв-приставки. Власне, тут відкрився переді мною багатий і насичений світ радіотехніки. Так як раніше я не мав справи з серйозною розробкою електроніки, мій вибір припав на більш простий варіант — 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 > (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;}
}
}

Найскладніше позаду, можете успішно зітхнути.

На даний момент нам залишається тільки зробити систему підрахунку балів, таймер і рестарт.

Почнемо з таймера. Є змінна секунд 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 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();
}

Ось і все, друзі, ми повністю написали код гри. Вийшло досить цікаво і можна пограти.



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

Повний скриптВсього 218 рядків.
#include <TVout.h>
#include <fontALL.h>

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(!призупинено)
{
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, з двома геймпадами, в яку ми можемо пограти і навіть залипати.

Додаткові джерела
1. Інформація про бібліотеку
2. Інформація про порти підключення


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.