从 XInput 移植到 GameInput
从 XInput 移植到 GameInput 是所有旧 API 中最不复杂的一种移植。 这是由于 GameInput 受到了 XInput 的简单(且易于使用)的编程模型的很大影响,因此许多 XInput API 都是与 GameInput 中的等效函数一一对应的。
主要差异
XInput 和 GameInput 之间的主要差异将在下面几个部分中讨论。
C 与 C++
XInput API 是扁平 C 函数的集合。 另一方面,GameInput 是 C++ 函数,它使用接口(就像图形和音频 API 一样)。 实际上,这并不会使使用 GameInput API 的代码变得复杂,不会影响性能,并且一旦您更熟悉 GameInput 的工作原理,它就会变得通俗易懂。
重要的是要理解虽然这些接口可能看起来像 COM,但其实不然。 使用这些接口只需要对引用计数有基本的了解。 有关更多信息,可查看 GameInput 基础知识主题的“接口”部分。
获取输入
在 XInput 中,大多数游戏都会循环访问用户索引,直到找到一个的连接设备,然后从该设备读取状态。 游戏通常会记住用户索引,因此下次不必循环。 例如,下面的代码是提示用户在其控制器上“按 A”的游戏的典型代码:
// This function looks for a gamepad that currently has the "A" button pressed.
void FindActiveGamepad()
{
for (DWORD index = 0; index < XUSER_MAX_COUNT; index++)
{
XINPUT_STATE state;
if (XInputGetState(index, &state) == ERROR_SUCCESS)
{
if (state.Gamepad.wButtons & XINPUT_GAMEPAD_A)
{
// Found the user's gamepad at this index.
}
}
}
}
在 GameInput 中,您首先获得输入而不指定设备,如果需要,可以查询输入来自哪个设备。 代码看起来很相似,但是不需要显式设备枚举可以使算法更简单。
// This function looks for a gamepad that currently has the "A" button pressed.
void FindActiveGamepad(IGameInput * gameInput)
{
// This checks for input from all gamepads simultaneously.
IGameInputReading * reading;
if (SUCCEEDED(gameInput->GetCurrentReading(GameInputKindGamepad, nullptr, &reading)))
{
GameInputGamepadState state;
reading->GetGamepadState(&state);
if (state.buttons & GameInputGamepadA)
{
// Found the user's gamepad. At this point we can
// get the device that generated this input, and then
// pass that into future calls to the GetCurrentReading
// method to receive input only from that gamepad.
}
reading->Release():
}
}
代码并不像 XInput 那么简单,但它非常相似。 随着您对 GameInput API 愈发熟悉,您将看到此模型如何提供强大的选项来处理 XInput 中不存在的输入。
此外值得一提的是,XInput 会将来自触发器的模拟值作为 BYTE 类型返回,将来自控制杆的模拟值作为 SHORT 类型返回。 使用 GameInput API,这些模拟值将作为浮点值返回,对于触发器是从 0 到 1 的浮点值,对于控制杆是从 -1 到 1 的浮点值。
Rumble 反馈
在 XInput 中,游戏调用 XInputSetState
以向设备发送 rumble(振动)命令。 在 GameInput 中,游戏需要获得设备的 IGameInputDevice 实例,然后调用其 SetRumbleState 方法。 这两种方法的用途类似。 这是您在设备接口上调用函数时的示例,而不是仅仅是设备标识符。
应用程序焦点
在主机上,GameInput 仅在应用程序处于焦点时才提供对应用程序的输入。 否则,返回的状态包含中性或“静止”值,就好像用户根本没有触摸设备一样。 这消除了处理焦点变化的额外输入代码的需要(例如,调用 XInputEnable
)。
在电脑上,默认情况下,输入将转到所有进程。 将来,此行为将可通过使用 SetFocusPolicy 方法进行更改。
XInputOnGameInput 包装器
Microsoft 游戏开发工具包 (GDK) 附带名为 XInputOnGameInput.h
的头文件,它包含基于 GameInput 的 XInput API 的实现。 我们建议直接移植到 GameInput,特别是如果需要键盘和鼠标或其他输入设备。 但是,XInputOnGameInput 包装器可用于帮助引导初始移植工作,而无需对现有 XInput 代码进行任何更改。
要使用 XInputOnGameInput 包装器,只需替换此代码:
#include <XInput.h>
替换为:
#include <XInputOnGameInput.h>
using namespace XInputOnGameInput;
然后重新编译您的代码。
XInput 包装器代码的实现完全在头文件中进行,因此它也可以作为使用 GameInput API 的示例进行检查,和/或根据需要进行修改。
XInput 和 XInputOnGameInput 之间的差异
一般情况下,XInputOnGameInput 包装器是旧版 XInput API 的直接替代品。 但有几个细微的区别:
为简单起见,只有游戏板设备支持已编码到包装器中。 如果您需要支持赛车方向盘或街机摇杆等其他设备,请直接使用 GameInput,或者在 XInputOnGameInput 代码中添加对这些设备的支持。
仅当焦点在游戏上时,包装器才会返回游戏手柄输入。 当焦点不在游戏上时,返回的任何游戏手柄状态均设置为中性或“其他”值,就像用户未触碰游戏手柄一样。 无论对
XInputEnable
进行任何调用(或不进行调用),都会这样。XUSER_MAX_COUNT
的值已从 4 增加到 8。 对于大多数旧版 XInput 代码来说,这通常应该是透明的。 但是,请务必仔细检查在您的代码中对XInputGetKeystroke
函数的任何使用,以便确保没有任何内容是硬编码的,从而假定在XINPUT_KEYSTROKE
结构的UserIndex
成员中返回 4 的最大值。 否则,可能会发生缓冲区溢出。添加了一些新函数(见下文),如果您打算继续在生产代码中使用 XInput 包装器,那么这些函数应该是有意义的。
在生产代码中使用 XInputOnGameInput
XInputOnGameInput 包装器的编写方式决定,它具有高性能且并未锁定,还继承了 GameInput API 的所有性能优化,因此适用于生产代码。 它还继承 GameInput 的更广泛的设备支持(例如常见的 HID 游戏手柄),并向 API 添加以下新功能:
XInputSetStateEx
类似于XInputSetState
,但增加了对扳机键马达的支持。XInputGetStateWithToken
类似于XInputGetState
,但允许调用方提供 D3DX 帧管道令牌,以将特定输入读取与图形帧相关联,以便以后在 PIX 中进行分析。注意
在 5 月预览版中,
XInputGetStateWithToken
当前的行为与XInputGetState
相同,因为基础 GameInput 代码未完全实现。XInputGetDeviceId
会返回给定用户索引处的设备的APP_LOCAL_DEVICE_ID
。 在 IGameInput 上将此 ID 传递给 FindDeviceFromId 方法,将返回该用户索引对应的 IGameInputDevice。 然后,可用它访问 GameInput API 中未通过 XInput 包装器公开的其他功能。
优化包装器代码
默认情况下,XInputOnGameInput 包装器配置为与旧版 XInput API 向下兼容。 不要求 100% 兼容行为的游戏可以通过定义以下任何预处理器宏来微调包装器的行为和性能:
XINPUT_ON_GAMEINPUT_EXPLICIT_INITIALIZATION
默认情况下,XInput 包装器会在第一次调用任何包装器函数时自动延迟初始化基础 GameInput API。 这确保了与现有 XInput 代码的嵌入式兼容性,但有几个小缺点:
第一个 XInput 包装器函数调用的执行时间将比平时长。
每次调用 XInput 包装器函数时,都必须执行检查以查看是否已执行延迟初始化。 这是对全局变量的简单测试,因此分支预测器应该改善成本,但它是额外的开销。
虽然它应该不会失败,但无法判断基础 GameInput API 的延迟初始化是否成功。
在(由于模块卸载或进程终止而)清理 XInput 包装器的全局变量之前,不会释放基础 IGameInput 实例。
游戏可通过定义 XINPUT_ON_GAMEINPUT_EXPLICIT_INITIALIZATION
宏来手动控制该包装器的初始化和关闭。 这增加了两个新函数 XInputOnGameInputInitialize
和 XInputOnGameInputUninitialize
,可以调用它们来精确控制何时发生初始化和关闭。
XINPUT_ON_GAMEINPUT_NO_XINPUTENABLE
实现 XInputEnable
函数所需的代码向 XInputGetState
, XInputGetStateWithToken
、XInputSetState
、XInputSetStateEx
和 XInputGetKeystroke
函数的每一个调用都添加了额外开销。 如果您的代码未调用 XInputEnable
,或者它可以轻松地从您的代码中消除,则定义 XINPUT_ON_GAMEINPUT_NO_XINPUTENABLE
宏将删除对 XInputEnable
的支持以及它所带来的关联开销。 只要焦点发生变化,基础 GameInput 代码就会自动执行 XInputEnable
的功能,因此如果可能,大多数游戏都想要定义该宏。
XINPUT_ON_GAMEINPUT_NO_XINPUTGETKEYSTROKE
实现 XInputGetKeystroke
函数所需的代码为包装器的实现添加了一些额外的函数和变量。 它不会增加任何其他 XInput API 函数的开销,但如果您的代码没有调用 XInputGetKeystroke
,则可以定义 XINPUT_ON_GAMEINPUT_NO_XINPUTGETKEYSTROKE
宏来略微减少 XInput 包装器的代码/数据大小。