Ігрова приставка на Ардуїно. 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(" "); , 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); )==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 і зробив нескладну гру для неї в моєму улюбленому ігровому движку 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? Правда правда. З екранчиком, кнопками та іграми. І це не фантазія. Існувало (і існує) кілька проектів, які спробували це зробити і один із них (Gamebuino) ми і спробуємо самостійно реалізувати.

Так як проект відкритий, то вся схематика доступна на Вікі, як і посилання на ПЗ, ігри та інша корисна інформація.

Для створення своєї ігрової консолі нам знадобляться недорогі компоненти – екран Nokia 5110, шість кнопок та п'єзодинамік. Все це легко знаходиться на Aliexpress або інших сайтах китайських магазинів (переважно проблема знайти екран) і навіть у Росії. Екран можна назвати «масовим» і добре описаний, тому підключити його також не проблематично, як і дістати.

Так як Gamebuino використовує розведення зручну для розміщення елементів на платі, то при спробі підключити елементи через стандартні піни Arduino ми отримуємо таку картину:

Насправді частина елементів вже була «втрачена», тому що в Gamebuino використовуються контакти мікропроцесора, які не розведені на Arduino або просто не потрібні. В "утиль" пішли датчик заряду акумулятора, кнопка C і SD-карта (вони взаємопов'язані) і датчик зовнішнього освітлення. Також вирішено було забрати резистори - ми не збираємося використовувати дисплей в режимі 24/7 і можна обійтися без цього захисту.

Можна було б зробити «красиво», але тоді довелося б виправляти код усіх бібліотек та «мепіти» контакти за новою схемою. Я вирішив обмежитися лише косметикою – тобто використовувати «стандартні» контакти і лише за необхідністю внести невеликі редагування в код.

Отже - після складання схеми необхідно встановити IDE Arduino і завантажити ось цей архів. Його необхідно розархівувати в директорію Документи/Arduino та після цього запустити Arduino IDE. До цього архіву вже внесено зміни до коду бібліотек і він «з коробки» працюватиме з даною схемою.

У вас мають з'явитись приклади Gamebuino, які дозволять перевірити дисплей, кнопки тощо. і навіть запустити Pong. Для плати використовуйте стандартну Arduino UNO або Leonardo (на інших поки не тестував).

Коли у вас все запрацювало, рухається, пищить тощо. виникає закономірне питання – а де ж ігри? Gamebuino використовує свій завантажувач для роботи з SD-картами, тому завантажити HEX через їх loader не вийде без перепрошивки самої Arduino. Але навіть якщо ви це зробите, то через відсутність сигналу з акумулятора ви нічого не зможете запустити, так як прошивка буде лаятися і вимикатися. Прекомпільовані HEX ​​з іграми теж не запустити через проблеми з датчиком заряду акумулятора.

Але не все втрачено – можна завантажити вихідні коди, відкрити їх в Arduino IDE та «залити» в нашу консоль.

Я перевірив частину ігор і вони… ПРАЦЮЮТЬ! Ви можете завантажити архів з вихідними кодами ігор звідси, відкрити їх в Arduino IDE та скомпілювати та завантажити самостійно. Інші ігри ви можете самі випробувати, завантаживши вихідні коди з Вікі Gamebuino.

Отже – місія виконана! Ми отримали мініатюрну ігрову консоль. Тепер наше завдання зробити її мобільним:) І це спробуємо зробити у наступних статтях.

І так, мої друзі, якщо у вас вже є набір конструктор 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 (Конструктори Ардуїно). Якщо дуже захотіти, то все можна зробити своїми руками. І це працює!

При світлі дня, а потім і уві сні, виникла у мене ідея створення власної регламентованої телевізійної приставки. Власне, тут відкрився переді мною багатий і насичений світ радіотехніки. Так як раніше я не мав справи із серйозною розробкою електроніки, мій вибір ліг на більш простий варіант - 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); 2);y += round(vel/2); ) if(angle == 6) ( x -= round(vel/2); y += round(vel/2); ( 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.clear_screen(); x = 60; y = 48; yb1 = 48; yb2 = 48; a = 8; ts = 900.0; s = 2; ) void loop () ( if(! (60,0,60,96,1); TV.select_font(font8x8); racketsmove(); ballscount(); TV.print(1,1,c1); ;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 з двома геймпадами, в яку ми можемо пограти і навіть залипати.