游戏引擎学习第六天

这节讲的内容比较多:
参考视频:https://www.bilibili.com/video/BV1apmpYVEQu/

XInput 是微软提供的一个 API,用于处理 Windows 平台上 Xbox 控制器(包括有线和无线)及其他游戏控制器的输入。它为开发者提供了一组函数,用于查询控制器的状态、接收按钮按下事件以及管理震动反馈。XInput 通过提供一个标准化的接口,简化了游戏控制器的支持,免去了处理低级细节的需要。

XInput 的主要功能:

  1. 按钮状态:提供查询特定按钮(如 A、B、X、Y、Start、Back 等)是否按下或释放的函数。
  2. 模拟输入:支持触发器(LT、RT)和摇杆(左摇杆、右摇杆)的模拟输入。
  3. 震动反馈:可以向控制器发送震动信号(例如,游戏事件发生时,像是受到伤害时的震动)。
  4. 控制器连接:检测控制器的连接和断开状态。

常用的 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 表示支持多个控制器,但通常情况下它会检查从 04 的控制器(最多支持四个)。
  • 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;
  • 摇杆坐标sThumbLXsThumbLY 分别表示左摇杆的 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 库中的 XInputGetStateXInputSetState 函数,而不是直接链接这些函数。这种做法通常用于动态加载共享库或 DLL 时,能够在运行时加载和调用这些函数。这样做有几个目的:

1. 避免直接链接

这种方式使用了函数指针和 #define 宏,代替了直接链接静态或动态库。通常,这种做法有以下好处:

  • 动态链接:程序不直接链接到 XInput 库,而是在运行时加载它。这样,你可以通过修改程序的 dll 路径来改变使用的 XInput 版本,而无需重新编译整个程序。
  • 延迟加载:只有在需要的时候才会加载 XInput 函数,这对于某些不一定会使用 XInput 功能的程序来说,能减少启动时的资源消耗。

2. 加载 DLL 并获取函数地址

通过这样的声明,程序可以在运行时加载动态链接库(DLL)并通过 GetProcAddress 或类似的机制来获取 XInputGetStateXInputSetState 的函数地址。函数指针 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 并获取 XInputGetStateXInputSetState 函数的地址。然后,通过 XInputGetState_XInputSetState_ 指针来调用这两个函数。

3. 动态加载 XInput 库

如果你不想静态链接 XInput 库,而是想使用动态加载的方式,你需要确保:

  • 使用 LoadLibraryGetProcAddress 动态加载 XInput DLL。
  • 将上述定义的函数指针与 GetProcAddress 配对,确保在运行时能够正确解析 XInputGetStateXInputSetState

例如:

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_STATEX_INPUT_SET_STATE 创建的函数签名分别定义为 x_input_get_statex_input_set_state 类型别名。这样,x_input_get_statex_input_set_state 就变成了类型,表示那些符合这两个宏定义的函数。
X_INPUT_GET_STATE(XInputGetStateStub) { return (0); }
X_INPUT_SET_STATE(XInputSetStateStub) { return (0); }
  • XInputGetStateStubXInputSetStateStub 这两个函数是 函数占位符。它们使用了由宏定义的签名,并简单地返回 0,表示一个空的实现。
    • 这些占位符函数提供了一个默认的行为,以确保在没有实际实现的情况下,程序仍能编译和运行,避免因为找不到具体实现而导致的错误。
global_variable x_input_get_state *XInputGetState_ = XInputGetStateStub;
global_variable x_input_set_state *XInputSetState_ = XInputSetStateStub;
  • 这两行代码定义了两个全局变量 XInputGetState_XInputSetState_,分别是指向 x_input_get_statex_input_set_state 类型的函数指针,初始值分别为 XInputGetStateStubXInputSetStateStub
  • 这意味着在程序的其他地方,调用 XInputGetState_XInputSetState_ 实际上是在调用 XInputGetStateStubXInputSetStateStub 函数,直到在运行时动态加载并替换这些函数指针为实际的 XInputGetStateXInputSetState 实现。
#define XInputGetState XInputGetState_
#define XInputSetState XInputSetState_
  • 通过这两行宏定义,XInputGetStateXInputSetState 被重新定义为 XInputGetState_XInputSetState_。这样,程序中的所有 XInputGetStateXInputSetState 调用将会转向实际的全局函数指针 XInputGetState_XInputSetState_
    • 这实际上是为 XInputGetStateXInputSetState 提供了动态加载的能力。

代码目的总结:

  1. 宏定义X_INPUT_GET_STATEX_INPUT_SET_STATE 使得我们能够定义带有相同函数签名的多个函数。宏接受一个 name 参数,用于创建不同的函数。
  2. 函数占位符XInputGetStateStubXInputSetStateStub 作为占位符函数,提供了默认的返回值,以便在没有实际函数实现时,程序仍能编译和运行。
  3. 函数指针XInputGetState_XInputSetState_ 是全局的函数指针,最初指向占位符函数 XInputGetStateStubXInputSetStateStub,允许在运行时动态替换为实际的实现。
  4. 动态替换#define 宏使得程序中对 XInputGetStateXInputSetState 的调用实际指向全局函数指针 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;
}

关键点解释

  1. 加载 DLL: 使用 LoadLibrary 加载 DLL,返回一个句柄 (HMODULE),这是 DLL 的标识符。
  2. 获取函数地址: 使用 GetProcAddress 获取 DLL 中某个函数的地址。你需要提供该函数的名称(字符串形式)或者函数的符号。
  3. 调用函数: 通过函数指针调用 DLL 中的函数。
  4. 卸载 DLL: 使用 FreeLibrary 卸载已经加载的 DLL。

注意事项

  • 路径: 如果没有提供完整路径,LoadLibrary 会在当前工作目录、系统目录、Windows 目录等标准路径中搜索 DLL。
  • 错误处理: 如果 LoadLibraryGetProcAddress 失败,可以通过 GetLastError 获取更多的错误信息。
  • 内存管理: 加载的 DLL 应在不需要时调用 FreeLibrary 卸载,以释放内存和资源。

常见错误

  • ERROR_MOD_NOT_FOUND: 指定的 DLL 文件未找到。
  • ERROR_PROC_NOT_FOUND: GetProcAddress 查找不到指定的函数。

通过 LoadLibraryGetProcAddress,你可以在运行时动态地加载和调用 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;

参数解释

  1. dwUserIndex (DWORD 类型):

    • 表示与设备关联的玩家索引,即控制器编号。
    • 常用值范围是 03,分别表示最多支持的四个控制器。
    • 例如,当值为 0 时表示玩家 1 的控制器;当值为 1 时,表示玩家 2 的控制器。
  2. pVibration (XINPUT_VIBRATION* 类型):

    • 指向 XINPUT_VIBRATION 结构的指针,该结构包含控制器的振动强度信息。

    • XINPUT_VIBRATION 结构的定义如下:

      typedef struct _XINPUT_VIBRATION {WORD wLeftMotorSpeed;  // 左振动马达的速度,取值范围 0 到 65535WORD wRightMotorSpeed; // 右振动马达的速度,取值范围 0 到 65535
      } XINPUT_VIBRATION;
      
    • wLeftMotorSpeedwRightMotorSpeed 分别表示左、右振动马达的振动强度。

    • 数值范围从 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_KEYDOWNWM_KEYUPWM_SYSKEYDOWNWM_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;

主要部分详解

  1. wParam 的用途

    • wParam 代表按键的虚拟键码 (VkCode),用于标识哪个键被按下或释放。例如,'W' 的虚拟键码是 0x57
    • 在代码中,通过 if (VkCode == 'W') 判断是否按下了 'W' 键。
  2. OutputDebugStringA("W\n")

    • 这是一个用于调试的函数,会在输出窗口中显示 W 和换行符 \n
    • 每当按下 'W' 键时,程序会将 "W\n" 输出到调试控制台。
  3. 使用 lParam 获取按键状态

    • lParam 包含了更多关于按键的状态信息。例如,代码注释中的 LParam & (1 << 30) 试图查看按键的重复标志。

    • lParam 的第 30 位表示按键是否已经按下并保持按住。按键被连续按住时,系统会将该位设置为 1

    • 可以使用如下代码检查这一位状态:

      bool isHeld = (lParam & (1 << 30)) != 0;
      
    • isHeldtrue 时,表示该按键正在被按住,否则表示这是按键首次被按下。

示例说明

结合上述内容,完整代码块用于检查键盘事件。如果 '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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/60336.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vivado+modelsim: xxx is not a function name

xxx is not a function name vivado问题:xxx is not a function name原因 vivado问题:xxx is not a function name 在写verilog modelsim仿真时&#xff0c;遇到error&#xff1a;xxx is not a function name。 原因 该变量xxx在仿真文件里&#xff0c;如下图红框所示&#…

云计算在教育领域的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 云计算在教育领域的应用 云计算在教育领域的应用 云计算在教育领域的应用 引言 云计算概述 定义与原理 发展历程 云计算的关键技…

立体工业相机提升工业自动化中的立体深度感知

深度感知对仓库机器人应用至关重要&#xff0c;尤其是在自主导航、物品拾取与放置、库存管理等方面。 通过将深度感知与各种类型的3D数据&#xff08;如体积数据、点云、纹理等&#xff09;相结合&#xff0c;仓库机器人可以在错综复杂环境中实现自主导航&#xff0c;物品检测…

模拟鼠标真人移动轨迹算法-易语言

一.简介 鼠标轨迹算法是一种模拟人类鼠标操作的程序&#xff0c;它能够模拟出自然而真实的鼠标移动路径。 鼠标轨迹算法的底层实现采用C/C语言&#xff0c;原因在于C/C提供了高性能的执行能力和直接访问操作系统底层资源的能力。 鼠标轨迹算法具有以下优势&#xff1a; 模拟…

JavaWeb——Web入门(8/9)- Tomcat:基本使用(下载与安装、目录结构介绍、启动与关闭、可能出现的问题及解决方案、总结)

目录 基本使用内容 下载与安装 目录结构介绍 启动与关闭 启动 关闭 可能出现的问题及解决方案 问题一&#xff1a;启动时窗口一闪而过 问题二&#xff1a;端口号冲突 问题三&#xff1a;部署应用程序 总结 基本使用内容 Tomcat 服务器在 Java Web 开发中扮演着至关重…

PostgreSQL中如果有Left Join的时候索引怎么加

在PostgreSQL中&#xff0c;当你的查询包含多个LEFT JOIN和WHERE条件时&#xff0c;合理地添加索引可以显著提高查询性能。以下是一些具体的优化步骤和建议&#xff1a; 1. 分析查询 使用 EXPLAIN ANALYZE 命令分析你的查询&#xff0c;了解查询的执行计划&#xff0c;识别出连…

通过DNS服务器架构解释DNS请求过程

在前面的章节,这里,基于PCAP数据包和RFC文档详细介绍了DNS请求和响应的每个字段的含义。但是在现实的网络世界中,DNS请求和响应的数据包是怎么流动的,会经过哪些设备。本文将着重说明一下目前网络空间中DNS请求和响应的流动过程。 当前网络空间中比较常见DNS请求的流程如下…

aspose如何获取PPT放映页“切换”的“持续时间”值

aspose如何获取PPT放映页“切换”的“持续时间”值 项目场景问题描述问题1&#xff1a;从官方文档和资料查阅发现并没有对切换的持续时间进行处理的方法问题2&#xff1a;aspose的依赖包中&#xff0c;所有的关键对象都进行了混淆处理 解决方案1、找到ppt切换的持续时间对应的混…

GIT:如何查找已删除的文件的历史记录

首先你得知道文件的名称和路径 然后打开 gitlab&#xff0c;到项目中&#xff0c;仓库-> 文件 查找文件 复制文件名到可能存在过这个文件的分支当中&#xff0c;就能看到了

自动渗透测试与手动渗透测试

根据《渗透测试中发现的 5 种常见网络安全威胁》报告&#xff0c;渗透测试越来越受欢迎。预计到 2025 年&#xff0c;渗透测试市场规模将达到 45 亿美元。 什么是自动渗透测试&#xff1f; 自动化渗透测试工具可以快速有效地检查系统中是否存在已知的安全问题&#xff0c;即使…

使用elementUI实现表格行拖拽改变顺序,无需引入外部库

前言&#xff1a; 使用vue2element UI&#xff0c;且完全使用原生的拖拽事件,无需引入外部库。 如果表格数据量较大&#xff0c;或需要更多复杂功能&#xff0c;可以考虑使用 vuedraggable库&#xff0c;提供更多配置选项和拖拽功能。 思路&#xff1a; 1. 通过el-table的ro…

WPF Prism框架

Prism 是一个开源框架&#xff0c;专门用于开发可扩展、模块化和可测试的企业级 XAML 应用程序&#xff0c;适用于 WPF&#xff08;Windows Presentation Foundation&#xff09;和 Xamarin Forms 等平台。它基于 MVVM&#xff08;Model-View-ViewModel&#xff09;设计模式&am…

C#开发流程

注&#xff1a;检查数据库链接 设置搜索 1.新建模块文件夹 对应应用 右键-添加-新建文件夹 2.新建类 在新建模块下右键 新建-类&#xff0c;修改类名称 修改internal为public 新建所需字段&#xff0c;注意类型声明及必填设置 [SugarColumn(IsNullable false)]public strin…

区块链应用第1讲:基于区块链的智慧货运平台

基于区块链的智慧货运平台 网络货运平台已经比较成熟&#xff0c;提供了给货源方提供找司机的交易匹配方案&#xff1b;其中包含这几个角色&#xff1a;货主、承运人(司机、车队长)、监管机构、平台。司机要想接单&#xff0c;依赖于多个中心化的第三方平台&#xff0c;且三方平…

计算机毕业设计 | SpringBoot智慧⾼校学术报告系统 AI写作大模型生成平台(附源码)

1&#xff0c;项目介绍 智慧⾼校学术报告系统是⼀个基于 SpringBoot 开发的标准 Java Web 项⽬。系统整体⻚⾯设计简约⼤⽓&#xff0c;巧妙融合了⽬前备受瞩⽬的 AIGC ⽣成式 AI 技术&#xff0c;选择了阿⾥通⽤千问⼤语⾔模型&#xff0c;以智能⽣成趣味报告标题和润⾊报告内…

万字长文解读机器学习——决策树

&#x1f33a;历史文章列表&#x1f33a; 机器学习——损失函数、代价函数、KL散度机器学习——特征工程、正则化、强化学习机器学习——常见算法汇总机器学习——感知机、MLP、SVM机器学习——KNN机器学习——贝叶斯机器学习——决策树机器学习——随机森林、Bagging、Boostin…

在Django中安装、配置、使用CKEditor5,并将CKEditor5录入的文章展现出来,实现一个简单博客网站的功能

在Django中可以使用CKEditor4和CKEditor5两个版本&#xff0c;分别对应软件包django-ckeditor和django-ckeditor-5。原来使用的是CKEditor4&#xff0c;python manager.py makemigrations时总是提示CKEditor4有安全风险&#xff0c;建议升级到CKEditor5。故卸载了CKEditor4&…

实战项目:通过自我学习让AI学习五子棋 - 1 - 项目定义

项目介绍 五子棋是一种博弈游戏。在棋盘上黑子和白子交替落子&#xff0c;先于在任何方向上将至少五个棋子连在一起的一方获胜。在我们这个项目中我们尝试使用自学习的方法训练出一套走五子棋的算法。 这个项目本身并无特别大的实用价值。我们的目的在于&#xff1a; 尝试自…

从0开始搭建一个生产级SpringBoot2.0.X项目(十二)SpringBoot接口SpringSecurity JWT鉴权

前言 最近有个想法想整理一个内容比较完整springboot项目初始化Demo。 SpringBoot接口权限控制 SpringSecurity 接口使用 Bearer token类型 JWT 鉴权 一、pom文件新增依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>s…

JavaEE初阶---properties类+反射+注解

文章目录 1.配置文件properities2.快速上手3.常见方法3.1读取配置文件3.2获取k-v值3.3修改k-v值3.4unicode的说明 4.反射的引入4.1传统写法4.2反射的写法&#xff08;初识&#xff09;4.3反射的介绍4.4获得class类的方法4.5所有类型的class对象4.6类加载过程4.7类初始化的过程4…