利用高清鼠标移动

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

很多公司都在生产高清鼠标设备,例如 Microsoft 和罗技。 随着高分辨率鼠标设备的日益普及,开发人员有必要了解如何以最佳方式使用这些设备生成的信息。 本文重点介绍在第一人称射击游戏等游戏中优化高清鼠标输入的性能的最佳方法。

检索鼠标移动数据

下面是检索鼠标数据的 3 种主要方法:

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

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_MOUSEMOVE 消息相比,处理 WM_INPUT 消息更为复杂,但 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 数据;与直接读取 WM_INPUT 相比,使用 DirectInput API 会增加更多的开销。 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 游戏可以为用户提供更精确的控制。