这节讲的内容比较多:
参考视频: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 手柄)的当前状态。函数原型如下:
cpp
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
:
该宏表示这个函数不会抛出异常。
示例使用:
cpp
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)
cpp
int CALLBACK WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, //
PSTR cmdline, int cmdshow) {
uint8 BigOldBlockOfMemory[1004 * 1024];
WNDCLASS WindowClass = {};
// 使用大括号初始化,所有成员都被初始化为零(0)或 nullptr
Win32ResizeDIBSection(&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
)
cpp
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
。
cpp
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
判断方向键上的"上"按钮是否被按下。
cpp
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 按钮等。
cpp
int16 StickX = Pad->sThumbLX;
int16 StickY = Pad->sThumbLY;
- 摇杆坐标 :
sThumbLX
和sThumbLY
分别表示左摇杆的 X 和 Y 坐标,范围是 -32768 到 32767。
XInput
主要用于读取 Xbox 控制器的输入状态,获取按钮按下的状态以及摇杆的位置信息。在每一帧循环中,代码都检查并处理控制器的状态,这在游戏开发中非常常见,用于实现用户输入响应。
编译程序会出现未定义的情况,但是XInput 对于游戏来说并不是必须得,程序并不是非要有XInput才能运行。
在你提供的代码中,定义了以下内容:
cpp
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_
允许你在运行时调用这些函数,而不需要在编译时链接库。
示例代码可能类似于:
cpp
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
。
例如:
cpp
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
函数。
cpp
/**
* @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_
现在编译可以编译通过
运行是由问题的
上面代码进一步修改
cpp
/**
* @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_
以下是对你代码中每一部分的详细注释:
cpp
/**
* @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
是宏的参数,它允许你为每个具体的函数定义提供自定义名称。
cpp
/**
* @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
参数允许我们为每个具体的函数定义提供一个自定义名称。
cpp
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
就变成了类型,表示那些符合这两个宏定义的函数。
cpp
X_INPUT_GET_STATE(XInputGetStateStub) { return (0); }
X_INPUT_SET_STATE(XInputSetStateStub) { return (0); }
XInputGetStateStub
和XInputSetStateStub
这两个函数是 函数占位符 。它们使用了由宏定义的签名,并简单地返回0
,表示一个空的实现。- 这些占位符函数提供了一个默认的行为,以确保在没有实际实现的情况下,程序仍能编译和运行,避免因为找不到具体实现而导致的错误。
cpp
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
实现。
cpp
#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 中的函数。
用法
cpp
HMODULE LoadLibrary(
LPCSTR lpFileName // DLL 文件的路径或名称
);
lpFileName
: DLL 文件的路径,可以是绝对路径或相对路径。如果 DLL 文件位于系统目录或环境变量路径中,可以只指定文件名。
返回值
- 成功时,返回加载的 DLL 模块的句柄(
HMODULE
)。可以使用该句柄调用GetProcAddress
函数来获取 DLL 中的函数地址。 - 失败时,返回
NULL
,并且可以通过GetLastError
获取错误代码。
示例
以下是使用 LoadLibrary
动态加载 DLL 并调用其中一个函数的示例:
cpp
#include <windows.h>
#include <iostream>
typedef int (CALLBACK* AddFunc)(int, int); // 定义一个函数指针类型
int main() {
// 加载 DLL
HMODULE 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;
}
// 卸载 DLL
FreeLibrary(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 控制器的振动功能。它通过将特定的振动模式信息发送到控制器,来激活相应的振动马达。它的函数原型如下:
cpp
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
结构的定义如下:cpptypedef struct _XINPUT_VIBRATION { WORD wLeftMotorSpeed; // 左振动马达的速度,取值范围 0 到 65535 WORD wRightMotorSpeed; // 右振动马达的速度,取值范围 0 到 65535 } XINPUT_VIBRATION;
-
wLeftMotorSpeed
和wRightMotorSpeed
分别表示左、右振动马达的振动强度。 -
数值范围从
0
(关闭)到65535
(最大振动强度)。
-
返回值
- 返回一个
DWORD
值,表示执行的结果。 - 常见返回值包括:
ERROR_SUCCESS
:函数成功执行。ERROR_DEVICE_NOT_CONNECTED
:控制器未连接或无法识别。
使用示例
下面是一个简单的使用 XInputSetState
激活控制器振动的示例,将左马达设置为最大振动,右马达设置为中等振动:
cpp
#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'
)时执行相应操作。
以下是代码的逐步解析:
cpp
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
。 -
可以使用如下代码检查这一位状态:
cppbool isHeld = (lParam & (1 << 30)) != 0;
-
当
isHeld
为true
时,表示该按键正在被按住,否则表示这是按键首次被按下。
-
示例说明
结合上述内容,完整代码块用于检查键盘事件。如果 'W'
键被按下,它会将消息输出到调试窗口,同时可以利用 lParam
进一步判断按键是初次按下还是被持续按住。
cpp
// 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 bool
Running; // 用于控制程序运行的全局布尔变量,通常用于循环条件
global_variable win32_offscreen_buffer
GlobalBackbuffer; // 用于存储屏幕缓冲区的全局变量
/**
* @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, // 要释放的大小(字节),对部分释放有效,整体释放则设为 0
MEM_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; // 设置颜色平面数,通常为 1
Buffer->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)或 nullptr
Win32ResizeDIBSection(&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;
}