- PVSM.RU - https://www.pvsm.ru -
На игровых выставках разработчики Objects in Space [1] показывали демо своей игры с контроллером на кокпите огромного космического корабля. Он был дополнен загорающимися кнопками, аналоговыми приборами, световыми индикаторами состояния, переключателями и т.д… Это сильно влияет на погружение в игру:
На сайте игры выложен туториал по Arduino [2] с описанием коммуникационного протокола [3] для подобных контроллеров.
Я хочу создать то же самое для своей игры
В этом примере я потрачу примерно 40 долларов, чтобы добавить красивые, большие и тяжёлые переключатели на кокпит симулятора гонок. Основные затраты связаны с этими самыми переключателями — если бы я использовать простые переключатели/кнопки, то цена была в два раза ниже! Это настоящее оборудование, способное выдерживать 240 Вт мощности, а я буду пускать по ним только примерно 0,03 Вт.
Предупреждение: я решил сэкономить, поэтому оставляю ссылку на дешёвый китайский веб-сайт, где закупаю кучу разных компонентов/инструментов. Один из недостатков покупки компонентов по дешёвке заключается в том, что часто у них нет никакой документации, поэтому в статье я решу и эту проблему.
Я изучал электронику в старшей школе, научился пользоваться паяльником, узнал, что красные провода нужно соединять с красными, а чёрные с чёрными… Вольты, амперы, сопротивление и связывающие их уравнения — вот и всё, чем исчерпывалось моё формальное обучение электронике.
Для меня это был обучающий проект, поэтому в нём могут быть плохие советы или ошибки!
Как сказано выше, я покупаю дешёвые детали у розничного продавца с низкой маржей, поэтому первым делом нужно разобраться, как работают эти переключатели/кнопки.
С кнопкой всё просто — в ней нет светодиодов и всего два контакта. Переключаем мультиметр в режим непрерывности/прозвонки () и касаемся щупами разных контактов — на экране будет отображаться OL (open loop, разомкнутая цепь): это означает, что между двумя щупами нет соединения. Затем нажимаем на кнопку, по-прежнему касаясь щупами контактов — на экране теперь должно отобразиться что-то типа 0.1Ω и мультиметр начнёт пищать (сообщая о том, что между щупами присутствует очень низкое сопротивление — замкнутая цепь).
Теперь мы знаем, что при нажатии кнопки цепь замыкается, а при отжатии — размыкается. На схеме это можно обозначить как простой выключатель:
Найдите на плате Arduino два контакта: помеченный GND и помеченный «2» (или любым другим произвольным числом — это контакты ввода-вывода общего назначения, которыми мы можем управлять через ПО).
Если мы подключим переключатель таким образом, а потом прикажем Arduino сконфигурировать контакт «2» как контакт INPUT, то получим цепь, показанную слева (на рисунке ниже). При нажатии кнопки контакт 2 будет напрямую соединяться с землёй / 0V, а при отжатии контакт 2 не будет соединён ни с чем. Это состояние (ни с чем не соединён) называется «floating» (состояние с высоким импедансом) и, к сожалению, это не очень хорошее состояние для наших целей. Когда мы считываем данные с контакта в ПО (с помощью digitalRead(2)), получаем LOW, если контакт заземлён, и непредсказуемый результат (LOW или HIGH), если контакт находится в состоянии floating!
Чтобы исправить это, мы можем сконфигурировать контакт так, чтобы он находился в режиме INPUT_PULLUP, который соединяется с резистором внутри процессора и создаёт схему, показанную справа. В этой цепи при разомкнутом переключателе контакт 2 имеет путь к +5V, поэтому при его считывании результатом всегда будет HIGH. При замыкании переключателя у контакта по-прежнему будет путь с высоким сопротивлением к +5V, а также путь без сопротивления к земле / 0V, который «побеждает», благодаря чему при считывании контакта мы получаем LOW.
Разработчикам ПО порядок может показаться обратным — при нажатии кнопки мы считываем false / LOW, а при отжатии — true / HIGH.
Можно сделать и наоборот, но у процессора есть только встроенные подтягивающие резисторы и нет утягивающих вниз резисторов, поэтому мы будем придерживаться этой модели.
Простейшая программа для Arduino, которая считывает состояние переключателя и сообщает PC о его состоянии, выглядит примерно так, как показано ниже. Вы можете нажать кнопку загрузки в Arduino IDE, а затем открыть Serial Monitor (в меню Tools), чтобы увидеть результаты.
void setup()
{
Serial.begin(9600);
pinMode(2, INPUT_PULLUP);
}
void loop()
{
int state = digitalRead(pin);
Serial.println( state == HIGH ? "Released" : "Pressed" );
delay(100);//artifically reduce the loop rate so the output is at a human readable rate...
}
К счастью, на основных переключателях моей панели есть пометки трёх контактов:
Я не полностью уверен, как он работает, поэтому мы снова переключим мультиметр в режим непрерывности и коснёмся всех пар контактов при включенном и отключенном переключателе… однако на этот раз мультиметр вообще не пищит, когда мы касаемся щупами [GND] и [+] при «включенном» переключателе! Единственная конфигурация, при которой мультиметр пищит (обнаруживает соединение) — когда переключатель «включен», а щупы находятся на [+] и [lamp].
Светодиод внутри переключателя блокирует измерения непрерывности, поэтому из проведённых выше проверок мы можем предположить, что LED подключен непосредственно к контакту [GND], а не к контактам [+] и [lamp]. Далее мы переключим мультиметр в режим проверки диодов (символ ) и снова проверим пары контактов, но на этот раз важна полярность (красный и чёрный щуп). Теперь если мы соединим красный щуп с [lamp], а чёрный — с [GND], то светодиод загорится, а на мультиметре отобразится 2.25V. Это прямое напряжение диода, или минимальное напряжение, необходимое для его включения. Вне зависимости от положения переключателя, 2.25V от [lamp] к [GND] заставляет LED загореться. Если мы соединим красный щуп с [+], а чёрный — с [GND], то светодиод загорится только при включённом переключателе.
Из этих показаний мы можем предположить, что внутренности этого переключателя выглядят примерно как на схеме ниже:
Честно говоря, о присутствии резистора здесь можно только догадываться. Светодиод должен быть соединён с соответствующим резистором, чтобы ограничивать подаваемый на него ток, или он сгорит. Мой не сгорел и похоже, что работает правильно. На форуме веб-сайта продавца я нашёл пост, в котором говорится об установленном резисторе, поддерживающем работу до 12 В, и это сэкономило мне время на проверку/вычисления подходящего резистора.
Проще всего использовать переключатель с Arduino, проигнорировав контакт [lamp]: подключить [GND] к GND в Arduino и соединить [+] с одним из пронумерованных контактов Arduino, например 3.
Если мы сконфигурируем контакт 3 как INPUT_PULLUP (так же, как и для предыдущей кнопки), то придём к показанному ниже результату. Слева вверху показано значение, которое мы будем получать, выполнив «digitalRead(3)» в коде Arduino.
Когда переключатель включен/замкнут, мы считываем LOW и светодиод загорается! Для использования такого переключателя в данной конфигурации мы можем использовать тот же код Arduino, что и в примере с кнопкой.
После подключения к Arduino полная цепь выглядит так:
Однако здесь мы можем увидеть, что при замыкании переключателя кроме небольшого ограничивающего ток резистора перед LED (я предполагаю, что его сопротивление 100 Ом) есть и ещё и подтягивающий резистор на 20 кОм, который ещё больше снижает величину тока, текущего через светодиод. Это означает, что хотя цепь и работает, светодиод будет не очень ярким.
Ещё один недостаток этой схемы в том, что у нас нет программного контроля над LED — он включён, когда включён переключатель, и отключен в противоположном случае.
Можно посмотреть, что случится, если мы подключим контакт [lamp] или к 0V, или к +5V.
Если [lamp] подключен к 0V, то светодиод постоянно отключен (вне зависимости от позиции переключателя), а распознавание позиции Arduino всё равно выполняется. Это позволяет нам при желании программно отключать LED!
Если [lamp] подключен к +5V, то светодиод постоянно включен (вне зависимости от позиции переключателя), однако распознавание позиции Arduino поломано — с контакта всегда будет считываться HIGH.
Мы можем преодолеть описанные выше ограничения (низкий ток/яркость светодиода и отсутствие программного контроля над светодиодом), написав больше кода! Чтобы разрешить конфликт между возможностью управления светодиодом и сломанным из-за него распознаванием позиции, мы можем разделить две задачи по времени, то есть временно отключать LED при считывании контакта датчика (3).
Сначала подключим контакт [lamp] к ещё одному контакту Arduino общего назначения, например, к 4, чтобы можно было управлять lamp.
Чтобы создать программу, которая будет правильно считывать позицию переключателя и управлять светодиодом (мы заставим его мигать), нам достаточно просто отключать светодиод перед считыванием состояния переключателя. Светодиод будет отключаться всего на доли миллисекунд, поэтому мерцание не должно быть заметно:
int pinSwitch = 3;
int pinLed = 4;
void setup()
{
//connect to the PC
Serial.begin(9600);
//connect our switch's [+] connector to a digital sensor, and to +5V through a large resistor
pinMode(pinSwitch, INPUT_PULLUP);
//connect our switch's [lamp] connector to 0V or +5V directly
pinMode(pinLed, OUTPUT);
}
void loop()
{
int lampOn = (millis()>>8)&1;//make a variable that alternates between 0 and 1 over time
digitalWrite(pinLed, LOW);//connect our [lamp] to +0V so the read is clean
int state = digitalRead(pinSwitch);
if( lampOn )
digitalWrite(pinLed, HIGH);//connect our [lamp] to +5V
Serial.println(state);//report the switch state to the PC
}
В Arduino Mega контакты 2-13 и 44-46 могут использовать функцию analogWrite, которая на самом деле не создаёт напряжения от 0V до +5V, а аппроксимирует его при помощи прямоугольной волны. При желании можно использовать её для управления яркостью светодиода! Этот код заставит свет пульсировать, а не просто мерцать:
void loop()
{
int lampState = (millis()>>1)&0xFF;//make a variable that alternates between 0 and 255 over time
digitalWrite(pinLed, LOW);//connect our [lamp] to +0V so the read is clean
int state = digitalRead(pinSwitch);
if( lampState > 0 )
analogWrite(pinLed, lampState);
}
Пост и так уже довольно большой, так что я не буду добавлять ещё и туториал по пайке, можете его загуглить!
Однако приведу самые базовые советы:
Чтобы ОС распознала устройство как игровой USB-контроллер, нужен достаточно простой код, но, к сожалению, также необходимо заменить firmware USB-чипа Arduino другим, которое можно взять здесь: https://github.com/harlequin-tech/arduino-usb [19].
Но после заливки этого firmware в Arduino устройство становится USB-джойстиком и перестаёт быть Arduino. Поэтому чтобы перепрограммировать его, нужно заново перепрошить исходную firmware Arduino. Эти итерации довольно мучительны — загружаем код Arduino, прошиваем firmware джойстика, тестируем, прошиваем firmware arduino, повторяем…
Пример программы для Arduino, которую можно использовать с этим firmware, показан ниже — он конфигурирует три кнопки в качестве вводов, считывает их значения, копирует значения в структуру данных, ожидаемую этим firmware, а затем отправляет данные. Смыть, намылить, повторить.
// define DEBUG if you want to inspect the output in the Serial Monitor
// don't define DEBUG if you're ready to use the custom firmware
#define DEBUG
//Say we've got three buttons, connected to GND and pins 2/3/4
int pinButton1 = 2;
int pinButton2 = 3;
int pinButton3 = 4;
void setup()
{
//configure our button's pins properly
pinMode(pinButton1, INPUT_PULLUP);
pinMode(pinButton2, INPUT_PULLUP);
pinMode(pinButton3, INPUT_PULLUP);
#if defined DEBUG
Serial.begin(9600);
#else
Serial.begin(115200);//The data rate expected by the custom USB firmware
delay(200);
#endif
}
//The structure expected by the custom USB firmware
#define NUM_BUTTONS 40
#define NUM_AXES 8 // 8 axes, X, Y, Z, etc
typedef struct joyReport_t {
int16_t axis[NUM_AXES];
uint8_t button[(NUM_BUTTONS+7)/8]; // 8 buttons per byte
} joyReport_t;
void sendJoyReport(struct joyReport_t *report)
{
#ifndef DEBUG
Serial.write((uint8_t *)report, sizeof(joyReport_t));//send our data to the custom USB firmware
#else
// dump human readable output for debugging
for (uint8_t ind=0; ind<NUM_AXES; ind++)
{
Serial.print("axis[");
Serial.print(ind);
Serial.print("]= ");
Serial.print(report->axis[ind]);
Serial.print(" ");
}
Serial.println();
for (uint8_t ind=0; ind<NUM_BUTTONS/8; ind++)
{
Serial.print("button[");
Serial.print(ind);
Serial.print("]= ");
Serial.print(report->button[ind], HEX);
Serial.print(" ");
}
Serial.println();
#endif
}
joyReport_t joyReport = {};
void loop()
{
//check if our buttons are pressed:
bool button1 = LOW == digitalRead( pinButton1 );
bool button2 = LOW == digitalRead( pinButton2 );
bool button3 = LOW == digitalRead( pinButton3 );
//write the data into the structure
joyReport.button[0] = (button1?0x01:0) | (button2?0x02:0) | (button3?0x03:0);
//send it to the firmware
sendJoyReport(joyReport)
}
Если у вас есть контроль над игрой, с которой должно взаимодействовать устройство, то в качестве альтернативы можно общаться с контроллером напрямую — нет необходимости делать его видимым для ОС как джойстик! В начале поста я упомянул Objects In Space; именно такой подход использовали её разработчики. Они создали простой коммуникационный ASCII-протокол, позволяющий контроллеру и игре общаться друг с другом. Достаточно просто перечислить последовательные порты системы (они же COM-порты в Windows; кстати, посмотрите, как ужасно это выглядит на C [22]), найти порт, к которому подключено устройство с названием «Arduino», и начать считывать/записывать ASCII по этой ссылке.
На стороне Arduino мы просто используем функции Serial.print, которые применялись в показанных выше примерах.
В начале этого поста я также упоминал мою библиотеку для решения этой задачи: https://github.com/hodgman/ois_protocol [20].
Она содержит код на C++, который можно интегрировать в игру и использовать её в качестве «сервера», и код Arduino, который можно выполнять в контроллере, чтобы использовать его в качестве «клиента».
В example_hardware.h [23] я создал классы, чтобы абстрагировать отдельные кнопки/переключатели; например, «Switch» — это простая кнопка из первого примера., а «LedSwitch2Pin» — переключатель с управляемым светодиодом из второго примера.
Код примера для моей панели кнопок находится в example.ino [24].
В качестве небольшого примера давайте допустим, что у нас есть единственная кнопка, которую нужно отправлять в игру, и один управляемый игрой светодиод. Необходимый код Arduino выглядит так:
#include "ois_protocol.h"
//instantiate the library
OisState ois;
//inputs are values that the game will send to the controller
struct
{
OisNumericInput myLedInput{"Lamp", Number};
} inputs;
//outputs are values the controller will send to the game
struct
{
OisNumericOutput myButtonOutput{"Button", Boolean};
} outputs;
//commands are named events that the controller will send to the game
struct
{
OisCommand quitCommand{"Quit"};
} commands;
int pinButton = 2;
int pinLed = 3;
void setup()
{
ois_setup_structs(ois, "My Controller", 1337, 42, commands, inputs, outputs);
pinMode(pinButton, INPUT_PULLUP);
pinMode(pinLed, OUTPUT);
}
void loop()
{
//read our button, send it to the game:
bool buttonPressed = LOW == digitalRead(pin);
ois_set(ois, outputs.myButtonOutput, buttonPressed);
//read the LED value from the game, write it to the LED pin:
analogWrite(pinLed, inputs.myLedInput.value);
//example command / event:
if( millis() > 60 * 1000 )//if 60 seconds has passed, tell the game to quit
ois_execute(ois, commands.quitCommand);
//run the library code (communicates with the game)
ois_loop(ois);
}
Код игры написан в стиле «single header». Для импорта библиотеки включим в игру oisdevice.h [25].
В едином файле CPP, прежде чем выполнять #include заголовка, напишем #define OIS_DEVICE_IMPL и #define OIS_SERIALPORT_IMPL — это добавит в файл CPP исходный код классов. Если у вас есть собственные утверждения, логгинг, строки или векторы, то существует несколько других макросов OIS_*, которые можно определить перед импортом заголовка, чтобы воспользоваться возможностями движка.
Для перечисления COM-портов и создания соединения с конкретным устройством можно использовать такой код:
OIS_PORT_LIST portList;
OIS_STRING_BUILDER sb;
SerialPort::EnumerateSerialPorts(portList, sb, -1);
for( auto it = portList.begin(); it != portList.end(); ++it )
{
std::string label = it->name + '(' + it->path + ')';
if( /*device selection choice*/ )
{
int gameVersion = 1;
OisDevice* device = new OisDevice(it->id, it->path, it->name, gameVersion, "Game Title");
...
}
}
Получив экземпляр OisDevice, нужно регулярно вызывать его функцию-член Poll (например, в каждом кадре), можно получать текущее состояние вывода контроллера с помощью DeviceOutputs(), использовать события устройства с помощью PopEvents() и отправлять устройству значения с помощью SetInput().
Пример приложения, делающего всё это, можно найти здесь: example_ois2vjoy/main.cpp [26].
Чтобы контроллер мог работать в других играх (часть 2), нужно установить собственное firmware и одну программу Arduino, но чтобы контроллер полностью программировался игрой, мы использовали стандартное firmware Arduino и другую программу Arduino. Но что если мы хотим иметь обе возможности одновременно?
Пример приложения, на который я давал ссылку выше (ois2vjoy [27]), решает эту проблему.
Это приложение общается с OIS-устройством (программа из части 3), а затем на PC преобразует эти данные в обычные данные контроллера/джойстика, которые потом передаются в виртуальное устройство контроллера/джойстика. Это означает, что можно позволить своему контроллеру постоянно использовать библиотеку OIS (другое firmware не требуется), а если мы захотим использовать его как обычный контроллер/джойстик, то просто запустим на PC приложение ois2vjoy, выполняющее преобразование.
Надеюсь, кому-то эта статья показалась полезной или интересной. Спасибо, что дочитали до конца!
Если вам стало любопытно, то я приглашаю вас поучаствовать в развитии библиотеки ois_protocol [20]! Думаю, будет здорово разработать единый протокол для поддержки всевозможных самодельных контроллеров в играх и стимулировать игры к прямой поддержке самодельных контроллеров!
Автор: PatientZero
Источник [28]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/arduino/310889
Ссылки в тексте:
[1] Objects in Space: http://objectsgame.com/
[2] туториал по Arduino: http://objectsgame.com/the-controllers/arduino-tutorial/
[3] коммуникационного протокола: http://objectsgame.com/the-controllers/ois-serial-data-protocol/
[4] Arduino Mega2560: https://www.banggood.com/Mega-2560-R3-ATmega2560-16AU-Control-Board-Without-USB-Cable-For-Arduino-p-1044805.html?p=L107012055587201508R&cur_warehouse=CN
[5] гоночная панель переключателей зажигания: https://www.banggood.com/12V-Racing-Car-Ignition-Switch-4-Blue-1-Red-LED-Toggle-Button-Panel-Housing-p-1363098.html?p=L107012055587201508R&cur_warehouse=CN
[6] «male to male»: https://www.banggood.com/40pcs-20cm-Male-To-Male-Color-Breadboard-Cable-Jumper-Cable-Dupont-Wire-p-70127.html?p=L107012055587201508R&cur_warehouse=CN
[7] «male to female»: https://www.banggood.com/40pcs-20cm-Male-to-Female-Color-Breadboard-Cable-Jump-Wire-Jumper-p-992837.html?p=L107012055587201508R&cur_warehouse=CN
[8] хороший: https://www.banggood.com/MINI-TS100-Digital-OLED-Programmable-Interface-DC-5525-Soldering-Iron-Station-Built-in-STM32-Chip-p-984214.html?p=L107012055587201508R&cur_warehouse=CN
[9] дешёвый: https://www.banggood.com/YIHUA-908D-220V-LED-Digital-Display-Soldering-Station-Soldering-Iron-Kit-p-1059873.html?p=L107012055587201508R
[10] с канифолью 60/40: https://www.banggood.com/100g-0_7mm-6040-Tin-Lead-Soldering-Wire-Reel-Solder-Rosin-Core-p-1025802.html?p=L107012055587201508R&cur_warehouse=CN
[11] Термоусадочная трубка: https://www.banggood.com/Soloop-328pcs-21-Polyolefin-Halogen-Free-Heat-Shrink-Tube-Sleeving-5-Color-8-Size-p-969574.html?p=L107012055587201508R&cur_warehouse=CN
[12] изолента: https://www.banggood.com/3M-Electrical-Insulating-Tape-Household-Electrical-Adhesive-Tape-p-931869.html?p=L107012055587201508R&cur_warehouse=CN
[13] Клеевой пистолет: https://www.banggood.com/Electric-20W-Hot-Melt-Art-Craft-Glue-Gun-with-50Pcs-Free-Mini-Clear-Glue-Sticks-p-1084022.html?p=L107012055587201508R&cur_warehouse=CN
[14] эпоксидная смола: https://www.banggood.com/60g-Rapid-Curing-Epoxy-Adhesive-Quick-Drying-AB-Resin-Hardener-Material-Universal-Glue-p-1329144.html?p=L107012055587201508R&cur_warehouse=CN
[15] Мультиметр: https://www.banggood.com/BSIDE-ADM08A-6000-Counts-True-RMS-Digital-Multimeter-p-1071663.html?p=L107012055587201508R&cur_warehouse=CN
[16] Кусачки/плоскогубцы для зачистки проводов: https://www.banggood.com/Multifunctional-Durable-Multifunction-Handle-Tool-Wire-Stripper-Stripping-Pliers-p-983170.html?p=L107012055587201508R&cur_warehouse=CN
[17] Arduino IDE: https://www.arduino.cc/en/main/software
[18] FLIP: https://www.microchip.com/developmenttools/ProductDetails/FLIP
[19] arduino-usb: https://github.com/harlequin-tech/arduino-usb
[20] ois_protocol: https://github.com/hodgman/ois_protocol
[21] Драйвер vJoy: http://vjoystick.sourceforge.net/site/
[22] посмотрите, как ужасно это выглядит на C: https://github.com/hodgman/ois_protocol/blob/master/serial_host_cpp/serialport.hpp#L109
[23] example_hardware.h: https://github.com/hodgman/ois_protocol/blob/master/serial_device_arduino/example/example_hardware.h
[24] example.ino: https://github.com/hodgman/ois_protocol/blob/master/serial_device_arduino/example/example.ino
[25] oisdevice.h: https://github.com/hodgman/ois_protocol/blob/master/serial_host_cpp/oisdevice.h
[26] example_ois2vjoy/main.cpp: https://github.com/hodgman/ois_protocol/blob/master/serial_host_cpp/example_ois2vjoy/main.cpp
[27] ois2vjoy: https://github.com/hodgman/ois_protocol/tree/master/serial_host_cpp/example_ois2vjoy
[28] Источник: https://habr.com/ru/post/442816/?utm_source=habrahabr&utm_medium=rss&utm_campaign=442816
Нажмите здесь для печати.