RGFW 内部原理:原始鼠标输入和鼠标锁定





1.00/5 (1投票)
本教程解释了如何在 X11、WinAPI、Cocoa 和 Emscripten 中锁定鼠标指针并启用原始鼠标输入。
引言
RGFW 是一个轻量级的单头文件窗口库,其源代码可以在 此处 找到。本教程基于其源代码。
当您创建锁定鼠标指针的应用程序时,例如具有第一人称摄像机的游戏,禁用鼠标指针非常重要。这意味着将鼠标指针锁定在屏幕中央并获取原始输入。
这种方法的唯一替代方法是一种“hack”,即在鼠标移动时将其拉回到窗口中央。然而,这是一种 hack,因此可能存在 bug 且在所有操作系统上都无法正常工作。因此,通过使用原始输入来正确锁定鼠标非常重要。
本教程解释了 RGFW 如何处理原始鼠标输入,以便您可以理解如何自己实现它。
概述
所需步骤的快速概述
- 锁定鼠标指针
- 居中鼠标指针
- 启用原始输入
- 处理原始输入
- 禁用原始输入
- 解锁鼠标指针
当用户请求 RGFW 锁定鼠标指针时,RGFW 会启用一个位标志,表示鼠标指针已锁定。
win->_winArgs |= RGFW_HOLD_MOUSE;
步骤 1(锁定鼠标指针)
在 X11 上,可以通过 XGrabPointer
来锁定鼠标指针。
XGrabPointer(display, window, True, PointerMotionMask, GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
这使得窗口对指针拥有完全控制权。
在 Windows 上,ClipCursor
将鼠标指针锁定在屏幕上的特定矩形内。这意味着我们必须找到窗口在屏幕上的矩形,然后将鼠标剪辑到该矩形。
还使用:GetClientRect
)和ClientToScreen
//First get the window size (the RGFW_window struct also includes this information, but using this ensures it's correct)
RECT clipRect;
GetClientRect(window, &clipRect);
// ClipCursor needs screen coordinates, not coordinates relative to the window
ClientToScreen(window, (POINT*) &clipRect.left);
ClientToScreen(window, (POINT*) &clipRect.right);
// Now we can lock the cursor
ClipCursor(&clipRect);
在 MacOS 和 Emscripten 上,启用原始输入的函数也会锁定鼠标指针。因此,我将在第 4 步介绍其功能。
步骤 2(居中鼠标指针)
锁定鼠标指针后,应将其置于屏幕中央。这可确保鼠标指针锁定在正确的位置,不会干扰其他任何内容。
RGFW 使用 RGFW 函数 RGFW_window_moveMouse
将鼠标移动到窗口中央。
在 X11 上,可以使用 XWarpPointer
将鼠标指针移动到窗口中央。
XWarpPointer(display, None, window, 0, 0, 0, 0, window_width / 2, window_height / 2);
在 Windows 上,使用 SetCursorPos
。
SetCursorPos(window_x + (window_width / 2), window_y + (window_height / 2));
在 MacOS 上,使用 CGWarpMouseCursorPosition
。
CGWarpMouseCursorPosition(window_x + (window_width / 2), window_y + (window_height / 2));
在 Emscripten 上,RGFW 不会移动鼠标。
步骤 3(启用原始输入)
在 X11 上,使用 XI 来启用原始输入。
// mask for XI and set mouse for raw mouse input ("RawMotion")
unsigned char mask[XIMaskLen(XI_RawMotion)] = { 0 };
XISetMask(mask, XI_RawMotion);
// set up X1 struct
XIEventMask em;
em.deviceid = XIAllMasterDevices;
em.mask_len = sizeof(mask);
em.mask = mask;
//Enable raw input using the structure
XISelectEvents(display, XDefaultRootWindow(display), &em, 1);
在 Windows 上,您需要设置 RAWINPUTDEVICE 结构并使用 RegisterRawInputDevices 来启用它。
const RAWINPUTDEVICE id = { 0x01, 0x02, 0, window };
RegisterRawInputDevices(&id, 1, sizeof(id));
在 MacOS 上,您只需要运行 CGAssociateMouseAndMouseCursorPosition。这也会通过分离鼠标指针和鼠标移动来锁定鼠标指针。
CGAssociateMouseAndMouseCursorPosition(0);
在 Emscripten 上,您只需要请求用户锁定指针。
emscripten_request_pointerlock("#canvas", 1);
步骤 4(处理原始输入事件)
所有这些都发生在事件循环期间。
对于 X11,您必须处理正常的 MotionNotify,手动将其转换为原始输入。要检查原始鼠标输入事件,您需要使用 GenericEvent。
switch (E.type) {
(...)
case MotionNotify:
/* check if mouse hold is enabled */
if ((win->_winArgs & RGFW_HOLD_MOUSE)) {
/* convert E.xmotion to raw input by subtracting the previous point */
win->event.point.x = win->_lastMousePoint.x - E.xmotion.x;
win->event.point.y = win->_lastMousePoint.y - E.xmotion.y;
}
break;
case GenericEvent: {
/* MotionNotify is used for mouse events if the mouse isn't held */
if (!(win->_winArgs & RGFW_HOLD_MOUSE)) {
XFreeEventData(display, &E.xcookie);
break;
}
XGetEventData(display, &E.xcookie);
if (E.xcookie.evtype == XI_RawMotion) {
XIRawEvent *raw = (XIRawEvent *)E.xcookie.data;
if (raw->valuators.mask_len == 0) {
XFreeEventData(display, &E.xcookie);
break;
}
double deltaX = 0.0f;
double deltaY = 0.0f;
/* check if relative motion data exists where we think it does */
if (XIMaskIsSet(raw->valuators.mask, 0) != 0)
deltaX += raw->raw_values[0];
if (XIMaskIsSet(raw->valuators.mask, 1) != 0)
deltaY += raw->raw_values[1];
//The mouse must be moved back to the center when it moves
XWarpPointer(display, None, window, 0, 0, 0, 0, window_width / 2, window_height / 2);
win->event.point = RGFW_POINT((i32)deltaX, (i32)deltaY);
}
XFreeEventData(display, &E.xcookie);
break;
}
在 Windows 上,您只需要处理 WM_INPUT
事件并检查原始移动输入。
switch (msg.message) {
(...)
case WM_INPUT: {
/* check if the mouse is being held */
if (!(win->_winArgs & RGFW_HOLD_MOUSE))
break;
/* get raw data as an array */
unsigned size = sizeof(RAWINPUT);
static RAWINPUT raw[sizeof(RAWINPUT)];
GetRawInputData((HRAWINPUT)msg.lParam, RID_INPUT, raw, &size, sizeof(RAWINPUTHEADER));
//Make sure raw data is valid
if (raw->header.dwType != RIM_TYPEMOUSE || (raw->data.mouse.lLastX == 0 && raw->data.mouse.lLastY == 0) )
break;
win->event.point.x = raw->data.mouse.lLastX;
win->event.point.y = raw->data.mouse.lLastY;
break;
}
在 macOS 上,您可以在使用 deltaX 和 deltaY 获取鼠标点时,将鼠标输入视为正常。
switch (objc_msgSend_uint(e, sel_registerName("type"))) {
case NSEventTypeLeftMouseDragged:
case NSEventTypeOtherMouseDragged:
case NSEventTypeRightMouseDragged:
case NSEventTypeMouseMoved:
if ((win->_winArgs & RGFW_HOLD_MOUSE) == 0) // if the mouse is not held
break;
NSPoint p;
p.x = ((CGFloat(*)(id, SEL))abi_objc_msgSend_fpret)(e, sel_registerName("deltaX"));
p.y = ((CGFloat(*)(id, SEL))abi_objc_msgSend_fpret)(e, sel_registerName("deltaY"));
win->event.point = RGFW_POINT((i32) p.x, (i32) p.y));
在 Emscripten 上,鼠标事件可以像正常一样被检查,除了我们将使用并翻转 e->movementX/Y。
EM_BOOL Emscripten_on_mousemove(int eventType, const EmscriptenMouseEvent* e, void* userData) {
if ((RGFW_root->_winArgs & RGFW_HOLD_MOUSE) == 0) // if the mouse is not held
return
RGFW_point p = RGFW_POINT(e->movementX, e->movementY);
}
步骤 5(禁用原始输入)
最后,RGFW 允许禁用原始输入并解锁鼠标指针以恢复正常鼠标输入。
首先,RGFW 会禁用位标志。
win->_winArgs ^= RGFW_HOLD_MOUSE;
在 X11 上,首先,您必须创建一个带有空掩码的结构。这将禁用原始输入。
unsigned char mask[] = { 0 };
XIEventMask em;
em.deviceid = XIAllMasterDevices;
em.mask_len = sizeof(mask);
em.mask = mask;
XISelectEvents(display, XDefaultRootWindow(display), &em, 1);
对于 Windows,您传递一个 RAWINPUTDEVICE 结构,RIDEV_REMOVE
以禁用原始输入。
const RAWINPUTDEVICE id = { 0x01, 0x02, RIDEV_REMOVE, NULL };
RegisterRawInputDevices(&id, 1, sizeof(id));
在 MacOS 和 Emscripten 上,解锁鼠标指针也会禁用原始输入。
步骤 6(解锁鼠标指针)
在 X11 上,可以使用 XUngrabPoint
来解锁鼠标指针。
XUngrabPointer(display, CurrentTime);
在 Windows 上,将 NULL 矩形指针传递给 ClipCursor 以解除鼠标指针的剪辑。
ClipCursor(NULL);
在 MacOS 上,关联鼠标指针和鼠标移动将禁用原始输入并解锁鼠标指针。
CGAssociateMouseAndMouseCursorPosition(1);
在 Emscripten 上,退出指针锁定将解锁鼠标指针并禁用原始输入。
emscripten_exit_pointerlock();
完整的代码示例
X11
// This can be compiled with
// gcc x11.c -lX11 -lXi
#include <X11/Xlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <X11/extensions/XInput2.h>
int main(void) {
unsigned int window_width = 200;
unsigned int window_height = 200;
Display* display = XOpenDisplay(NULL);
Window window = XCreateSimpleWindow(display, RootWindow(display, DefaultScreen(display)), 400, 400, window_width, window_height, 1, BlackPixel(display, DefaultScreen(display)), WhitePixel(display, DefaultScreen(display)));
XSelectInput(display, window, ExposureMask | KeyPressMask);
XMapWindow(display, window);
XGrabPointer(display, window, True, PointerMotionMask, GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
XWarpPointer(display, None, window, 0, 0, 0, 0, window_width / 2, window_height / 2);
// mask for XI and set mouse for raw mouse input ("RawMotion")
unsigned char mask[XIMaskLen(XI_RawMotion)] = { 0 };
XISetMask(mask, XI_RawMotion);
// set up X1 struct
XIEventMask em;
em.deviceid = XIAllMasterDevices;
em.mask_len = sizeof(mask);
em.mask = mask;
// enable raw input using the structure
XISelectEvents(display, XDefaultRootWindow(display), &em, 1);
Bool rawInput = True;
XPoint point;
XPoint _lastMousePoint;
XEvent event;
for (;;) {
XNextEvent(display, &event);
switch (event.type) {
case MotionNotify:
/* check if mouse hold is enabled */
if (rawInput) {
/* convert E.xmotion to rawinput by substracting the previous point */
point.x = _lastMousePoint.x - event.xmotion.x;
point.y = _lastMousePoint.y - event.xmotion.y;
printf("rawinput %i %i\n", point.x, point.y);
}
break;
case GenericEvent: {
/* MotionNotify is used for mouse events if the mouse isn't held */
if (rawInput == False) {
XFreeEventData(display, &event.xcookie);
break;
}
XGetEventData(display, &event.xcookie);
if (event.xcookie.evtype == XI_RawMotion) {
XIRawEvent *raw = (XIRawEvent *)event.xcookie.data;
if (raw->valuators.mask_len == 0) {
XFreeEventData(display, &event.xcookie);
break;
}
double deltaX = 0.0f;
double deltaY = 0.0f;
/* check if relative motion data exists where we think it does */
if (XIMaskIsSet(raw->valuators.mask, 0) != 0)
deltaX += raw->raw_values[0];
if (XIMaskIsSet(raw->valuators.mask, 1) != 0)
deltaY += raw->raw_values[1];
point = (XPoint){deltaX, deltaY};
XWarpPointer(display, None, window, 0, 0, 0, 0, window_width / 2, window_height / 2);
printf("rawinput %i %i\n", point.x, point.y);
}
XFreeEventData(display, &event.xcookie);
break;
}
case KeyPress:
if (rawInput == False)
break;
unsigned char mask[] = { 0 };
XIEventMask em;
em.deviceid = XIAllMasterDevices;
em.mask_len = sizeof(mask);
em.mask = mask;
XISelectEvents(display, XDefaultRootWindow(display), &em, 1);
XUngrabPointer(display, CurrentTime);
printf("Raw input disabled\n");
break;
default: break;
}
}
XCloseDisplay(display);
}
Winapi
// compile with gcc winapi.c
#include <windows.h>
#include <stdio.h>
#include <stdint.h>
#include <assert.h>
int main() {
WNDCLASS wc = {0};
wc.lpfnWndProc = DefWindowProc; // Default window procedure
wc.hInstance = GetModuleHandle(NULL);
wc.lpszClassName = "SampleWindowClass";
RegisterClass(&wc);
int window_width = 300;
int window_height = 300;
int window_x = 400;
int window_y = 400;
HWND hwnd = CreateWindowA(wc.lpszClassName, "Sample Window", 0,
window_x, window_y, window_width, window_height,
NULL, NULL, wc.hInstance, NULL);
ShowWindow(hwnd, SW_SHOW);
UpdateWindow(hwnd);
// first get the window size (the RGFW_window struct also includes this informaton, but using this ensures it's correct)
RECT clipRect;
GetClientRect(hwnd, &clipRect);
// ClipCursor needs screen coords, not coords relative to the window
ClientToScreen(hwnd, (POINT*) &clipRect.left);
ClientToScreen(hwnd, (POINT*) &clipRect.right);
// now we can lock the cursor
ClipCursor(&clipRect);
SetCursorPos(window_x + (window_width / 2), window_y + (window_height / 2));
const RAWINPUTDEVICE id = { 0x01, 0x02, 0, hwnd };
RegisterRawInputDevices(&id, 1, sizeof(id));
MSG msg;
BOOL holdMouse = TRUE;
BOOL running = TRUE;
POINT point;
while (running) {
if (PeekMessageA(&msg, hwnd, 0u, 0u, PM_REMOVE)) {
switch (msg.message) {
case WM_CLOSE:
case WM_QUIT:
running = FALSE;
break;
case WM_INPUT: {
/* check if the mouse is being held */
if (holdMouse == FALSE)
break;
/* get raw data as an array */
unsigned size = sizeof(RAWINPUT);
static RAWINPUT raw[sizeof(RAWINPUT)];
GetRawInputData((HRAWINPUT)msg.lParam, RID_INPUT, raw, &size, sizeof(RAWINPUTHEADER));
// make sure raw data is valid
if (raw->header.dwType != RIM_TYPEMOUSE || (raw->data.mouse.lLastX == 0 && raw->data.mouse.lLastY == 0) )
break;
point.x = raw->data.mouse.lLastX;
point.y = raw->data.mouse.lLastY;
printf("raw input: %i %i\n", point.x, point.y);
break;
}
case WM_KEYDOWN:
if (holdMouse == FALSE)
break;
const RAWINPUTDEVICE id = { 0x01, 0x02, RIDEV_REMOVE, NULL };
RegisterRawInputDevices(&id, 1, sizeof(id));
ClipCursor(NULL);
printf("rawinput disabled\n");
holdMouse = FALSE;
break;
default: break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
running = IsWindow(hwnd);
}
DestroyWindow(hwnd);
return 0;
}