Pull to refresh

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

Reading time 11 min
Views 60K

Вступление


При свете дня, а затем и во сне, возникла у меня идея создания собственной регламентированной тв-приставки. Собственно, тут-то открылся передо мной богатый и насыщенный мир радиотехники. Так как ранее я не имел дела с серьезной разработкой электроники, мой выбор пал на более простой вариант — 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(!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, с двумя геймпадами, в которую мы можем поиграть и даже залипать.

Дополнительные источники и Примечания



1. Информация про библиотеку
2. Информация про порты подключения
3. По некоторым просьбам, изображение обложки было сжато.
Tags:
Hubs:
+25
Comments 30
Comments Comments 30

Articles