这节讲的内容比较多:
参考视频:https://www.bilibili.com/video/BV1apmpYVEQu/
XInput
是微软提供的一个 API,用于处理 Windows 平台上 Xbox 控制器(包括有线和无线)及其他游戏控制器的输入。它为开发者提供了一组函数,用于查询控制器的状态、接收按钮按下事件以及管理震动反馈。XInput
通过提供一个标准化的接口,简化了游戏控制器的支持,免去了处理低级细节的需要。
XInput 的主要功能:
- 按钮状态:提供查询特定按钮(如 A、B、X、Y、Start、Back 等)是否按下或释放的函数。
- 模拟输入:支持触发器(LT、RT)和摇杆(左摇杆、右摇杆)的模拟输入。
- 震动反馈:可以向控制器发送震动信号(例如,游戏事件发生时,像是受到伤害时的震动)。
- 控制器连接:检测控制器的连接和断开状态。
常用的 XInput 函数:
- XInputGetState:获取控制器的当前状态(按钮按下、摇杆位置、触发器值)。
- XInputSetState:设置连接控制器的震动状态。
- XInputGetCapabilities:返回连接控制器的功能,例如是否支持震动反馈、是否支持耳机等。
这段代码定义了一个 Windows API 函数 XInputGetState
,其用于获取特定玩家的游戏控制器(如 Xbox 手柄)的当前状态。函数原型如下:
DWORD WINAPI XInputGetState
(_In_ DWORD dwUserIndex, // 索引,指定与设备关联的玩家_Out_ XINPUT_STATE* pState // 用于接收设备的当前状态
) WIN_NOEXCEPT;
各个参数解释:
-
dwUserIndex
(In):
该参数指定玩家的索引,用于标识当前要获取状态的设备。通常,dwUserIndex
的值为 0 到 3,代表最多四个玩家的控制器(索引 0 对应第一个玩家,索引 1 对应第二个玩家,依此类推)。 -
pState
(Out):
该参数是一个指向XINPUT_STATE
结构体的指针,函数会将玩家控制器的当前状态写入此结构体。XINPUT_STATE
结构包含了游戏控制器的按钮状态、摇杆位置、触发器状态等信息。
返回值:
- 返回值类型
DWORD
:
该函数返回一个DWORD
类型的值,表示执行的结果。常见的返回值包括:ERROR_SUCCESS (0)
: 表示成功获取控制器的状态。ERROR_DEVICE_NOT_CONNECTED
: 表示指定的设备未连接。
WIN_NOEXCEPT
:
该宏表示这个函数不会抛出异常。
示例使用:
XINPUT_STATE state;
DWORD dwResult = XInputGetState(0, &state); // 获取第一个玩家的控制器状态
if (dwResult == ERROR_SUCCESS) {// 成功获取状态,可以访问 state 结构体中的数据// 比如访问按钮状态: state.Gamepad.wButtons// 或访问摇杆位置: state.Gamepad.sThumbLX, state.Gamepad.sThumbLY
} else {// 处理错误
}
总结:
XInputGetState
函数用于读取指定玩家的游戏控制器状态,并将其存储在 XINPUT_STATE
结构体中。它常用于游戏开发中,尤其是在需要处理多玩家游戏输入的场景。
控制器输入处理 (XInput)
int CALLBACK WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, //PSTR cmdline, int cmdshow) {uint8 BigOldBlockOfMemory[1004 * 1024];WNDCLASS WindowClass = {};// 使用大括号初始化,所有成员都被初始化为零(0)或 nullptrWin32ResizeDIBSection(&GlobalBackbuffer, 1280, 720);// WindowClass.style:表示窗口类的样式。通常设置为一些 Windows// 窗口样式标志(例如 CS_HREDRAW, CS_VREDRAW)。WindowClass.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW;// CS_HREDRAW 当窗口的宽度发生变化时,窗口会被重绘。// CS_VREDRAW 当窗口的高度发生变化时,窗口会被重绘// WindowClass.lpfnWndProc:指向窗口过程函数的指针,窗口过程用于处理与窗口相关的消息。WindowClass.lpfnWndProc = Win32MainWindowCallback;// WindowClass.hInstance:指定当前应用程序的实例句柄,Windows// 应用程序必须有一个实例句柄。WindowClass.hInstance = hInst;// WindowClass.lpszClassName:指定窗口类的名称,通常用于创建窗口时注册该类。WindowClass.lpszClassName = "gameWindowClass"; // 类名if (RegisterClass(&WindowClass)) { // 如果窗口类注册成功HWND Window = CreateWindowEx(0, // 创建窗口,使用扩展窗口风格WindowClass.lpszClassName, // 窗口类的名称,指向已注册的窗口类"game", // 窗口标题(窗口的名称)WS_OVERLAPPEDWINDOW |WS_VISIBLE, // 窗口样式:重叠窗口(带有菜单、边框等)并且可见CW_USEDEFAULT, // 窗口的初始位置:使用默认位置(X坐标)CW_USEDEFAULT, // 窗口的初始位置:使用默认位置(Y坐标)CW_USEDEFAULT, // 窗口的初始宽度:使用默认宽度CW_USEDEFAULT, // 窗口的初始高度:使用默认高度0, // 父窗口句柄(此处无父窗口,传0)0, // 菜单句柄(此处没有菜单,传0)hInst, // 当前应用程序的实例句柄0 // 额外的创建参数(此处没有传递额外参数));// 如果窗口创建成功,Window 将保存窗口的句柄if (Window) { // 检查窗口句柄是否有效,若有效则进入消息循环int xOffset = 0;int yOffset = 0;Running = true;while (Running) { // 启动一个无限循环,等待和处理消息MSG Message; // 声明一个 MSG 结构体,用于接收消息while (PeekMessage(&Message,// 指向一个 `MSG` 结构的指针。`PeekMessage`// 将在 `lpMsg` 中填入符合条件的消息内容。0,// `hWnd` 为`NULL`,则检查当前线程中所有窗口的消息;// 如果设置为特定的窗口句柄,则只检查该窗口的消息。0, //0, // 用于设定消息类型的范围PM_REMOVE // 将消息从消息队列中移除,类似于 `GetMessage` 的行为。)) {if (Message.message == WM_QUIT) {Running = false;}TranslateMessage(&Message); // 翻译消息,如果是键盘消息需要翻译DispatchMessage(&Message); // 分派消息,调用窗口过程处理消息}// TODO: 我们应该频繁的轮询吗for (DWORD ControllerIndex = 0; ControllerIndex < XUSER_INDEX_ANY;ControllerIndex++) {// 定义一个 XINPUT_STATE 结构体,用来存储控制器的状态XINPUT_STATE ControllerState;// 调用 XInputGetState 获取控制器的状态if (XInputGetState(ControllerIndex, &ControllerState) ==ERROR_SUCCESS) {// 如果获取控制器状态成功,提取 Gamepad 的数据// NOTE:// 获取方向键的按键状态XINPUT_GAMEPAD *Pad = &ControllerState.Gamepad;bool Up = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_UP);bool Down = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_DOWN);bool Left = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_LEFT);bool Right = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_RIGHT);// 获取肩部按钮的按键状态bool LeftShoulder = (Pad->wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER);bool RightShoulder =(Pad->wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER);// 获取功能按钮的按键状态bool Start = (Pad->wButtons & XINPUT_GAMEPAD_START);bool Back = (Pad->wButtons & XINPUT_GAMEPAD_BACK);bool A = (Pad->wButtons & XINPUT_GAMEPAD_A);bool B = (Pad->wButtons & XINPUT_GAMEPAD_B);bool X = (Pad->wButtons & XINPUT_GAMEPAD_X);bool Y = (Pad->wButtons & XINPUT_GAMEPAD_Y);// 获取摇杆的 X 和 Y 坐标值(-32768 到 32767)int16 StickX = Pad->sThumbLX;int16 StickY = Pad->sThumbLY;} else {}}RenderWeirdGradient(GlobalBackbuffer, xOffset, yOffset);// 这个地方需要渲染一下不然是黑屏{HDC DeviceContext = GetDC(Window);win32_window_dimension Dimension = Win32GetWindowDimension(Window);RECT WindowRect;GetClientRect(Window, &WindowRect);int WindowWidth = WindowRect.right - WindowRect.left;int WindowHeigh = WindowRect.bottom - WindowRect.top;Win32DisplayBufferInWindow(DeviceContext, Dimension.Width,Dimension.Height, GlobalBackbuffer, 0, 0,WindowWidth, WindowHeigh);ReleaseDC(Window, DeviceContext);}++xOffset;}} else { // 如果窗口创建失败// 这里可以处理窗口创建失败的逻辑// 比如输出错误信息,或退出程序等// TODO:}} else { // 如果窗口类注册失败// 这里可以处理注册失败的逻辑// 比如输出错误信息,或退出程序等// TODO:}return 0;
}
这段代码主要是实现了一个 Windows 程序的窗口管理和控制器输入处理,使用 XInput
库来获取 Xbox 控制器的状态。XInput
是微软提供的一种接口,用于在 Windows 上处理 Xbox 控制器的输入。以下是这段代码的详细解释:
控制器输入处理 (XInput
)
for (DWORD ControllerIndex = 0; ControllerIndex < XUSER_INDEX_ANY; ControllerIndex++) {XINPUT_STATE ControllerState;if (XInputGetState(ControllerIndex, &ControllerState) == ERROR_SUCCESS) {
for (DWORD ControllerIndex = 0; ControllerIndex < XUSER_INDEX_ANY; ControllerIndex++)
:这是一个循环,遍历所有的控制器。XUSER_INDEX_ANY
表示支持多个控制器,但通常情况下它会检查从0
到4
的控制器(最多支持四个)。XInputGetState(ControllerIndex, &ControllerState)
:调用XInputGetState
获取指定控制器的状态,并将其存储在ControllerState
中。如果获取成功,返回ERROR_SUCCESS
。
XINPUT_GAMEPAD *Pad = &ControllerState.Gamepad;
bool Up = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_UP);
bool Down = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_DOWN);
bool Left = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_LEFT);
bool Right = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_RIGHT);
XINPUT_GAMEPAD *Pad = &ControllerState.Gamepad;
:获取XINPUT_STATE
结构体中的Gamepad
部分,存储所有控制器按钮的状态。- 按键状态:通过按位与操作(
&
)检查每个按钮的状态。例如,XINPUT_GAMEPAD_DPAD_UP
判断方向键上的“上”按钮是否被按下。
bool LeftShoulder = (Pad->wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER);
bool RightShoulder = (Pad->wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER);
bool Start = (Pad->wButtons & XINPUT_GAMEPAD_START);
bool Back = (Pad->wButtons & XINPUT_GAMEPAD_BACK);
bool A = (Pad->wButtons & XINPUT_GAMEPAD_A);
bool B = (Pad->wButtons & XINPUT_GAMEPAD_B);
bool X = (Pad->wButtons & XINPUT_GAMEPAD_X);
bool Y = (Pad->wButtons & XINPUT_GAMEPAD_Y);
- 获取其他常见按钮的状态,如左肩按钮、右肩按钮、A/B/X/Y 按钮等。
int16 StickX = Pad->sThumbLX;
int16 StickY = Pad->sThumbLY;
- 摇杆坐标:
sThumbLX
和sThumbLY
分别表示左摇杆的 X 和 Y 坐标,范围是 -32768 到 32767。
XInput
主要用于读取 Xbox 控制器的输入状态,获取按钮按下的状态以及摇杆的位置信息。在每一帧循环中,代码都检查并处理控制器的状态,这在游戏开发中非常常见,用于实现用户输入响应。
编译程序会出现未定义的情况,但是XInput 对于游戏来说并不是必须得,程序并不是非要有XInput才能运行。
在你提供的代码中,定义了以下内容:
typedef DWORD WINAPI x_input_get_state(_In_ DWORD dwUserIndex,_Out_ XINPUT_STATE *pState);typedef DWORD WINAPI x_input_set_state(_In_ DWORD dwUserIndex,_In_ XINPUT_VIBRATION *pVibration);global_variable x_input_get_state *XInputGetState_;
global_variable x_input_set_state *XInputSetState_;#define XInputGetState XInputGetState_
#define XInputSetState XInputSetState_
这些代码的目的是为了通过 函数指针 动态加载 XInput
库中的 XInputGetState
和 XInputSetState
函数,而不是直接链接这些函数。这种做法通常用于动态加载共享库或 DLL 时,能够在运行时加载和调用这些函数。这样做有几个目的:
1. 避免直接链接
这种方式使用了函数指针和 #define
宏,代替了直接链接静态或动态库。通常,这种做法有以下好处:
- 动态链接:程序不直接链接到
XInput
库,而是在运行时加载它。这样,你可以通过修改程序的dll
路径来改变使用的XInput
版本,而无需重新编译整个程序。 - 延迟加载:只有在需要的时候才会加载
XInput
函数,这对于某些不一定会使用XInput
功能的程序来说,能减少启动时的资源消耗。
2. 加载 DLL 并获取函数地址
通过这样的声明,程序可以在运行时加载动态链接库(DLL)并通过 GetProcAddress
或类似的机制来获取 XInputGetState
和 XInputSetState
的函数地址。函数指针 XInputGetState_
和 XInputSetState_
允许你在运行时调用这些函数,而不需要在编译时链接库。
示例代码可能类似于:
HMODULE xinputDLL = LoadLibrary("xinput1_4.dll");
if (xinputDLL != NULL) {XInputGetState_ = (x_input_get_state *)GetProcAddress(xinputDLL, "XInputGetState");XInputSetState_ = (x_input_set_state *)GetProcAddress(xinputDLL, "XInputSetState");
}
这样,程序会动态加载 xinput1_4.dll
并获取 XInputGetState
和 XInputSetState
函数的地址。然后,通过 XInputGetState_
和 XInputSetState_
指针来调用这两个函数。
3. 动态加载 XInput 库
如果你不想静态链接 XInput
库,而是想使用动态加载的方式,你需要确保:
- 使用
LoadLibrary
和GetProcAddress
动态加载XInput
DLL。 - 将上述定义的函数指针与
GetProcAddress
配对,确保在运行时能够正确解析XInputGetState
和XInputSetState
。
例如:
HMODULE xinputDLL = LoadLibrary("xinput1_4.dll");
if (xinputDLL != NULL) {XInputGetState_ = (x_input_get_state *)GetProcAddress(xinputDLL, "XInputGetState");XInputSetState_ = (x_input_set_state *)GetProcAddress(xinputDLL, "XInputSetState");
}
然后,你可以像调用普通函数一样使用 XInputGetState_
和 XInputSetState_
。
总结
- 你定义这些函数指针是为了实现动态加载
XInput
库,而不是在编译时静态链接该库。这种做法可以延迟加载XInput
,避免直接链接库,常用于 DLL 或共享库的动态加载。 - 如果你希望解决
LNK2019
错误,可以考虑确保正确链接XInput
库,或者确保使用动态加载库的方式正确调用XInput
函数。
/*** @param dwUserIndex // 与设备关联的玩家索引* @param pState // 接收当前状态的结构体*/
typedef DWORD WINAPI x_input_get_state(_In_ DWORD dwUserIndex,_Out_ XINPUT_STATE *pState);/*** @param dwUserIndex // 与设备关联的玩家索引* @param pVibration // 要发送到控制器的震动信息*/
typedef DWORD WINAPI x_input_set_state(_In_ DWORD dwUserIndex,_In_ XINPUT_VIBRATION *pVibration);global_variable x_input_get_state *XInputGetState_;
global_variable x_input_set_state *XInputSetState_;#define XInputGetState XInputGetState_
#define XInputSetState XInputsetState_
现在编译可以编译通过
运行是由问题的
上面代码进一步修改
/*** @param dwUserIndex // 与设备关联的玩家索引* @param pState // 接收当前状态的结构体*/
#define X_INPUT_GET_STATE(name) \DWORD WINAPI name(DWORD dwUserIndex, XINPUT_STATE *pState)
/*** @param dwUserIndex // 与设备关联的玩家索引* @param pVibration // 要发送到控制器的震动信息*/
#define X_INPUT_SET_STATE(name) \DWORD WINAPI name(DWORD dwUserIndex, XINPUT_VIBRATION *pVibration)
typedef X_INPUT_GET_STATE(x_input_get_state);
typedef X_INPUT_SET_STATE(x_input_set_state);X_INPUT_GET_STATE(XInputGetStateStub) { return (0); }
X_INPUT_SET_STATE(XInputSetStateStub) { return (0); }global_variable x_input_get_state *XInputGetState_ = XInputGetStateStub;
global_variable x_input_set_state *XInputSetState_ = XInputSetStateStub;#define XInputGetState XInputGetState_
#define XInputSetState XInputsetState_
以下是对你代码中每一部分的详细注释:
/*** @param dwUserIndex // 与设备关联的玩家索引* @param pState // 接收当前状态的结构体*/
#define X_INPUT_GET_STATE(name) \DWORD WINAPI name(DWORD dwUserIndex, XINPUT_STATE *pState)
X_INPUT_GET_STATE
宏定义了一个标准的函数签名,用于声明XInputGetState
函数。宏通过传递的name
参数生成一个具有该名称的函数,该函数接受一个玩家索引(dwUserIndex
)和一个XINPUT_STATE
结构体指针(pState
),并返回一个DWORD
类型的值(通常是ERROR_SUCCESS
或错误代码)。name
是宏的参数,它允许你为每个具体的函数定义提供自定义名称。
/*** @param dwUserIndex // 与设备关联的玩家索引* @param pVibration // 要发送到控制器的震动信息*/
#define X_INPUT_SET_STATE(name) \DWORD WINAPI name(DWORD dwUserIndex, XINPUT_VIBRATION *pVibration)
X_INPUT_SET_STATE
宏定义了一个标准的函数签名,用于声明XInputSetState
函数。宏通过传递的name
参数生成一个具有该名称的函数,该函数接受一个玩家索引(dwUserIndex
)和一个XINPUT_VIBRATION
结构体指针(pVibration
),并返回一个DWORD
类型的值(通常是ERROR_SUCCESS
或错误代码)。name
参数允许我们为每个具体的函数定义提供一个自定义名称。
typedef X_INPUT_GET_STATE(x_input_get_state);
typedef X_INPUT_SET_STATE(x_input_set_state);
typedef
将宏X_INPUT_GET_STATE
和X_INPUT_SET_STATE
创建的函数签名分别定义为x_input_get_state
和x_input_set_state
类型别名。这样,x_input_get_state
和x_input_set_state
就变成了类型,表示那些符合这两个宏定义的函数。
X_INPUT_GET_STATE(XInputGetStateStub) { return (0); }
X_INPUT_SET_STATE(XInputSetStateStub) { return (0); }
XInputGetStateStub
和XInputSetStateStub
这两个函数是 函数占位符。它们使用了由宏定义的签名,并简单地返回0
,表示一个空的实现。- 这些占位符函数提供了一个默认的行为,以确保在没有实际实现的情况下,程序仍能编译和运行,避免因为找不到具体实现而导致的错误。
global_variable x_input_get_state *XInputGetState_ = XInputGetStateStub;
global_variable x_input_set_state *XInputSetState_ = XInputSetStateStub;
- 这两行代码定义了两个全局变量
XInputGetState_
和XInputSetState_
,分别是指向x_input_get_state
和x_input_set_state
类型的函数指针,初始值分别为XInputGetStateStub
和XInputSetStateStub
。 - 这意味着在程序的其他地方,调用
XInputGetState_
和XInputSetState_
实际上是在调用XInputGetStateStub
和XInputSetStateStub
函数,直到在运行时动态加载并替换这些函数指针为实际的XInputGetState
和XInputSetState
实现。
#define XInputGetState XInputGetState_
#define XInputSetState XInputSetState_
- 通过这两行宏定义,
XInputGetState
和XInputSetState
被重新定义为XInputGetState_
和XInputSetState_
。这样,程序中的所有XInputGetState
和XInputSetState
调用将会转向实际的全局函数指针XInputGetState_
和XInputSetState_
。- 这实际上是为
XInputGetState
和XInputSetState
提供了动态加载的能力。
- 这实际上是为
代码目的总结:
- 宏定义:
X_INPUT_GET_STATE
和X_INPUT_SET_STATE
使得我们能够定义带有相同函数签名的多个函数。宏接受一个name
参数,用于创建不同的函数。 - 函数占位符:
XInputGetStateStub
和XInputSetStateStub
作为占位符函数,提供了默认的返回值,以便在没有实际函数实现时,程序仍能编译和运行。 - 函数指针:
XInputGetState_
和XInputSetState_
是全局的函数指针,最初指向占位符函数XInputGetStateStub
和XInputSetStateStub
,允许在运行时动态替换为实际的实现。 - 动态替换:
#define
宏使得程序中对XInputGetState
和XInputSetState
的调用实际指向全局函数指针XInputGetState_
和XInputSetState_
,从而支持在运行时加载和使用实际的函数。
这样,代码就能在运行时动态加载 XInput
函数,而不需要在编译时直接链接。这种技术可以用来实现延迟加载 DLL 函数,或者在多个不同版本的库之间切换。
加载库
LoadLibrary
是 Windows API 用于动态加载 DLL(动态链接库)的函数。它加载指定路径的 DLL 文件到当前进程的地址空间,使得你可以调用 DLL 中的函数。
用法
HMODULE LoadLibrary(LPCSTR lpFileName // DLL 文件的路径或名称
);
lpFileName
: DLL 文件的路径,可以是绝对路径或相对路径。如果 DLL 文件位于系统目录或环境变量路径中,可以只指定文件名。
返回值
- 成功时,返回加载的 DLL 模块的句柄(
HMODULE
)。可以使用该句柄调用GetProcAddress
函数来获取 DLL 中的函数地址。 - 失败时,返回
NULL
,并且可以通过GetLastError
获取错误代码。
示例
以下是使用 LoadLibrary
动态加载 DLL 并调用其中一个函数的示例:
#include <windows.h>
#include <iostream>typedef int (CALLBACK* AddFunc)(int, int); // 定义一个函数指针类型int main() {// 加载 DLLHMODULE hModule = LoadLibrary("example.dll");if (hModule != NULL) {// 获取函数地址AddFunc Add = (AddFunc)GetProcAddress(hModule, "Add");if (Add != NULL) {// 调用 DLL 中的函数int result = Add(5, 3);std::cout << "Result of Add: " << result << std::endl;} else {std::cerr << "Error getting function address: " << GetLastError() << std::endl;}// 卸载 DLLFreeLibrary(hModule);} else {std::cerr << "Error loading DLL: " << GetLastError() << std::endl;}return 0;
}
关键点解释
- 加载 DLL: 使用
LoadLibrary
加载 DLL,返回一个句柄 (HMODULE
),这是 DLL 的标识符。 - 获取函数地址: 使用
GetProcAddress
获取 DLL 中某个函数的地址。你需要提供该函数的名称(字符串形式)或者函数的符号。 - 调用函数: 通过函数指针调用 DLL 中的函数。
- 卸载 DLL: 使用
FreeLibrary
卸载已经加载的 DLL。
注意事项
- 路径: 如果没有提供完整路径,
LoadLibrary
会在当前工作目录、系统目录、Windows 目录等标准路径中搜索 DLL。 - 错误处理: 如果
LoadLibrary
或GetProcAddress
失败,可以通过GetLastError
获取更多的错误信息。 - 内存管理: 加载的 DLL 应在不需要时调用
FreeLibrary
卸载,以释放内存和资源。
常见错误
ERROR_MOD_NOT_FOUND
: 指定的 DLL 文件未找到。ERROR_PROC_NOT_FOUND
:GetProcAddress
查找不到指定的函数。
通过 LoadLibrary
和 GetProcAddress
,你可以在运行时动态地加载和调用 DLL 中的函数,而不需要在编译时链接它们。
找一下XInput的库
load 动态库
调试一下
模拟手柄软件
因为我没有手柄只能用模拟器进行调试
游戏手柄模拟器Gaming Keyboard Splitter
可以到这个完整下载对应的软件Gaming Keyboard Splitter
https://softlookup.com/download.asp?id=280311
我已经传到CSDN
https://download.csdn.net/download/TM1695648164/89982708
软件第一次运行会安装驱动会重启电脑
测试下载的软件
测试程序
XInputSetState
是一个 XInput
API 函数,用于控制 Xbox 控制器的振动功能。它通过将特定的振动模式信息发送到控制器,来激活相应的振动马达。它的函数原型如下:
DWORD WINAPI XInputSetState(_In_ DWORD dwUserIndex, // 与设备关联的玩家索引_In_ XINPUT_VIBRATION *pVibration // 要发送到控制器的振动信息
) WIN_NOEXCEPT;
参数解释
-
dwUserIndex (
DWORD
类型):- 表示与设备关联的玩家索引,即控制器编号。
- 常用值范围是
0
到3
,分别表示最多支持的四个控制器。 - 例如,当值为
0
时表示玩家 1 的控制器;当值为1
时,表示玩家 2 的控制器。
-
pVibration (
XINPUT_VIBRATION*
类型):-
指向
XINPUT_VIBRATION
结构的指针,该结构包含控制器的振动强度信息。 -
XINPUT_VIBRATION
结构的定义如下:typedef struct _XINPUT_VIBRATION {WORD wLeftMotorSpeed; // 左振动马达的速度,取值范围 0 到 65535WORD wRightMotorSpeed; // 右振动马达的速度,取值范围 0 到 65535 } XINPUT_VIBRATION;
-
wLeftMotorSpeed
和wRightMotorSpeed
分别表示左、右振动马达的振动强度。 -
数值范围从
0
(关闭)到65535
(最大振动强度)。
-
返回值
- 返回一个
DWORD
值,表示执行的结果。 - 常见返回值包括:
ERROR_SUCCESS
:函数成功执行。ERROR_DEVICE_NOT_CONNECTED
:控制器未连接或无法识别。
使用示例
下面是一个简单的使用 XInputSetState
激活控制器振动的示例,将左马达设置为最大振动,右马达设置为中等振动:
#include <XInput.h>void VibrateController(DWORD dwUserIndex) {XINPUT_VIBRATION vibration = {};vibration.wLeftMotorSpeed = 65535; // 设置左马达为最大振动vibration.wRightMotorSpeed = 32768; // 设置右马达为中等振动DWORD result = XInputSetState(dwUserIndex, &vibration);if (result == ERROR_SUCCESS) {// 振动已成功启动} else if (result == ERROR_DEVICE_NOT_CONNECTED) {// 控制器未连接}
}
在此示例中,VibrateController
函数会尝试让控制器振动。如果 XInputSetState
返回 ERROR_SUCCESS
,振动成功;如果返回 ERROR_DEVICE_NOT_CONNECTED
,则控制器未连接。
增加马达震动貌似没法测
在 Windows 程序中,WM_KEYDOWN
、WM_KEYUP
、WM_SYSKEYDOWN
和 WM_SYSKEYUP
是处理键盘事件的消息类型,它们帮助捕获按键的按下和释放。代码示例展示了如何通过这些消息检测按键事件,并在按下特定键(例如 'W'
)时执行相应操作。
以下是代码的逐步解析:
case WM_SYSKEYDOWN: // 系统按键按下消息,例如 Alt 键组合。
case WM_SYSKEYUP: // 系统按键释放消息。
case WM_KEYDOWN: // 普通按键按下消息。
case WM_KEYUP: { // 普通按键释放消息。uint32 VkCode = wParam; // `wParam` 包含按键的虚拟键码(Virtual-Key Code)if (VkCode == 'W') { // 检查是否按下了 'W' 键OutputDebugStringA("W\n"); // 调试输出字符 "W" 和换行符}// 检查 lParam 位 30 的状态,用于获取按键的重复标志// LParam & (1 << 30);
} break;
主要部分详解
-
wParam
的用途:wParam
代表按键的虚拟键码 (VkCode
),用于标识哪个键被按下或释放。例如,'W'
的虚拟键码是0x57
。- 在代码中,通过
if (VkCode == 'W')
判断是否按下了'W'
键。
-
OutputDebugStringA("W\n")
:- 这是一个用于调试的函数,会在输出窗口中显示
W
和换行符\n
。 - 每当按下
'W'
键时,程序会将"W\n"
输出到调试控制台。
- 这是一个用于调试的函数,会在输出窗口中显示
-
使用
lParam
获取按键状态:-
lParam
包含了更多关于按键的状态信息。例如,代码注释中的LParam & (1 << 30)
试图查看按键的重复标志。 -
lParam
的第 30 位表示按键是否已经按下并保持按住。按键被连续按住时,系统会将该位设置为1
。 -
可以使用如下代码检查这一位状态:
bool isHeld = (lParam & (1 << 30)) != 0;
-
当
isHeld
为true
时,表示该按键正在被按住,否则表示这是按键首次被按下。
-
示例说明
结合上述内容,完整代码块用于检查键盘事件。如果 'W'
键被按下,它会将消息输出到调试窗口,同时可以利用 lParam
进一步判断按键是初次按下还是被持续按住。
// game.cpp : Defines the entry point for the application.
//#include <cstdint>
#include <stdint.h>
#include <windows.h>
#include <winuser.h>
#include <xinput.h>#define internal static // 用于定义内翻译单元内部函数
#define local_persist static // 局部静态变量
#define global_variable static // 全局变量typedef uint8_t uint8;
typedef uint16_t uint16;
typedef uint32_t uint32;
typedef uint64_t uint64;typedef int8_t int8;
typedef int16_t int16;
typedef int32_t int32;
typedef int64_t int64;struct win32_offscreen_buffer {BITMAPINFO Info;void *Memory;// 后备缓冲区的宽度和高度int Width;int Height;int Pitch;int BytesPerPixel;
};
// 添加这个去掉重复的冗余代码
struct win32_window_dimension {int Width;int Height;
};
// TODO: 全局变量
global_variable boolRunning; // 用于控制程序运行的全局布尔变量,通常用于循环条件
global_variable win32_offscreen_bufferGlobalBackbuffer; // 用于存储屏幕缓冲区的全局变量/*** @param dwUserIndex // 与设备关联的玩家索引* @param pState // 接收当前状态的结构体*/
#define X_INPUT_GET_STATE(name) \DWORD WINAPI name(DWORD dwUserIndex, \XINPUT_STATE *pState) // 定义一个宏,将指定名称设置为// XInputGetState 函数的类型定义/*** @param dwUserIndex // 与设备关联的玩家索引* @param pVibration // 要发送到控制器的震动信息*/
#define X_INPUT_SET_STATE(name) \DWORD WINAPI name( \DWORD dwUserIndex, \XINPUT_VIBRATION *pVibration) // 定义一个宏,将指定名称设置为// XInputSetState 函数的类型定义typedef X_INPUT_GET_STATE(x_input_get_state); // 定义了 x_input_get_state 类型,为 `XInputGetState`// 函数的类型
typedef X_INPUT_SET_STATE(x_input_set_state); // 定义了 x_input_set_state 类型,为 `XInputSetState`// 函数的类型// 定义一个 XInputGetState 的打桩函数,返回值为 0,表示未执行操作
X_INPUT_GET_STATE(XInputGetStateStub) { return (0); }// 定义一个 XInputSetState 的打桩函数,返回值为 0,表示未执行操作
X_INPUT_SET_STATE(XInputSetStateStub) { return (0); }// 设置全局变量 XInputGetState_ 和 XInputSetState_ 的初始值为打桩函数
global_variable x_input_get_state *XInputGetState_ = XInputGetStateStub;
global_variable x_input_set_state *XInputSetState_ = XInputSetStateStub;// 定义宏将 XInputGetState 和 XInputSetState 重新指向 XInputGetState_ 和
// XInputSetState_
#define XInputGetState XInputGetState_
#define XInputSetState XInputSetState_// 加载 XInput DLL 并获取函数地址
internal void Win32LoadXInput(void) { //HMODULE XInputLibrary = LoadLibrary("xinput1_4.dll"); // 加载 XInput 库(DLL)if (XInputLibrary) { // 检查库是否加载成功XInputGetState = (x_input_get_state *)GetProcAddress(XInputLibrary, "XInputGetState"); // 获取 XInputGetState 函数地址if (!XInputGetState) { // 如果获取失败,使用打桩函数XInputGetState = XInputGetStateStub;}XInputSetState = (x_input_set_state *)GetProcAddress(XInputLibrary, "XInputSetState"); // 获取 XInputSetState 函数地址if (!XInputSetState) { // 如果获取失败,使用打桩函数XInputSetState = XInputSetStateStub;}}
}internal win32_window_dimension Win32GetWindowDimension(HWND Window) {win32_window_dimension Result;RECT ClientRect;GetClientRect(Window, &ClientRect);// 计算绘制区域的宽度和高度Result.Height = ClientRect.bottom - ClientRect.top;Result.Width = ClientRect.right - ClientRect.left;return Result;
}// 渲染一个奇异的渐变图案
internal void RenderWeirdGradient(win32_offscreen_buffer Buffer, int BlueOffset,int GreenOffset) {// TODO:让我们看看优化器是怎么做的uint8 *Row = (uint8 *)Buffer.Memory; // 指向位图数据的起始位置for (int Y = 0; Y < Buffer.Height; ++Y) { // 遍历每一行uint32 *Pixel = (uint32 *)Row; // 指向每一行的起始像素for (int X = 0; X < Buffer.Width; ++X) { // 遍历每一列uint8 Blue = (X + BlueOffset); // 计算蓝色分量uint8 Green = (Y + GreenOffset); // 计算绿色分量*Pixel++ = ((Green << 8) | Blue); // 设置当前像素的颜色}Row += Buffer.Pitch; // 移动到下一行}
}// 这个函数用于重新调整 DIB(设备独立位图)大小
internal void Win32ResizeDIBSection(win32_offscreen_buffer *Buffer, int width,int height) {// device independent bitmap(设备独立位图)// TODO: 进一步优化代码的健壮性// 可能的改进:先不释放,先尝试其他方法,再如果失败再释放。if (Buffer->Memory) {VirtualFree(Buffer->Memory, // 指定要释放的内存块起始地址0, // 要释放的大小(字节),对部分释放有效,整体释放则设为 0MEM_RELEASE); // MEM_RELEASE:释放整个内存块,将内存和地址空间都归还给操作系统}// 赋值后备缓冲的宽度和高度Buffer->Width = width;Buffer->Height = height;Buffer->BytesPerPixel = 4;// 设置位图信息头(BITMAPINFOHEADER)Buffer->Info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); // 位图头大小Buffer->Info.bmiHeader.biWidth = Buffer->Width; // 设置位图的宽度Buffer->Info.bmiHeader.biHeight =-Buffer->Height; // 设置位图的高度(负号表示自上而下的方向)Buffer->Info.bmiHeader.biPlanes = 1; // 设置颜色平面数,通常为 1Buffer->Info.bmiHeader.biBitCount =32; // 每像素的位数,这里为 32 位(即 RGBA)Buffer->Info.bmiHeader.biCompression =BI_RGB; // 无压缩,直接使用 RGB 颜色模式// 创建 DIBSection(设备独立位图)并返回句柄// TODO:我们可以自己分配?int BitmapMemorySize =(Buffer->Width * Buffer->Height) * Buffer->BytesPerPixel;Buffer->Memory = VirtualAlloc(0, // lpAddress:指定内存块的起始地址。// 通常设为 NULL,由系统自动选择一个合适的地址。BitmapMemorySize, // 要分配的内存大小,单位是字节。MEM_COMMIT, // 分配物理内存并映射到虚拟地址。已提交的内存可以被进程实际访问和操作。PAGE_READWRITE // 内存可读写);Buffer->Pitch = width * Buffer->BytesPerPixel; // 每一行的字节数// TODO:可能会把它清除成黑色
}// 这个函数用于将 DIBSection 绘制到窗口设备上下文
internal void Win32DisplayBufferInWindow(HDC DeviceContext, int WindowWidth,int WindowHeight,win32_offscreen_buffer Buffer, int X,int Y, int Width, int Height) {// 使用 StretchDIBits 将 DIBSection 绘制到设备上下文中StretchDIBits(DeviceContext, // 目标设备上下文(窗口或屏幕的设备上下文)/*X, Y, Width, Height, // 目标区域的 x, y 坐标及宽高X, Y, Width, Height,*/0, 0, WindowWidth, WindowHeight, //0, 0, Buffer.Width, Buffer.Height, //// 源区域的 x, y 坐标及宽高(此处源区域与目标区域相同)Buffer.Memory, // 位图内存指针,指向 DIBSection 数据&Buffer.Info, // 位图信息,包含位图的大小、颜色等信息DIB_RGB_COLORS, // 颜色类型,使用 RGB 颜色SRCCOPY); // 使用 SRCCOPY 操作符进行拷贝(即源图像直接拷贝到目标区域)
}LRESULT CALLBACK
Win32MainWindowCallback(HWND hwnd, // 窗口句柄,表示消息来源的窗口UINT Message, // 消息标识符,表示当前接收到的消息类型WPARAM wParam, // 与消息相关的附加信息,取决于消息类型LPARAM LParam) { // 与消息相关的附加信息,取决于消息类型LRESULT Result = 0; // 定义一个变量来存储消息处理的结果switch (Message) { // 根据消息类型进行不同的处理case WM_CREATE: {OutputDebugStringA("WM_CREATE\n");};case WM_SIZE: { // 窗口大小发生变化时的消息} break;case WM_DESTROY: { // 窗口销毁时的消息// TODO: 处理错误,用重建窗口Running = false;} break;case WM_SYSKEYDOWN: // 系统按键按下消息,例如 Alt 键组合。case WM_SYSKEYUP: // 系统按键释放消息。case WM_KEYDOWN: // 普通按键按下消息。case WM_KEYUP: { // 普通按键释放消息。uint64 VKCode = wParam; // `wParam` 包含按键的虚拟键码(Virtual-Key Code)bool WasDown = ((LParam & (1 << 30)) != 0);bool IsDown = ((LParam & (1 << 30)) == 0);if (IsDown != WasDown) {if (VKCode == 'W') { // 检查是否按下了 'W' 键} else if (VKCode == 'A') {} else if (VKCode == 'S') {} else if (VKCode == 'D') {} else if (VKCode == 'Q') {} else if (VKCode == 'E') {} else if (VKCode == VK_UP) {} else if (VKCode == VK_DOWN) {} else if (VKCode == VK_LEFT) {} else if (VKCode == VK_RIGHT) {} else if (VKCode == VK_ESCAPE) {OutputDebugStringA("ESCAPE: ");if (IsDown) {OutputDebugString(" IsDown ");}if (WasDown) {OutputDebugString(" WasDown ");}} else if (VKCode == VK_SPACE) {}}// 检查 lParam 位 30 的状态,用于获取按键的重复标志// LParam & (1 << 30);} break;case WM_CLOSE: { // 窗口关闭时的消息// TODO: 像用户发送消息进行处理Running = false;} break;case WM_ACTIVATEAPP: { // 应用程序激活或失去焦点时的消息OutputDebugStringA("WM_ACTIVATEAPP\n"); // 输出调试信息,表示应用程序激活或失去焦点} break;case WM_PAINT: { // 处理 WM_PAINT 消息,通常在窗口需要重新绘制时触发PAINTSTRUCT Paint; // 定义一个 PAINTSTRUCT 结构体,保存绘制的信息// 调用 BeginPaint 开始绘制,并获取设备上下文 (HDC),同时填充 Paint 结构体HDC DeviceContext = BeginPaint(hwnd, &Paint);// 获取当前绘制区域的左上角坐标int X = Paint.rcPaint.left;int Y = Paint.rcPaint.top;// 计算绘制区域的宽度和高度int Height = Paint.rcPaint.bottom - Paint.rcPaint.top;int Width = Paint.rcPaint.right - Paint.rcPaint.left;win32_window_dimension Dimension = Win32GetWindowDimension(hwnd);Win32DisplayBufferInWindow(DeviceContext, Dimension.Width, Dimension.Height,GlobalBackbuffer, X, Y, Width, Height);// 调用 EndPaint 结束绘制,并释放设备上下文EndPaint(hwnd, &Paint);} break;default: { // 对于不处理的消息,调用默认的窗口过程Result = DefWindowProc(hwnd, Message, wParam,LParam); // 调用默认窗口过程处理消息} break;}return Result; // 返回处理结果
}int CALLBACK WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, //PSTR cmdline, int cmdshow) {Win32LoadXInput();WNDCLASS WindowClass = {};// 使用大括号初始化,所有成员都被初始化为零(0)或 nullptrWin32ResizeDIBSection(&GlobalBackbuffer, 1280, 720);// WindowClass.style:表示窗口类的样式。通常设置为一些 Windows// 窗口样式标志(例如 CS_HREDRAW, CS_VREDRAW)。WindowClass.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW;// CS_HREDRAW 当窗口的宽度发生变化时,窗口会被重绘。// CS_VREDRAW 当窗口的高度发生变化时,窗口会被重绘// WindowClass.lpfnWndProc:指向窗口过程函数的指针,窗口过程用于处理与窗口相关的消息。WindowClass.lpfnWndProc = Win32MainWindowCallback;// WindowClass.hInstance:指定当前应用程序的实例句柄,Windows// 应用程序必须有一个实例句柄。WindowClass.hInstance = hInst;// WindowClass.lpszClassName:指定窗口类的名称,通常用于创建窗口时注册该类。WindowClass.lpszClassName = "gameWindowClass"; // 类名if (RegisterClass(&WindowClass)) { // 如果窗口类注册成功HWND Window = CreateWindowEx(0, // 创建窗口,使用扩展窗口风格WindowClass.lpszClassName, // 窗口类的名称,指向已注册的窗口类"game", // 窗口标题(窗口的名称)WS_OVERLAPPEDWINDOW |WS_VISIBLE, // 窗口样式:重叠窗口(带有菜单、边框等)并且可见CW_USEDEFAULT, // 窗口的初始位置:使用默认位置(X坐标)CW_USEDEFAULT, // 窗口的初始位置:使用默认位置(Y坐标)CW_USEDEFAULT, // 窗口的初始宽度:使用默认宽度CW_USEDEFAULT, // 窗口的初始高度:使用默认高度0, // 父窗口句柄(此处无父窗口,传0)0, // 菜单句柄(此处没有菜单,传0)hInst, // 当前应用程序的实例句柄0 // 额外的创建参数(此处没有传递额外参数));// 如果窗口创建成功,Window 将保存窗口的句柄if (Window) { // 检查窗口句柄是否有效,若有效则进入消息循环int xOffset = 0;int yOffset = 0;Running = true;while (Running) { // 启动一个无限循环,等待和处理消息MSG Message; // 声明一个 MSG 结构体,用于接收消息while (PeekMessage(&Message,// 指向一个 `MSG` 结构的指针。`PeekMessage`// 将在 `lpMsg` 中填入符合条件的消息内容。0,// `hWnd` 为`NULL`,则检查当前线程中所有窗口的消息;// 如果设置为特定的窗口句柄,则只检查该窗口的消息。0, //0, // 用于设定消息类型的范围PM_REMOVE // 将消息从消息队列中移除,类似于 `GetMessage` 的行为。)) {if (Message.message == WM_QUIT) {Running = false;}TranslateMessage(&Message); // 翻译消息,如果是键盘消息需要翻译DispatchMessage(&Message); // 分派消息,调用窗口过程处理消息}// TODO: 我们应该频繁的轮询吗for (DWORD ControllerIndex = 0; ControllerIndex < XUSER_INDEX_ANY;ControllerIndex++) {// 定义一个 XINPUT_STATE 结构体,用来存储控制器的状态XINPUT_STATE ControllerState;// 调用 XInputGetState 获取控制器的状态if (XInputGetState(ControllerIndex, &ControllerState) ==ERROR_SUCCESS) {// 如果获取控制器状态成功,提取 Gamepad 的数据// NOTE:// 获取方向键的按键状态XINPUT_GAMEPAD *Pad = &ControllerState.Gamepad;bool Up = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_UP);bool Down = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_DOWN);bool Left = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_LEFT);bool Right = (Pad->wButtons & XINPUT_GAMEPAD_DPAD_RIGHT);// 获取肩部按钮的按键状态bool LeftShoulder = (Pad->wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER);bool RightShoulder =(Pad->wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER);// 获取功能按钮的按键状态bool Start = (Pad->wButtons & XINPUT_GAMEPAD_START);bool Back = (Pad->wButtons & XINPUT_GAMEPAD_BACK);bool AButton = (Pad->wButtons & XINPUT_GAMEPAD_A);bool BButton = (Pad->wButtons & XINPUT_GAMEPAD_B);bool XButton = (Pad->wButtons & XINPUT_GAMEPAD_X);bool YButton = (Pad->wButtons & XINPUT_GAMEPAD_Y);// std::cout << "AButton " << AButton << " BButton " << BButton// << " XButton " << XButton << " YButton " << YButton// << std::endl;// 获取摇杆的 X 和 Y 坐标值(-32768 到 32767)int16 StickX = Pad->sThumbLX;int16 StickY = Pad->sThumbLY;if (AButton) {yOffset += 2;}} else {}}XINPUT_VIBRATION Vibration; // 要发送到控制器的振动信息Vibration.wLeftMotorSpeed = 65535; // 设置左马达为最大振动Vibration.wRightMotorSpeed = 32768; // 设置右马达为中等振动XInputSetState(0, &Vibration);RenderWeirdGradient(GlobalBackbuffer, xOffset, yOffset);// 这个地方需要渲染一下不然是黑屏{HDC DeviceContext = GetDC(Window);win32_window_dimension Dimension = Win32GetWindowDimension(Window);RECT WindowRect;GetClientRect(Window, &WindowRect);int WindowWidth = WindowRect.right - WindowRect.left;int WindowHeigh = WindowRect.bottom - WindowRect.top;Win32DisplayBufferInWindow(DeviceContext, Dimension.Width,Dimension.Height, GlobalBackbuffer, 0, 0,WindowWidth, WindowHeigh);ReleaseDC(Window, DeviceContext);}++xOffset;}} else { // 如果窗口创建失败// 这里可以处理窗口创建失败的逻辑// 比如输出错误信息,或退出程序等// TODO:}} else { // 如果窗口类注册失败// 这里可以处理注册失败的逻辑// 比如输出错误信息,或退出程序等// TODO:}return 0;
}