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

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

starIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

1.00/5 (1投票)

2024年8月16日

CPOL

3分钟阅读

viewsIcon

1381

downloadIcon

32

本教程解释了如何在 X11、WinAPI、Cocoa 和 Emscripten 中锁定鼠标指针并启用原始鼠标输入。

引言

RGFW 是一个轻量级的单头文件窗口库,其源代码可以在 此处 找到。本教程基于其源代码。

当您创建锁定鼠标指针的应用程序时,例如具有第一人称摄像机的游戏,禁用鼠标指针非常重要。这意味着将鼠标指针锁定在屏幕中央并获取原始输入。

这种方法的唯一替代方法是一种“hack”,即在鼠标移动时将其拉回到窗口中央。然而,这是一种 hack,因此可能存在 bug 且在所有操作系统上都无法正常工作。因此,通过使用原始输入来正确锁定鼠标非常重要。

本教程解释了 RGFW 如何处理原始鼠标输入,以便您可以理解如何自己实现它。

概述

所需步骤的快速概述

  1. 锁定鼠标指针
  2. 居中鼠标指针
  3. 启用原始输入
  4. 处理原始输入
  5. 禁用原始输入
  6. 解锁鼠标指针

当用户请求 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;
}
© . All rights reserved.