Steam 输入是一项服务,允许 Steam 用户用自己喜爱的设备游玩任何支持控制器的游戏。 Steam 输入将通过手柄模拟、键鼠模拟或 Steam 输入 API 将用户的输入转化为游戏所能理解的信息。 在这里,我们将重点说明如何最好地使用 Steam 输入手柄模拟,来扩展您游戏现有的控制器支持。

什么是手柄模拟?

在 Windows 操作系统中,Steam 界面会和 XInput、DirectInput、RawInput 以及 Windows.Gaming.Input 挂钩,并注入模拟的 Xbox 控制器设备。 在 MacOS 和 Linux 中,模拟的控制器由驱动提供。

控制器将会作为 Xbox 控制器出现在您游戏中,也就意味着带有额外输入的控制器,有些输入会彼此重复。比如,Playstation 触控板点击和选项按键都会映射至 XInput 的 Start 按键。

除了一般的手柄输入外,还可以将 Switch、PlayStation 和 Steam 控制器陀螺仪输入绑定至鼠标模拟,提供动作控制。 这仅适用于单一本地玩家的游戏,因为只有一个鼠标输入,并且也依赖于游戏同时接收鼠标和手柄输入。 如果您对这些功能感兴趣同时又不想受到以上警告限制,请考虑添加 Steam Input API。

您可以询问 Steam 当前使用的控制器类型,来显示特定设备的字形,但仅限于您当前 Steamworks SDK 支持的类型。 如果您对永不过时的字形支持感兴趣,请整合 Steam Input API。

即使您已经支持某些控制器,可能还是有用户会通过 Steam 输入来使用这些控制器,因为 Steam 远程畅玩使用它在流式传输的同时提供输入,而有相当一部分用户也都启用了 Steam 输入,来在整个 Steam 库中重新配置其控制器。 总体来说,2020 年所有的控制器会话中,大约有四分之一使用的是 Steam 输入,其中包括近乎一半的 Playstation 控制器会话。

Steam 上有超过 2000 款游戏在为至少一种控制器使用手柄模拟,包括 Monster Hunter: World、Ace Combat 7、《勇者斗恶龙 XI》、Into the Breach、《中土世界:战争之影》等知名游戏,尽管不是所有这些游戏都采纳了全部的最佳实践 在这里,我们要以 Into the Breach 为例,因为这款游戏在遵照每一项最佳实践方面做得非常好。

显示设备专用艺术作品我们支持几种通过 Steam 输入手柄模拟获取设备专用字形的途径。一种针对可以在运行时载入永不过时的图像(即当 Steam 更新了 Steam 输入时这类图像无需任何游戏更新也能使用)的游戏;另外两种针对需要将图像烘焙进自己的资产中的游戏,或希望使用与控制台端口相同风格的艺术作品的游戏。 Into the Breach 为控制器使用了自己的艺术作品。Xbox/Steam:

java手柄模组 手柄模拟工具_API

PlayStation:

java手柄模组 手柄模拟工具_API_02

Nintendo Switch 控制器:

java手柄模组 手柄模拟工具_API_03

注意:即使您使用的是您自己的艺术作品,我们也建议您在无法识别控制器时回退至 Steam 艺术作品,或调用我们的帮助函数以找到您的游戏发行时存在的最接近选项,这样今后添加的设备会有适当的字形。

显示 Steam 的永不过时字形您需要使用以下函数:

// 在使用单个函数前初始化接口——仅需调用一次!
SteamInput()->Init()
// ...
// SteamAPI_RunCallbacks() 会为所有初始化的接口调用 RunFrame 函数,
// 且大部分游戏会已经开始定期调用此代码。 如果您没有这么做,则需要手动更新
// Steam 输入接口
SteamInput()->RunFrame();
// ...
// 用您正在询问的 XInput 槽位代替。 这一数值在 0 至 3 之间
// 如果您在决定使用哪个 API 之前就使用 RawInput 来检测设备,
// 请参阅“使用 RawInput 检测设备”一节。
int nXinputSlot = 0;
// 用您查询的按钮进行替代
EXboxOrigin eXboxButtonToGetGlyphFor = k_EXboxOrigin_A;
EInputActionOrigin buttonOrigin = k_EInputActionOrigin_XBoxOne_A;
// 如果控制器是通过 Steam 输入配置的,转换按钮
InputHandle_t controller1Handle = SteamInput()->GetControllerForGamepadIndex( nXinputSlot );
if ( controller1Handle > 0 )
{
// 有效的句柄不是 0,这是通过 Steam 输入配置的控制器
// 注意:使用 Steam 输入 API 的控制器将不会通过 GetControllerForGamepadIndex() 返回句柄
buttonOrigin = SteamInput()->GetActionOriginFromXboxOrigin( controller1Handle, k_EXboxOrigin_A );
}
else
{
// 有效的句柄不是 0,这是一个普通的 Xbox 控制器
// 继续使用原来的按键
}
// EInputActionOrigin 的值会随着 Steam 添加更多支持而增加,但这没有关系,
// 因为在此示例中,我们会从 Steam 处得到设备图像,同时也可以提供新的字形图像
// 从 Steam 客户端获取图像
const char *localGlyphPath = SteamInput()->GetGlyphForActionOrigin( buttonOrigin );
printf( "path = %s\n", localGlyphPath ); // "path = C:\Program Files (x86)\Steam\tenfoot\resource\images\library\controller\api\ps4_button_x.png"
// 将此替换为游戏中将文件路径转换为可用游戏纹理的函数
glyphTextureID = loadButtonGlyphTextureFromLocalPath( localGlyphPath )

显示您自己的艺术作品——字形面板如果您在使用多个字形面板并根据控制器类型来选择使用哪一个,您可以使用以下方法通过 Xinput 槽位来向 Steam 询问此信息:

// 在使用单个函数前初始化接口——仅需调用一次!
SteamInput()->Init()
// ...
// SteamAPI_RunCallbacks() 会为所有初始化的接口调用 RunFrame 函数,
// 且大部分游戏会已经开始定期调用此代码。 如果您没有这么做,则需要手动更新
// Steam 输入接口
SteamInput()->RunFrame();
// ...
// 用您正在询问的 XInput 槽位代替。 这一数值在 0 至 3 之间
// 如果您在决定使用哪个 API 之前就使用 RawInput 来检测设备,
// 请参阅“使用 RawInput 检测设备”一节。
int nXinputSlotIndex = 0;
InputHandle_t inputHandle = SteamInput()->GetControllerForGamepadIndex( nXinputSlotIndex );
if ( inputHandle == 0 )
{
// 有效的输入句柄不是 0,这是一个普通的 Xbox 控制器。
}
else
{
// Steam will always return an enum value that was valid for your SDK version.
ESteamInputType inputType = SteamInput()->GetInputTypeForHandle( inputHandle );
switch( inputType )
{
case k_ESteamInputType_Unknown:
printf( "unknown!\n" ); break;
case k_ESteamInputType_SteamController:
printf( "Steam controller!\n" ); break;
case k_ESteamInputType_XBox360Controller:
printf( "XBox 360 controller!\n" ); break;
case k_ESteamInputType_XBoxOneController:
printf( "XBox One controller!\n" ); break;
case k_ESteamInputType_GenericXInput:
printf( "Generic XInput!\n" ); break;
case k_ESteamInputType_PS4Controller:
printf( "PS4 controller!\n" ); break;
}
}

显示您自己的艺术作品——每个按键的艺术作品如果您在使用单一的由操作源索引的查找表,您可以使用这些函数来实现特定设备的字形,并将任何无法识别的控制器转换为最为接近的控制器。

// 在使用单个函数前初始化接口——仅需调用一次!
SteamInput()->Init()
// ...
// SteamAPI_RunCallbacks() 会为所有初始化的接口调用 RunFrame 函数,
// 且大部分游戏会已经开始定期调用此代码。 如果您没有这么做,则需要手动更新
// Steam 输入接口
SteamInput()->RunFrame();
// ...
// 用您正在询问的 XInput 槽位代替。 这一数值在 0 至 3 之间
// 如果您在决定使用哪个 API 之前就使用 RawInput 来检测设备,
// 请参阅“使用 RawInput 检测设备”一节。
int nXinputSlot = 0;
// 使用您查询的按钮进行替代
EXboxOrigin eXboxButtonToGetGlyphFor = k_EXboxOrigin_A;
EInputActionOrigin buttonOrigin = k_EInputActionOrigin_XBoxOne_A;
// 如果控制器是通过 Steam 输入来配置的,转换按钮
InputHandle_t controller1Handle = SteamInput()->GetControllerForGamepadIndex( nXinputSlot );
if ( controller1Handle > 0 )
{
// 有效的句柄不是 0,这是通过 Steam 输入配置的控制器。
// 注意:使用 Steam 输入 API 的控制器将不会通过 GetControllerForGamepadIndex() 返回句柄
buttonOrigin = SteamInput()->GetActionOriginFromXboxOrigin( controller1Handle, k_EXboxOrigin_A ); //i.e, k_EInputActionOrigin_PS4_X
}
else
{
// 有效句柄不是 0,这是一个普通的 Xobx 控制器
// 继续使用原来的按键
}
// Steam 将会继续添加操作,且将来的控制器将会超过当前的范围
// 如果您想要进行测试,您可以假装 Switch/PS5 控制器不存在,并将此更改为:
// if ( buttonOrigin >= k_EInputActionOrigin_XBox360_Reserved10 )
if ( buttonOrigin >= k_EInputActionOrigin_Count )
{
// 我们的游戏中没有为此源推出任何艺术作品! 我想 Steam 已添加了
// 对一个新控制器的支持。 让我们获取最接近于我们针对其而创建的 SDK 的值
buttonOrigin = SteamInput()->TranslateActionOrigin( k_ESteamInputType_Unknown, buttonOrigin );
}
// 将此替换为游戏中会为某个按键返回您的艺术作品的函数
int glyphTextureID = getHardCodedButtonGlyphTexture( buttonOrigin );

使用 RawInput 检测设备一些游戏会使用 RawInput 来支持 4 个以上的控制器,或使用它来判定是否有 PlayStation 控制器连接到了游戏。 此 API 还会返回其他非手柄类设备,且并没有可靠的索引以直接从 Steam 输入函数映射。 想要获得此信息,您可以从 GetRawInputDeviceInfo() 查询 RIDI_DEVICENAME 字符串,前者用真实设备的 USB VID/PID、手柄索引以及 Steam 输入句柄编码,格式如下:

\\.\pipe\HID#VID_045E&PID_028E&IG_00#{Real device VID}&{Real Device PID}&{Steam Input API handle}#{Steam Input Gamepad Index}#{ProcessID}

例如,\\.\pipe\HID#VID_045E&PID_028E&IG_00#045E&0B00&00045EB00704DAF3#0#20160 for an Xbox one controller with a VID/PID of 0x45e/0x0b00。

代码示例:

#define VALVE_DIRECTINPUT_GAMEPAD_VID0x28DE
#define VALVE_DIRECTINPUT_GAMEPAD_PID0x11FF
#define STEAM_INPUT_VID_INDEX37
#define STEAM_INPUT_PID_INDEX42
#define STEAM_INPUT_SIAPI_HANDLE_INDEX47
#define STEAM_INPUT_GAMEPAD_INDEX64
#define MAX_PATH 260
//...
void YourFunction()
{
PRAWINPUTDEVICELIST devices = NULL;
UINT i, j, device_count = 0;
if ((GetRawInputDeviceList(NULL, &device_count, sizeof(RAWINPUTDEVICELIST)) == -1) || (!device_count)) {
return; /* oh well. */
}
devices = (PRAWINPUTDEVICELIST)malloc(sizeof(RAWINPUTDEVICELIST) * device_count);
if (devices == NULL) {
return;
}
if (GetRawInputDeviceList(devices, &device_count, sizeof(RAWINPUTDEVICELIST)) == -1) {
free(devices);
return; /* oh well. */
}
for (i = 0; i < device_count; i++) {
RID_DEVICE_INFO rdi;
char devName[MAX_PATH];
UINT rdiSize = sizeof(rdi);
UINT nameSize = MAX_PATH;
rdi.cbSize = sizeof(rdi);
if ( devices[i].dwType == RIM_TYPEHID &&
GetRawInputDeviceInfoA( devices[i].hDevice, RIDI_DEVICEINFO, &rdi, &rdiSize ) != (UINT)-1 &&
GetRawInputDeviceInfoA( devices[i].hDevice, RIDI_DEVICENAME, devName, &nameSize ) != (UINT)-1 )
{
if ( rdi.hid.dwVendorId == VALVE_DIRECTINPUT_GAMEPAD_VID && rdi.hid.dwProductId == VALVE_DIRECTINPUT_GAMEPAD_PID )
{
uint32 ulVID = strtoul( &devName[STEAM_INPUT_VID_INDEX], NULL, 16 );
uint32 ulPID = strtoul( &devName[STEAM_INPUT_PID_INDEX], NULL, 16 );
uint64 ulDeviceHandle = strtoull( &devName[STEAM_INPUT_SIAPI_HANDLE_INDEX], NULL, 16 );
uint32 unGamepadIndex = strtoul( &devName[STEAM_INPUT_GAMEPAD_INDEX], NULL, 16 );
[/i][/i][/i]
Log( "Raw input device: VID = 0x%x, PID = 0x%x, handle = 0x%llx, index = 0x%x, %s\n", ulVID, ulPID, ulDeviceHandle, unGamepadIndex, devName );
// 您现在可以使用 VID/PID 直接识别设备或使用 ulDeviceHandle 和 GetInputTypeForHandle
ESteamInputType inputType = SteamInput()->GetInputTypeForHandle( ulDeviceHandle );
switch( inputType )
{
case k_ESteamInputType_Unknown:
printf( "unknown!\n" ); break;
case k_ESteamInputType_SteamController:
printf( "Steam controller!\n" ); break;
case k_ESteamInputType_XBox360Controller:
printf( "XBox 360 controller!\n" ); break;
case k_ESteamInputType_XBoxOneController:
printf( "XBox One controller!\n" ); break;
case k_ESteamInputType_GenericGamepad:
printf( "Generic Gamepad(DirectInput)!\n" ); break;
case k_ESteamInputType_PS3Controller:
case k_ESteamInputType_PS4Controller:
case k_ESteamInputType_PS5Controller:
printf( "PlayStation controller!\n" ); break;
}
}
else
{
// 设备不是通过 Steam 输入而运行的控制器——请使用您的普通控制器 ID 逻辑
continue;
}
}
}
free(devices);
}

Setting the Steamworks Settings第一步是在应用程序->Steam 输入下 Steamworks 设置中的选择加入栏目里设置哪个控制器将会使用 Steam 输入。 对于带有 Xbox 控制器支持的标准游戏,我们建议勾选 Xbox 外的所有复选框:

java手柄模组 手柄模拟工具_API_04

如果您的游戏支持飞行摇杆或赛车方向盘,您需要取消勾选 Generic/DirectInput 控制器的复选框,因为 Steam 不支持重映射这些设备。

选择一个配置您可以创建您自己的配置或从我们预制的模板中选择一个。 您无需创建自己的配置,除非您计划不采用我们默认的配置,或者要按游戏中操作标记单个按键设置。 Steam 输入的内置模板已本地化为所有 Steam 支持的语言,因此请确保您同样对配置进行本地化,以避免向非英语使用者显示英语。

选择一个模板游戏主机控制器不区分各种不同的手柄模板,但这些设置对于 Steam 控制器而言很重要。 以下是对各模板的解释,以及各模板适用的游戏类型:

手柄 - 将右触控板转化为原始模拟摇杆,模拟 Xbox 控制器。 对双摇杆、平台游戏或体育类游戏非常有用。

高精度视角/瞄准手柄 - 第一人称射击或任何使用视角控制的游戏的理想配置。 在选择此项前,请认真测试您的游戏是否支持同时使用手柄和鼠标,包括所有辅助场景,如库存或地图界面等。

带视角控制的手柄 - 此配置从触控板获得鼠标样式输入的支持,并将其转换为仿真摇杆拨动。 如果您的游戏可以配置成摇杆极高灵敏度、无死区、具有直线加速曲线,那么此配置将有效运作。否则,请考虑使用鼠标/键盘配置。

java手柄模组 手柄模拟工具_java手柄模组_05

您可以在此处了解在 Steamworks 中为您的游戏将模板设置为上线的步骤。

创建自定义配置您可以采用在多种控制器类型中自动转换的配置,但是您至少需要制作一个 Steam 控制器和 Xbox 控制器配置。

注意:如果您计划利用设备专用功能,如运动控制等,您还需要一个针对 Playstation 4 或 Nintendo Switch 控制器的配置。

文本输入

屏幕上的文本输入从技术上讲并不是 ISteamInput(Steam 输入)的组成部分,而是属于 ISteamUtils。 当前,仅在玩家通过大屏幕模式启动游戏时才会实现。

部分快速参考如下:

配置本地化

在不使用 Steam 输入 API 时制作本地化的配置会更复杂一些,但并非不可实现。

首先,确定您在上次导出配置之后未编辑过配置。 打开配置器进行编辑(可以在修改后再复原)。

打开 Steam\Logs\controller_ui.xt 日志,找到这样的内容:

Steam Controller Saving Controller Configuration Autosave for [控制器序列号]- AppID: [您的 APPID]。

Loaded Config for Local Selection Path for App ID [您的 APPID], Controller 0: F:\ProgramFiles\Steam\client\userdata\[STEAMID]\config\controller_configs\apps\[您的 APPID]\[控制器序列号]\guest\controller_configuration.vdf
插入您要本地化的按键设置本地化令牌
标题/描述:
"controller_mappings"
{
"version""3"
"revision""5"
"title""#title"
"description""#description"
...
按键设置:
"button_a"
{
"activators"
{
"Full_Press"
{
"bindings"
{
"binding""xinput_button A, #abutton"
}
}
}
}
...

然后为本地化程序块中的各语言提供相应的值:

"localization"
{
"english"
{
"title""This is a localized title"
"description""This is a localized description. The button diamond binding also have localized names."
"abutton""Your name here 1"
"bbutton""Your name here 2"
"xbutton""Your name here 3"
"ybutton""Your name here 4"
}
...

您现在可以导出配置,并按这些说明在合作伙伴站点进行设置。 您需要为您的一个配置设置“使用操作快”复选框。

您可以在浏览器中输入以下 URL 以在 Steam 客户端里预览示例配置:steam://controllerconfig/681280/1744110334,或者,您可以在这里找到 VDF 文件。