游戏引擎学习第六天

这节讲的内容比较多:

参考视频: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 手柄)的当前状态。函数原型如下:

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

1. 避免直接链接

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

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

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

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

3. 动态加载 XInput 库

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

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

例如:

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_STATEX_INPUT_SET_STATE 创建的函数签名分别定义为 x_input_get_statex_input_set_state 类型别名。这样,x_input_get_statex_input_set_state 就变成了类型,表示那些符合这两个宏定义的函数。
cpp 复制代码
X_INPUT_GET_STATE(XInputGetStateStub) { return (0); }
X_INPUT_SET_STATE(XInputSetStateStub) { return (0); }
  • XInputGetStateStubXInputSetStateStub 这两个函数是 函数占位符 。它们使用了由宏定义的签名,并简单地返回 0,表示一个空的实现。
    • 这些占位符函数提供了一个默认的行为,以确保在没有实际实现的情况下,程序仍能编译和运行,避免因为找不到具体实现而导致的错误。
cpp 复制代码
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 实现。
cpp 复制代码
#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 中的函数。

用法

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;
}

关键点解释

  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 控制器的振动功能。它通过将特定的振动模式信息发送到控制器,来激活相应的振动马达。它的函数原型如下:

cpp 复制代码
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 结构的定义如下:

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

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

主要部分详解

  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

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

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

示例说明

结合上述内容,完整代码块用于检查键盘事件。如果 '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;
}
相关推荐
向宇it5 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
朝九晚五ฺ6 小时前
【Linux探索学习】第十四弹——进程优先级:深入理解操作系统中的进程优先级
linux·运维·学习
芋芋qwq7 小时前
Unity UI射线检测 道具拖拽
ui·unity·游戏引擎
tealcwu7 小时前
【Unity服务】关于Unity LevelPlay的基本情况
unity·游戏引擎
猫爪笔记8 小时前
前端:HTML (学习笔记)【1】
前端·笔记·学习·html
pq113_68 小时前
ftdi_sio应用学习笔记 3 - GPIO
笔记·学习·ftdi_sio
澄澈i8 小时前
设计模式学习[8]---原型模式
学习·设计模式·原型模式
爱米的前端小笔记9 小时前
前端八股自学笔记分享—页面布局(二)
前端·笔记·学习·面试·求职招聘
alikami10 小时前
【前端】前端学习
学习
一只小菜鸡..10 小时前
241118学习日志——[CSDIY] [ByteDance] 后端训练营 [06]
学习