解释如何锁定光标并为 x11、winapi、cocoa 和 emscripten 启用原始鼠标输入的教程。
介绍
rgfw 是一个轻量级单头窗口库,其源代码可以在这里找到。
本教程基于其源代码。
当您创建锁定光标的应用程序时,例如带有第一人称相机的游戏,能够禁用光标非常重要。
这意味着将光标锁定在屏幕中间并获取原始输入。
此方法的唯一替代方法是在鼠标移动时将鼠标拉回到窗口的中心。然而,这是一个 hack,所以它可能有错误
并且不适用于所有操作系统。因此,使用原始输入正确锁定鼠标非常重要。
本教程解释了 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_window_movemouse 的 rgfw 函数将鼠标移动到窗口中间。
在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;
//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);
}
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((u32)-deltax, (u32)-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;
//the data is flipped
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"));
//the raw input must be flipped for macos as well, and cast for rgfw's event data
win->event.point = rgfw_point((u32) -p.x, (u32) -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
//the raw input must be flipped for emscripten as well
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,您可以使用 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>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <x11>
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 %in", point.x, point.y);
xwarppointer(display, none, window, 0, 0, 0, 0, window_width / 2, window_height / 2);
}
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 %in", 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 disabledn");
break;
default: break;
}
}
xclosedisplay(display);
}
</x11></string.h></stdlib.h></stdio.h></x11>
维纳皮
// 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;
// the data is flipped
point.x = -raw->data.mouse.lLastX;
point.y = -raw->data.mouse.lLastY;
printf("raw input: %i %in", 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 disabledn");
holdMouse = FALSE;
break;
default: break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
running = IsWindow(hwnd);
}
DestroyWindow(hwnd);
return 0;
}
</assert.h></stdint.h></stdio.h></windows.h>