创建自己的游戏控制器

灵感来源


在游戏展览中,“ 太空中的物体”开发人员在大型太空船的驾驶舱中使用控制器演示了他们的游戏演示。 它还配有照明按钮,模拟设备,状态指示灯,开关等。...这极大地影响了游戏的沉浸感:


Arduino教程已发布在游戏的网站上,其中描述了此类控制器的通信协议

我想为我的游戏创建相同的内容

在此示例中,我将花费约40美元在赛车模拟器的驾驶舱中添加美观,大型和重型的开关。 这些开关的主要成本是相关的-如果我使用简单的开关/按钮,价格将是原来的一半! 这是可以承受240瓦功率的真实设备,我只会散发出约0.03瓦的功率。

警告:我决定省钱,所以我留下了一个便宜的中文网站的链接,在那里我购买了许多不同的组件/工具。 购买廉价组件的缺点之一是它们通常没有文档,因此在本文中,我将解决此问题。

主要组成






特色工具



软体类


  • Arduino IDE,用于对Arduino处理器进行编程
  • 要创建显示为真实硬件USB控制器/操纵杆的控制器,请执行以下操作:
    • FLIP用于在Arduino USB控制器中刷新新固件
    • github上的Arduino-usb
  • 创建与游戏直接通信的控制器( 或显示为虚拟USB控制器/操纵杆的控制器

警告


我在高中学习了电子学,学习了如何使用烙铁,学习了红线需要连接到红色,黑线需要连接到黑...电压,安培,电阻和连接它们的方程式-这就是我在电子学方面的正式培训所花的全部时间。

对我来说,这是一个培训项目,因此可能有不好的建议或错误!

第1部分。将控制器放在一起!


我们使用没有文档的开关...


如上所述,我从低利润零售商那里购买便宜的零件,所以要做的第一件事就是弄清楚这些开关/按钮的工作方式。

简单的按钮/开关


使用该按钮,一切都很简单-里面没有LED,只有两个触点。 将万用表切换到连续性/拨号模式( )并触摸不同触点的探针-屏幕上将显示OL(开环,开路):这意味着两个探针之间没有连接。 然后,我们按下按钮,仍然触摸接触式探针-屏幕上现在应该会出现类似0.1Ω的信号,万用表将开始发出哔声( 表明探针之间的电阻非常低-闭合电路 )。

现在我们知道,按下按钮时,电路闭合,按下时,电路断开。 在图中,可以将其描述为一个简单的开关:

我们将开关连接到Arduino


在Arduino板上找到两个引脚:标记为GND和标记为“ 2”(或任何其他任意数字-这些是我们可以通过软件控制的通用I / O引脚)。

如果我们以这种方式连接开关,然后我们命令Arduino将引脚“ 2”配置为INPUT引脚,则会得到左侧所示的电路(如下图所示)。 按下按钮时,引脚2将直接接地/ 0V,按下时,引脚2将不连接任何东西。 这种状态( 未连接任何东西 )被称为“浮动”(高阻抗状态),不幸的是,对于我们的目的而言,这不是一个很好的条件。 当我们从软件中的触点读取数据( 使用digitalRead(2) )时,如果触点接地,则为LOW;如果触点处于浮动状态,则将得到不可预测的结果(LOW或HIGH)!

为了解决这个问题,我们可以将触点配置为INPUT_PULLUP模式,该模式连接到处理器内部的电阻并创建右侧所示的电路。 在此电路中,在开关断开的情况下,引脚2的路径为+ 5V,因此当读取时,结果始终为HIGH。 当开关闭合时,触点仍将具有高至+ 5V电阻的路径,以及不具有接地电阻/ 0V的路径,该路径“获胜”,因此当我们读取触点时,将得到LOW。


对于软件开发人员来说,顺序似乎是相反的-单击按钮时,读为false / LOW,而当按下按钮时,读为true / HIGH。

您可以做相反的事情,但是处理器只有内置的上拉电阻,没有下拉电阻,因此我们将坚持这种模式。

Arduino的最简单程序读取开关的状态并告知PC其状态,看起来类似于以下所示。 您可以单击Arduino IDE中的下载按钮,然后打开“串行监视器”(在“工具”菜单中)以查看结果。

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... } 

其他几乎没有文档的交换机...


三针LED开关


幸运的是,在面板的主开关上有三个触点的标记:


我不太确定它是如何工作的,因此我们将万用表切换回连续模式,并在开关打开和关闭时触​​摸所有的触点对……但是,这一次,当我们在触摸[GND]和[+]探针时用“开机! 万用表发出哔哔声( 检测到连接 )的唯一配置是当开关“打开”且探头在[+]和[lamp]上时。

开关内部的LED阻止了连续性测量,因此从上述测试中我们可以假定LED直接连接到[GND]引脚,而不是[+]和[lamp]触点。 接下来,我们将万用表切换到二极管测试模式(符号 )并再次检查一对触点,但是这次极性很重要( 红色和黑色探针 )。 现在,如果将红色探针连接到[lamp],将黑色探针连接到[GND],则LED将点亮,并且万用表上将显示2.25V。 这是二极管的直流电压,或者是打开二极管所需的最低电压。 无论开关位置如何,从[灯]到[GND]的2.25V电压都会使LED点亮。 如果将红色探针连接到[+],将黑色探针连接到[GND],则仅当开关打开时,LED才会点亮。

从这些读数中,我们可以假定此开关的内部外观类似于下图:

  1. 当开关打开/关闭时,[+]和[灯]短路。
  2. 从[灯]到[GND]的正电压始终点亮LED。
  3. 只有当开关打开/关闭时,从[+]到[GND]的正电压才会点亮LED。



老实说,我们只能猜测电阻的存在。 LED 必须连接到适当的电阻器,以限制提供给它的电流,否则它会烧坏。 矿井没有烧毁,看起来工作正常。 在卖方的网站论坛上,我找到了一篇帖子,内容涉及已安装的支持最高12 V的电阻器,这节省了我检查/计算合适电阻器的时间。

我们将开关连接到Arduino


最简单的方法是将开关与Arduino一起使用,而忽略[lamp]引脚:将[GND]连接至Arduino中的GND,并将[+]连接至编号的Arduino触点之一,例如3。

如果将引脚3配置为INPUT_PULLUP( 与上一个按钮相同 ),则将得到如下所示的结果。 左上方显示了通过执行Arduino代码中的“ digitalRead(3)”将获得的值。

当开关打开/关闭时,我们读到LOW(低),LED点亮! 要在此配置中使用此类开关,我们可以使用与按钮示例中相同的Arduino代码。


该解决方案的问题


连接到Arduino之后,整个电路如下所示:


但是,在这里我们可以看到,当开关闭合时,除了LED前面的小限流电阻(我假设其电阻为100欧姆)外,还存在一个20 kOhm的上拉电阻,这进一步减少了流经LED的电流量。 这意味着尽管电路正常工作,但LED不会非常亮。

该方案的另一个缺点是我们没有对LED进行软件控制的功能-当开关打开时,LED会打开,在相反的情况下会禁用。

您可以看到如果将[lamp]引脚连接到0V或+ 5V会发生什么。

如果[lamp]连接到0V,则LED指示灯将始终熄灭( 与开关位置无关 ),并且仍执行Arduino位置识别。 这使我们能够以编程方式禁用LED!


如果[lamp]连接到+ 5V,则LED一直亮着( 无论开关的位置如何 ), 但是, Arduino位置识别被破坏了-始终从触点读取HIGH。


我们将此开关正确连接到Arduino


通过编写更多代码,我们可以克服上述限制( LED的低电流/亮度以及对LED缺乏程序控制 )! 为了解决控制LED的能力与由于其损坏而导致的位置识别之间的冲突,我们可以及时分离两个任务,即在读取传感器触点(3)时暂时关闭LED。

首先,将[lamp]引脚连接到另一个通用Arduino引脚,例如,连接到4,以便您可以控制灯泡。

要创建一个可以正确读取开关位置并控制LED的程序(我们将使其闪烁),我们只需要在读取开关状态之前关闭LED即可。 LED将仅关闭一毫秒的时间,因此闪烁不应明显:

 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的电压,而是使用方波对其进行逼近。 如果需要,可以使用它来控制LED的亮度! 此代码将使光脉动,而不仅仅是闪烁:

 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); } 

组装技巧


帖子已经很大,所以我不会添加焊接教程,可以谷歌搜索!

但是,我将给出最基本的提示:

  • 连接带有大金属触点的电线时,首先要确保烙铁很热,然后将金属触点加热一会儿。 焊接的含义是通过形成合金来形成永久连接,但是如果连接的仅一部分是热的,则可以轻松获得看起来像连接但实际上并未连接的“冷连接”。
  • 连接两根电线时, 首先要在其中一根上放一根热缩管-连接后,就不能再穿管了。 这似乎很明显,但是我经常忘记它,我必须使用电工胶带代替管子……将收缩管从连接处拉开,以免其提前加热。 检查焊接连接后,将管子滑到其上并加热。
  • 我一开始提到的细细连接线非常适合无焊连接(例如,当连接到Arduino时!),但相当脆弱。 焊接后,使用胶枪将其固定,并消除连接本身的所有应力。 例如,下图中的红色导线在操作过程中可能会被意外拉出,因此在焊接后,我用一滴热胶将其固定:


第2部分。我们将设备变成游戏控制器!


为了使操作系统将设备识别为USB游戏控制器,您需要一个相当简单的代码,但是,不幸的是,您还需要用另一个USB固件替换Arduino USB芯片固件,可在此处获取: https : //github.com/harlequin-tech/arduino-usb

但是在将该固件上传到Arduino之后,设备变成了USB游戏杆,不再是Arduino。 因此,要对其进行重新编程,您需要重新刷新原始Arduino固件。 这些迭代非常痛苦-加载Arduino代码,刷新操纵杆固件,测试,刷新arduino固件,重复执行...

可以与该固件一起使用的Arduino程序示例如下所示-将三个按钮配置为输入,读取它们的值,将值复制到该固件所需的数据结构中,然后发送数据。 洗净,用肥皂洗,重复一遍。

 // 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) } 

第3部分。我们将设备与我们自己的游戏集成在一起!


如果您可以控制设备应与之交互的游戏,则可以选择直接与控制器通信-无需将其作为操纵杆在OS上显示! 在文章的开头,我提到了太空中的物体; 这是开发人员使用的方法。 他们创建了一个简单的ASCII通信协议,该协议允许控制器和游戏相互通信。 只需列出系统的串行端口( 它们是Windows上的COM端口;顺便说一下, 看一下它在C语言中的表现如何 ),找到称为“ Arduino”的设备所连接的端口,然后开始从此链接读取/写入ASCII。

在Arduino方面,我们仅使用上面示例中使用的Serial.print函数。

在本文的开头,我还提到了用于解决此问题的库: https : //github.com/hodgman/ois_protocol

它包含可以集成到游戏中并用作“服务器”的C ++代码,以及可以在控制器中执行以将其用作“客户端”的Arduino代码。

自定义Arduino


example_hardware.h中,我创建了一些类来抽象单个按钮/单选按钮; 例如,“开关”是第一个示例中的一个简单按钮。而“ LedSwitch2Pin”是第二个示例中的带有受控LED的开关。

我的按钮栏的示例代码在example.ino中

举一个小例子,假设我们有一个需要发送到游戏的按钮和一个游戏控制的LED。 所需的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); } 

定制游戏


游戏代码以“单个标题”的形式编写。 要导入库, 在游戏中包含oisdevice.h

在单个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

第4部分。如果我同时需要第2部分和第3部分,该怎么办?


为了使控制器能够在其他游戏中工作(第2部分),您需要安装自己的固件和一个Arduino程序,但是对于要由游戏进行完全编程的控制器,我们使用了标准的Arduino固件和另一个Arduino程序。 但是,如果我们想同时拥有两种可能性怎么办?

我在上面提供了链接的示例应用程序( ois2vjoy )解决了此问题。

该应用程序与OIS设备(第3部分中的程序)进行通信,然后在PC上将此数据转换为常规控制器/操纵杆数据,然后将其传输到虚拟控制器/操纵杆设备。 这意味着您可以允许您的控制器不断使用OIS库(不需要其他固件),并且如果我们想将其用作常规控制器/操纵杆,则只需在PC上运行ois2vjoy应用程序即可执行转换。

第5部分。完成


我希望有人觉得这篇文章有用或有趣。 感谢您阅读到底!

如果您感到好奇,那么我邀请您参与ois_protocol库的开发! 我认为开发单一协议以支持游戏中各种自制控制器并鼓励游戏直接支持自制控制器将是一件很棒的事情!

Source: https://habr.com/ru/post/zh-CN442816/


All Articles