利用 High-Definition 鼠标移动

标准计算机鼠标以每英寸 400 点(DPI)返回数据,而高清鼠标以 800 DPI 或更高版本生成数据。 这使得来自高清鼠标的输入比标准鼠标更精确。 但是,无法通过标准WM_MOUSEMOVE消息获取高清数据。 通常,游戏将受益于高清鼠标设备,但仅使用WM_MOUSEMOVE获取鼠标数据的游戏将无法访问鼠标的完整未筛选分辨率。

许多公司正在制造高清鼠标设备,如Microsoft和 Logitech。 随着高分辨率鼠标设备的日益普及,开发人员必须了解如何以最佳方式使用这些设备生成的信息。 本文重点介绍优化游戏(如第一人称射击器)中高清鼠标输入性能的最佳方法。

检索鼠标移动数据

下面是检索鼠标数据的三个主要方法:

每个方法都有优点和缺点,具体取决于数据的使用方式。

WM_MOUSEMOVE

读取鼠标移动数据的最简单方法是通过WM_MOUSEMOVE消息。 下面是如何从WM_MOUSEMOVE消息读取鼠标移动数据的示例:

case WM_MOUSEMOVE:
{
    int xPosAbsolute = GET_X_PARAM(lParam); 
    int yPosAbsolute = GET_Y_PARAM(lParam);
    // ...
    break;
}

WM_MOUSEMOVE数据的主要缺点是它仅限于屏幕分辨率。 这意味着,如果鼠标稍微移动,但不足以导致指针移动到下一个像素,则不会生成WM_MOUSEMOVE消息。 因此,使用此方法读取鼠标移动会否定高清输入的优点。

然而,WM_MOUSEMOVE的优势在于,Windows 将指针加速(也称为弹道)应用于原始鼠标数据,这使得鼠标指针的行为与客户预期一样。 这使得WM_MOUSEMOVE成为指针控制的首选选项,相对于WM_INPUT或DirectInput,因为它为用户带来了更自然的操作体验。 虽然WM_MOUSEMOVE非常适合移动鼠标指针,但它并不适合移动第一人称相机,因为高清精度将丢失。

有关WM_MOUSEMOVE的详细信息,请参阅 WM_MOUSEMOVE

WM_INPUT

获取鼠标数据的第二种方法是读取WM_INPUT消息。 处理WM_INPUT消息比处理WM_MOUSEMOVE消息更为复杂,但WM_INPUT消息直接从人机接口设备(HID)堆栈读取,并反映高分辨率结果。

若要从WM_INPUT消息中读取鼠标移动数据,必须先注册设备;以下代码提供了一个示例:

// you can #include <hidusage.h> for these defines
#ifndef HID_USAGE_PAGE_GENERIC
#define HID_USAGE_PAGE_GENERIC         ((USHORT) 0x01)
#endif
#ifndef HID_USAGE_GENERIC_MOUSE
#define HID_USAGE_GENERIC_MOUSE        ((USHORT) 0x02)
#endif

RAWINPUTDEVICE Rid[1];
Rid[0].usUsagePage = HID_USAGE_PAGE_GENERIC; 
Rid[0].usUsage = HID_USAGE_GENERIC_MOUSE; 
Rid[0].dwFlags = RIDEV_INPUTSINK;   
Rid[0].hwndTarget = hWnd;
RegisterRawInputDevices(Rid, 1, sizeof(Rid[0]));

以下代码处理应用程序的 WinProc 处理程序中的WM_INPUT消息:

case WM_INPUT: 
{
    UINT dwSize = sizeof(RAWINPUT);
    static BYTE lpb[sizeof(RAWINPUT)];

    GetRawInputData((HRAWINPUT)lParam, RID_INPUT, lpb, &dwSize, sizeof(RAWINPUTHEADER));

    RAWINPUT* raw = (RAWINPUT*)lpb;

    if (raw->header.dwType == RIM_TYPEMOUSE) 
    {
        int xPosRelative = raw->data.mouse.lLastX;
        int yPosRelative = raw->data.mouse.lLastY;
    } 
    break;
}

使用WM_INPUT的优点是游戏在可能的最低级别接收来自鼠标的原始数据。

缺点是,WM_INPUT没有对其数据应用加速和减速曲线,因此,如果要使用此数据驱动光标,还需要额外努力才能使光标像在 Windows 中那样动作。 有关应用指针弹道的详细信息,请参阅适用于 Windows XP 指针弹道。

有关WM_INPUT的详细信息,请参阅 关于原始输入

DirectInput

DirectInput 是一组 API 调用,用于抽象系统上的输入设备。 在内部,DirectInput 创建另一个线程来读取WM_INPUT数据,使用 DirectInput API 会增加比直接读取WM_INPUT更多的开销。 DirectInput 仅适用于从 DirectInput 游戏杆读取数据;但是,如果只需要支持 Windows 的控制器,请改用 XInput。 总的来说,从鼠标或键盘设备读取数据时,使用 DirectInput 没有优点,因此不建议在这些方案中使用 DirectInput。

DirectInput的复杂性(如以下代码所示)与前面所述的方法进行比较。 创建 DirectInput 鼠标需要以下调用集:

LPDIRECTINPUT8 pDI;
LPDIRECTINPUTDEVICE8 pMouse;

hr = DirectInput8Create(GetModuleHandle(NULL), DIRECTINPUT_VERSION, IID_IDirectInput8, (VOID**)&pDI, NULL);
if(FAILED(hr))
    return hr;

hr = pDI->CreateDevice(GUID_SysMouse, &pMouse, NULL);
if(FAILED(hr))
    return hr;

hr = pMouse->SetDataFormat(&c_dfDIMouse2);
if(FAILED(hr))
    return hr;

hr = pMouse->SetCooperativeLevel(hWnd, DISCL_NONEXCLUSIVE | DISCL_FOREGROUND);
if(FAILED(hr))
    return hr;

if(!bImmediate)
{
    DIPROPDWORD dipdw;
    dipdw.diph.dwSize       = sizeof(DIPROPDWORD);
    dipdw.diph.dwHeaderSize = sizeof(DIPROPHEADER);
    dipdw.diph.dwObj        = 0;
    dipdw.diph.dwHow        = DIPH_DEVICE;
    dipdw.dwData            = 16; // Arbitrary buffer size

    if(FAILED(hr = pMouse->SetProperty(DIPROP_BUFFERSIZE, &dipdw.diph)))
        return hr;
}

pMouse->Acquire();

然后,DirectInput 鼠标设备可以在每个帧中读取。

DIMOUSESTATE2 dims2; 
ZeroMemory(&dims2, sizeof(dims2));

hr = pMouse->GetDeviceState(sizeof(DIMOUSESTATE2), &dims2);
if(FAILED(hr)) 
{
    hr = pMouse->Acquire();
    while(hr == DIERR_INPUTLOST) 
        hr = pMouse->Acquire();

    return S_OK; 
}

int xPosRelative = dims2.lX;
int yPosRelative = dims2.lY;

总结

总的来说,接收高清鼠标移动数据的最佳方法是WM_INPUT。 如果用户只是移动鼠标指针,请考虑使用WM_MOUSEMOVE以避免需要执行指针弹道。 即使鼠标不是高清鼠标,这两条窗口消息也能正常工作。 通过支持高清,Windows 游戏可以为用户提供更精确的控制。