65.9K
CodeProject 正在变化。 阅读更多。
Home

使用原始输入 API 处理操纵杆输入

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (12投票s)

2011年4月23日

CPOL

9分钟阅读

viewsIcon

181406

downloadIcon

16313

解释从原始输入 API 接收到的操纵杆数据的基本方法

Screenshot.png

引言

Microsoft 不鼓励在游戏中使用 DirectInput 来处理键盘和鼠标输入,但它仍然推荐用于处理来自旧式游戏控制器或其他游戏控制器的输入。新应用程序应使用 原始输入 API,例如,利用每英寸 800 DPI 甚至更高分辨率的计算机鼠标生成的数据。此外,Microsoft 还引入了 XInput,允许应用程序接收来自 Xbox 360 控制器的输入。因此,游戏开发者在设计输入系统时,必须处理多达三种不同的 API。

在本文中,我将展示如何使用原始输入 API 来处理游戏控制器/游戏手柄数据,至少可以使 DirectInput 的使用变得过时。

背景

随着 Windows XP 的推出,Microsoft 引入了原始输入 API,以支持键盘和鼠标以外的其他人机接口设备 (HID)。应用程序可以使用此 API 从任何可以想象到的 HID 获取数据。但是,除了键盘和鼠标输入之外,该 API 没有提供任何结构或函数来解释来自设备的数据,因此对于希望利用大量可用游戏控制器和游戏手柄的游戏应用程序来说,它是无用的。原始输入 API 文档没有提及如何检索诸如按钮数量、轴数量、是否存在操纵杆等有价值的信息。(我猜这就是它被称为“原始”的原因 ;-))

在开发游戏引擎的过程中,我想为我的 Logitech RumblePad 2 添加支持。对于鼠标和键盘输入,我已经使用了原始输入 API,因此在尝试实现管理 DirectInput 及其设备对象的第二条代码分支之前,先查看它似乎是合乎逻辑的。最终,我找到了一个使用原始输入 API 和 HIDClass 驱动程序的解决方案(参见 Windows 驱动程序工具包中的人机输入设备)。

本文接下来的几节将展示一种检索原始游戏控制器数据的基本方法以及如何解释它。

使用代码之前

如前所述,示例应用程序与 HIDClass 驱动程序紧密配合。要编译代码,您必须从 Microsoft 网站下载 Windows 驱动程序工具包 (WDK) 并相应地设置项目路径。您需要链接到 *hid.lib*,如果您遇到大量编译错误,您可能忘记包含“*\inc\crt”路径。

步骤 1:注册游戏控制器设备

使用原始输入 API 的每个应用程序首先要注册它感兴趣的数据来源设备的类型。

case WM_CREATE:
    {
        RAWINPUTDEVICE rid;

        rid.usUsagePage = 1;
        rid.usUsage     = 4; // Joystick
        rid.dwFlags     = 0;
        rid.hwndTarget  = hWnd;

        if(!RegisterRawInputDevices(&rid, 1, sizeof(RAWINPUTDEVICE)))
            return -1;
    }
    return 0;

这是最简单的部分。对于游戏控制器设备,我们只需要设置 usUsagePage = 1usUsage = 4。Usage Page 和 Usage 的术语源自 USB 实现者论坛的 HID Usage Tables。它们指定了各种输入设备及其控件的类型,即 1 是通用桌面控件页的 ID,4 是游戏控制器 Usage 名称的 ID。此外,要接收 WM_INPUT 消息,我们将 hwndTarget 设置为我们的窗口句柄。

步骤 2:检索设备数据

WM_INPUT 的情况下,我们获得指向包含游戏控制器数据的 RAWINPUT 结构的指针。

case WM_INPUT:
    {
        PRAWINPUT pRawInput;
        UINT      bufferSize;
        HANDLE    hHeap;

        GetRawInputData((HRAWINPUT)lParam, RID_INPUT, NULL, 
		&bufferSize, sizeof(RAWINPUTHEADER));

        hHeap     = GetProcessHeap();
        pRawInput = (PRAWINPUT)HeapAlloc(hHeap, 0, bufferSize);
        if(!pRawInput)
            return 0;

        GetRawInputData((HRAWINPUT)lParam, RID_INPUT, 
		pRawInput, &bufferSize, sizeof(RAWINPUTHEADER));
        ParseRawInput(pRawInput);

        HeapFree(hHeap, 0, pRawInput);
    }
    return 0;

GetRawInputDataWM_INPUT 消息提供的句柄转换为 RAWINPUT 指针。然后,我们将此指针传递给 ParseRawInput,后者执行大部分处理工作。

RAWINPUT 结构为我们提供了设备句柄、输入类型和输入数据本身。对于鼠标和键盘输入,我们可以使用 RAWMOUSERAWKEYBOARD 结构,但对于任何其他设备,我们必须使用 RAWHID 结构,它只包含原始输入数据,作为一个字节数组。现在就取决于我们来解释这个数组了。

步骤 3:解释设备数据

此时,第一个重要观察是,这个数组实际上包含了我们游戏控制器(即我的 RumblePad)的状态。如果我们按下按钮或移动一个轴,我们会收到一个新的 WM_INPUT 消息和一个反映游戏控制器新状态的数组。这样,我们就可以反汇编数组并确定对应于按钮的位和对应于轴的字节。但这只适用于我们的特殊游戏控制器,而不适用于大多数现有设备。那么,我们如何在运行时解释数据呢?

原始输入 API 允许我们使用 GetRawInputDeviceInfo 函数访问一些设备信息。给定原始输入设备的句柄和 RIDI_PREPARSEDDATA 命令,我们可以获得所谓的“预解析数据”。WDK 文档只说,“_HIDP_PREPARSED_DATA 结构的内部结构保留用于内部系统使用”。以下是获取此数据块的代码。

CHECK( GetRawInputDeviceInfo(pRawInput->header.hDevice, 
	RIDI_PREPARSEDDATA, NULL, &bufferSize) == 0 );
CHECK( pPreparsedData = (PHIDP_PREPARSED_DATA)HeapAlloc(hHeap, 0, bufferSize) );
CHECK( (int)GetRawInputDeviceInfo(pRawInput->header.hDevice, 
	RIDI_PREPARSEDDATA, pPreparsedData, &bufferSize) >= 0 );

GetRawInputDeviceInfo 的第一次调用仅给出预解析数据的大小。我们使用该大小来分配一个适合数据的块,并在第二次调用 GetRawInputDeviceInfo 中检索它。CHECK 是我用来处理错误的简短宏。如果参数中的表达式评估为 FALSE0,则宏会释放任何已分配的内存并从函数返回。

现在,这里是全局图景。我们通过每个 WM_INPUT 消息获得的设备数据实际上是来自 HIDClass 驱动程序的 HID 报告 系列,我们可以使用预解析数据来解包 HID 报告并确定按下的按钮和轴值。(我猜,这正是 DirectInput 的实现方式。)

首先,我们确定游戏控制器有多少按钮。为此,我们使用来自 hid.dllHidP_* 函数。显然,hid.dll 是用于与 HIDClass 驱动程序通信的用户模式库,也是 Windows 为此类 I/O 提供的最低级库。

CHECK( HidP_GetCaps(pPreparsedData, &Caps) == HIDP_STATUS_SUCCESS )
CHECK( pButtonCaps = (PHIDP_BUTTON_CAPS)HeapAlloc
	(hHeap, 0, sizeof(HIDP_BUTTON_CAPS) * Caps.NumberInputButtonCaps) );

capsLength = Caps.NumberInputButtonCaps;
CHECK( HidP_GetButtonCaps(HidP_Input, pButtonCaps, 
	&capsLength, pPreparsedData) == HIDP_STATUS_SUCCESS )
g_NumberOfButtons = pButtonCaps->Range.UsageMax - pButtonCaps->Range.UsageMin + 1;

HidP_GetCaps 告诉我们不同功能查询的数量、设备的使用情况和使用页面以及 HID 报告的长度(以字节为单位)。要查询输入按钮的数量,我们分配一个适合多个 HIDP_BUTTON_CAPS 结构的缓冲区,然后调用 HidP_GetButtonCaps 来填充它。HidP_GetButtonCaps 的第一个参数指定报告类型。HID 可以有输入报告(例如,用于按钮、旋钮、推子、触摸屏等)和输出报告(例如,用于 LED、显示器、力反馈等)。接下来的三个参数是指向我们分配的缓冲区的指针、其长度以及指向设备预解析数据的指针。

HidP_GetButtonCaps 返回我们游戏控制器每种 HID 控件 的功能,即它的 Usage。pButtonCaps[0].UsagePage 指示第一组 HID 控件的使用情况(请参阅 HID Usage Tables 的表 1)。UsageMinUsageMax 指示使用范围的下限和上限,即 HIDClass 驱动程序为游戏控制器按钮返回的索引范围。按钮的数量则为 UsageMaxUsageMin 之差加一。

HidP_GetButtonCaps 返回 HIDP_BUTTON_CAPS 结构数组的原因是,一个游戏控制器可以为不同的按钮分配不同的 Usage。例如,我的 RumblePad 有一个标有“Vibration”的按钮,用于启用或禁用力反馈效果。第一个数组元素的 UsagePage 成员指示十二个操作按钮的按钮页面,而第二个数组元素指示“Mode”和“Vibration”按钮的供应商定义的使用情况。

现在我们获取值功能数组。此数组指定了具有两个以上状态(即按下或释放)的 HID 控件的功能。这些控件通常具有一个值范围。例如,我的 RumblePad 有两个模拟摇杆,即四个轴,每个轴的值范围从 0x000xFF,其中 0x80 等于摇杆的居中位置。

CHECK( pValueCaps = (PHIDP_VALUE_CAPS)HeapAlloc
	(hHeap, 0, sizeof(HIDP_VALUE_CAPS) * Caps.NumberInputValueCaps) );
capsLength = Caps.NumberInputValueCaps;
CHECK( HidP_GetValueCaps(HidP_Input, pValueCaps, 
	&capsLength, pPreparsedData) == HIDP_STATUS_SUCCESS )

HIDP_VALUE_CAPS 结构的 UsagePage 成员再次指示值的用法(即哪个轴)。PhysicalMinPhysicalMax 指示值的范围。请注意,本文提供的示例代码不使用这些。示例代码假定范围在 0x000xFF 之间。

现在,我们从 pRawInput->data.hid.bRawData 中的 HID 输入报告中获取操作按钮的状态。

usageLength = g_NumberOfButtons;
CHECK(
    HidP_GetUsages(
        HidP_Input, pButtonCaps->UsagePage, 0, usage, 
	&usageLength, pPreparsedData,
        (PCHAR)pRawInput->data.hid.bRawData, pRawInput->data.hid.dwSizeHid
    ) == HIDP_STATUS_SUCCESS );

ZeroMemory(bButtonStates, sizeof(bButtonStates));
for(i = 0; i < usageLength; i++)
    bButtonStates[usage[i] - pButtonCaps->Range.UsageMin] = TRUE;

HidP_GetUsages 仅返回 usage 中实际按下的按钮。我猜这段代码不言自明。

最后,我们从 HID 输入报告中获取值控件的状态。

for(i = 0; i < Caps.NumberInputValueCaps; i++)
{
    CHECK(
        HidP_GetUsageValue(
            HidP_Input, pValueCaps[i].UsagePage, 0, 
		pValueCaps[i].Range.UsageMin, &value, pPreparsedData,
            (PCHAR)pRawInput->data.hid.bRawData, pRawInput->data.hid.dwSizeHid
        ) == HIDP_STATUS_SUCCESS );

    switch(pValueCaps[i].Range.UsageMin)
    {
    case 0x30:    // X-axis
        lAxisX = (LONG)value - 128;
        break;

    case 0x31:    // Y-axis
        lAxisY = (LONG)value - 128;
        break;

    case 0x32: // Z-axis
        lAxisZ = (LONG)value - 128;
        break;

    case 0x35: // Rotate-Z
        lAxisRz = (LONG)value - 128;
        break;

    case 0x39:    // Hat Switch
        lHat = value;
        break;
    }
}

HidP_GetUsageValue 的工作方式与 HidP_GetUsages 相同。它从 HID 输入报告中提取 UsageMinUsageMax 中的用法以及 HID 控件的当前 value。对于我的小 RumblePad,我只检查了 UsageMin 以区分轴。

我没有解释的示例代码的其余部分以适当的方式绘制了从 HID 输入报告中提取的值。就是这样!

进一步的考量

我在本文中展示了,通过 HIDClass 驱动程序的协助,可以使用原始输入 API 处理游戏控制器的输入。我认为这个 API 只是 HID 用户模式库之上的一个层,位于 DirectInput 之上。实际上,也可以绕过原始输入 API,仅通过使用 HID 用户库来完成所有 HID 通信。只需查看 WDK 中的 HClient 示例。它做了与我解释 HID 输入报告相同的事情,而无需使用原始输入。在开发游戏输入系统时,您无法获得更低级别的接口,因为 HID 用户模式库是唯一将您的应用程序与内核模式分隔开的接口,尽管我怀疑直接使用 HID 用户库不会带来太多速度提升。

最后但同样重要的是,我提供了一个使用 HID 用户模式库直接进行操作时需要执行的任务的简要概述。

  1. 使用 SetupAPI 枚举 HIDClass 设备设置类中的设备。
  2. 通过 SetupDiGetDeviceInterfaceDetail 获取特定设备的实例路径。
  3. 使用实例路径通过 CreateFile 打开设备句柄。
  4. 使用 ReadFileWriteFileHidP_* 函数与 HID 进行通信。
  5. 关闭句柄。

请注意,您不能对鼠标或键盘设备使用此方法,因为 Windows 独占使用这些设备(参见 Windows 用于系统使用的顶级集合)。

参考文献

历史

  • 2011 年 4 月 23 日:初始发布
© . All rights reserved.