游戏引擎学习第七天

视频参考:https://www.bilibili.com/video/BV1QFmhYcE69

ERROR_DEVICE_NOT_CONNECTED 是一个错误代码,通常在调用 XInputGetStateXInputSetState 函数时返回,表示指定的设备未连接。通常会出现以下几种情况:

  1. 未连接控制器:如果尝试访问的设备索引(如 0、1、2、3)没有相应的 XInput 控制器连接,调用 XInputGetState 时会返回 ERROR_DEVICE_NOT_CONNECTED,说明该设备未连接或不可用。

  2. 无效的玩家索引:XInput 支持的玩家索引范围是 0 到 3,表示最多支持 4 个设备。如果尝试访问的索引超出此范围,也可能导致该错误。

  3. 设备连接问题:如果控制器没有正确连接到电脑(比如 USB 或无线连接异常),也可能会出现此错误。

处理方式

处理 ERROR_DEVICE_NOT_CONNECTED 通常是为了避免在设备不连接时继续执行后续代码。可以参考以下代码:

DWORD dwResult;
for (DWORD ControllerIndex = 0; ControllerIndex < 4; ControllerIndex++) {XINPUT_STATE ControllerState;// 尝试获取控制器的状态dwResult = XInputGetState(ControllerIndex, &ControllerState);if (dwResult == ERROR_SUCCESS) {// 控制器已连接,可以访问 ControllerState 的数据} else if (dwResult == ERROR_DEVICE_NOT_CONNECTED) {// 设备未连接,跳过当前设备的处理} else {// 处理其他错误(如果有)}
}

注意事项

打桩函数的返回值

  • 通过检测 ERROR_DEVICE_NOT_CONNECTED 可以有效跳过未连接的控制器,防止对无效设备进行操作。
  • 使用循环遍历多个控制器时,可以用 XInputGetState 检查每个索引的设备状态,确保只对已连接的控制器执行操作。

我们的程序中有一个打桩的函数,XInputGetStateStub和XInputSetStateStub 之前是返回0,改为返回`ERROR_DEVICE_NOT_CONNECTED
在这里插入图片描述

关于库的选择

xinput1_4.dllxinput1_3.dll 是微软提供的 XInput 库的两个不同版本,主要用于与游戏控制器(如 Xbox 控制器)进行交互。两者之间的区别在于其系统支持和部分功能特性:

1. xinput1_4.dll

- **版本**:这是 Windows 8 和更高版本操作系统上的 XInput 版本。
- **系统支持**:仅支持 Windows 8、Windows 10 及更高版本(包括 Windows 11)。
- **文件位置**:通常系统自带,无需额外安装。
- **优势**:`xinput1_4.dll` 支持最新的 Windows 功能和性能优化,通常被推荐用于 Windows 8 以上的系统。

2. xinput1_3.dll

- **版本**:这是较旧的 XInput 版本,通常与 DirectX SDK(2010 年 6 月版本)一起发布。
- **系统支持**:适用于 Windows XP、Vista、Windows 7 及以上版本。Windows 8 和 10 也支持该版本,但在这些系统上会更推荐使用 `xinput1_4.dll`。
- **文件位置**:不会随操作系统自动安装。需要通过安装 DirectX Redistributable 来获得。
- **优势**:对于向后兼容性更好,特别是需要支持 Windows 7 及更早的系统时。许多较旧的游戏依赖 `xinput1_3.dll`,因此在开发老旧系统或特定平台兼容性要求的应用时,这个库更合适。

选择使用的 XInput 库

  • 优先级:一般情况下优先选择 xinput1_4.dll,并在 Windows 8 以上的系统使用。如果程序需要支持 Windows 7 或更早的系统,则使用 xinput1_3.dll
  • 动态加载:可以在代码中使用 LoadLibrary 动态加载库文件,并根据操作系统或用户环境动态选择。例如,优先加载 xinput1_4.dll,如果加载失败则尝试加载 xinput1_3.dll

动态加载示例

以下代码尝试优先加载 xinput1_4.dll,如果失败则加载 xinput1_3.dll

void Win32LoadXInput(void) {HMODULE XInputLibrary = LoadLibrary("xinput1_4.dll");if (!XInputLibrary) {// 如果无法加载 xinput1_4.dll,则回退到 xinput1_3.dllXInputLibrary = LoadLibrary("xinput1_3.dll");}if (XInputLibrary) {XInputGetState = (x_input_get_state *)GetProcAddress(XInputLibrary, "XInputGetState");if (!XInputGetState) { XInputGetState = XInputGetStateStub; }XInputSetState = (x_input_set_state *)GetProcAddress(XInputLibrary, "XInputSetState");if (!XInputSetState) { XInputSetState = XInputSetStateStub; }}
}

兼容性考虑

  • 如果软件需要在多个 Windows 版本上运行,最好兼容 xinput1_3.dll,以确保兼容 Windows 7 及更早的系统。
  • 需要注意在某些低版本 Windows 系统上,xinput1_4.dll 可能会无法找到,而导致运行时错误。
    在这里插入图片描述

添加在游戏卡死,ALT+F4 可以退出程序打代码

在 Windows 消息处理中,可以通过检查 lParam 的第 29 位来判断是否按下了 Alt 键。

具体方法如下:

  • lParam & (1 << 29) != 0 :当此表达式为 true 时,表示 Alt 键处于按下状态。

在 Windows 中,第 29 位的标志位在系统按键消息(如 WM_SYSKEYDOWNWM_SYSKEYUP)中指示 Alt 键是否被按下。这适用于处理 Alt+F4 等快捷键组合。

下面是如何判断 Alt 键是否被按下的示例代码:

bool AltKeyWasDown = (LParam & (1 << 29)) != 0;
  • AltKeyWasDown 会为 true,则表示 Alt 键在当前消息中处于按下状态。

将其应用到消息处理中,可以实现类似以下的效果:

case WM_SYSKEYDOWN:
case WM_KEYDOWN: {uint64 VKCode = wParam;bool AltKeyWasDown = (LParam & (1 << 29)) != 0;if (AltKeyWasDown && VKCode == VK_F4) {GlobalRunning = false; // 实现按下 Alt+F4 退出程序}
}

这样即可在程序中检测到 Alt 键的状态,用于判断组合键快捷操作。
按alt+f4 退出游戏
在这里插入图片描述

遇到的问题

bool AltKeyWasDown = ((LParam & (1 << 29)) != 0); // 检查Alt键是否被按下
bool AltKeyWasDown = (LParam & (1 << 29) != 0); // 检查Alt键是否被按下

这两种写法有一个重要的区别在于运算符的优先级:

  1. bool AltKeyWasDown = ((LParam & (1 << 29)) != 0);
    - 这个写法使用了外层的括号,确保了 (LParam & (1 << 29)) 会首先进行计算,然后再判断是否不等于 0
    - 正确结果:先进行位与运算,再进行不等于 0 的判断。

  2. bool AltKeyWasDown = (LParam & (1 << 29) != 0);
    - 这个写法没有外层括号,按运算优先级, (1 << 29) != 0 会首先计算,然后再与 LParam 进行位与运算。
    - 错误结果:因为 (1 << 29) != 0 始终为 true(即 1),所以代码等价于 LParam & 1,这并不是我们想要的检查结果。

因此,第一种写法是正确的,而第二种写法会导致逻辑错误,无法正确检查 Alt 键是否被按下。

VS 生成的CMAKE 中添加编译选项

在这里插入图片描述

bool AltKeyWasDown = ((LParam & (1 << 29)) != 0);

改成

typedef int32 bool32;
bool32 AltKeyWasDown = (LParam & (1 << 29));

我们来逐一分析:

1. LParam 的类型

LParamlong 类型,通常在 32 位系统中是 4 字节(32 位),在 64 位系统中是 8 字节(64 位)。C 语言和 C++ 中 long 类型的大小通常取决于系统架构(32 位或 64 位),所以在 32 位系统上 LParam 是 32 位的,按位操作和比较应该是合法的。

2. 原始代码的理解

原始代码:

bool AltKeyWasDown = ((LParam & (1 << 29)) != 0);  // 检查Alt键是否被按下

这里进行的是按位与操作 (LParam & (1 << 29)),然后判断结果是否不等于 0,即判断是否设置了第 29 位。(1 << 29) 是将数字 1 左移 29 位,生成一个二进制值,表示第 29 位。这样,LParam & (1 << 29) 的结果将会是非零值,表示第 29 位被设置。如果结果不等于 0,则说明 Alt 键被按下。

3. != 0 的作用

!= 0 的作用是检查按位与结果是否非零。如果是零,表示该位未被设置;如果非零,表示该位被设置。因此,((LParam & (1 << 29)) != 0) 是一个布尔值,用来表示 Alt 键是否被按下。

问题
LParamlong 类型,通常在 32 位系统中是 32 位。而 LParam & (1 << 29) 会得到一个结果,这个结果仍然是 long 类型,并且该值的大小可能是 long 类型的大小。比较 != 0 本身并不会有问题,因为最终比较的结果是布尔值。

4. 为什么要改成 bool32 和修改代码

修改:

typedef int32 bool32;
bool32 AltKeyWasDown = (LParam & (1 << 29));  // 检查Alt键是否被按下

这个修改实际上简化了 AltKeyWasDown 的存储类型和代码,避免了不必要的比较:

  1. 不需要 != 0
    LParam & (1 << 29) 结果是一个整数,LParam 的按位操作结果已经是非零或者零,直接用 bool32 类型来存储这个值,并可以让它显式地表示为布尔值。
- 如果 `(LParam & (1 << 29))` 为零,`AltKeyWasDown` 会被赋值为 `0`。
- 如果 `(LParam & (1 << 29))` 为非零,`AltKeyWasDown` 会被赋值为非零值,通常表示 `true`。
  1. bool32 是一个 32 位整数
    bool32int32 类型,这与 long 类型(通常也是 32 位)大小匹配,可以方便地进行按位操作并保证布尔值存储的一致性。如果你只想把它当作布尔值使用,bool32 作为 int32 类型可以方便地进行算术和按位操作。

5. 总结

  • 使用 bool 类型和 != 0 比较是完全可以的,它会将按位与操作的结果转换为布尔值。
  • 改为 bool32 类型并去掉 != 0 的比较简化了代码,同时让你可以将按位操作的结果直接存储在 bool32 类型的变量中(这通常是 int32 类型)。你可以通过 0 或非 0 值来表示布尔值,这样减少了额外的比较。

关于DefWindowProc

DefWindowProc 是 Windows 操作系统中用来处理窗口消息的默认窗口过程。它负责处理未被应用程序明确处理的消息,比如标准的窗口行为(最小化、最大化、关闭等),以及一些系统级的消息。当你不显式处理某些消息时,调用 DefWindowProc 可以确保消息仍然得到适当的处理。

处理了多种按键相关的消息 (WM_SYSKEYDOWN, WM_SYSKEYUP, WM_KEYDOWN, WM_KEYUP),这些消息都是键盘事件。如果你需要处理这些按键事件并执行一些特定的操作,你可以在消息处理块中添加相应的代码。

然而,对于一些你不关心的消息,或者你没有处理的特殊情况,DefWindowProc 会被调用。这意味着即使你在处理按键事件时做了自己的处理,DefWindowProc 仍然会确保其他未处理的消息继续按照默认方式进行处理。

对于 WS_WM_SYSKEYUPWM_SYSKEYDOWN 的例子,在你自定义了按键的处理后,如果你没有特殊需求,调用 DefWindowProc 可以确保这些消息能够传递给系统进行默认处理。例如,如果你没有处理 Alt 键或系统级的按键,DefWindowProc 会负责执行它们的默认行为,比如关闭窗口、激活窗口等。

关于XInput的bug

XInputGetState 性能问题是一个众所周知的限制,尤其是在调用时未插入控制器的情况下。如果轮询所有控制器(即使某些控制器未连接),XInputGetState 可能会造成延迟和不必要的 CPU 消耗。

为了解决这个问题,你可以尝试通过以下方法只轮询已连接的控制器,而不是全部控制器:

1. 首先检查控制器是否连接

使用 XInputGetState 之前,可以通过调用 XInputGetState 来检查每个控制器是否已连接。XInputGetState 的返回值可以帮助判断控制器是否存在,如果控制器未连接,返回值会是 ERROR_DEVICE_NOT_CONNECTED

2. 只轮询连接的控制器

你可以通过一个循环检查每个控制器的连接状态,只有在控制器连接时才调用 XInputGetState。以下是如何实现这一策略的示例代码:

// 轮询连接的控制器,避免轮询未连接的控制器
for (DWORD ControllerIndex = 0; ControllerIndex < XUSER_MAX_COUNT; ControllerIndex++) {// 定义一个 XINPUT_STATE 结构体,用来存储控制器的状态XINPUT_STATE ControllerState;// 先检查控制器是否已连接DWORD dwResult = XInputGetState(ControllerIndex, &ControllerState);if (dwResult == 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);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);// 获取摇杆坐标int16 StickX = Pad->sThumbLX;int16 StickY = Pad->sThumbLY;// 检测 A 按钮并进行处理if (AButton) {yOffset += 2;}} else {// 控制器未连接,跳过该控制器// 可以输出日志来确认未连接的控制器// OutputDebugString("Controller not connected\n");}
}

3. 如何提高性能

  1. 减少轮询频率:你可以通过增加轮询控制器的间隔来减少 CPU 占用。例如,每隔几帧轮询一次控制器,而不是每帧都轮询。

  2. 优化控制器检查:如果你能够预知哪些控制器是插入的,可以将已连接控制器的索引缓存起来,从而避免每次都需要进行连接检查。

  3. 异步处理:如果需要多个控制器数据并且不希望阻塞主线程,可以考虑使用线程或异步方式来处理 XInputGetState 的调用。

总结:

  • 通过 XInputGetState 返回值判断控制器是否连接,如果未连接,则跳过不需要轮询的控制器。
  • 优化轮询频率或只处理已连接的控制器,可以有效减少 CPU 占用,避免性能下降。

这样做可以显著提升你的程序性能,避免不必要的 XInputGetState 调用。

关于音频

这段文字描述了音频缓冲区的基本工作原理,以及如何通过 DirectSound 来处理音频输出。主要内容可以理解为:

  1. 缓冲区的分配与循环播放:描述了音频缓冲区的作用,并指出缓冲区是循环的,这意味着音频数据会不断从缓冲区中读取并播放。当音频播放到缓冲区末尾时,它会回到开头,继续循环播放。

  2. 缓冲区大小和采样率:音频采样率是 48 kHz,这意味着每秒钟有 48,000 个音频样本。假设缓冲区大小是 2 秒,那么缓冲区中的样本数大约为 96,000 个。每帧画面需要输出 800 个音频样本,这样可以确保音频和视频同步。

  3. 音频同步问题:提到音频与视频的同步是一个挑战,特别是在游戏中,视频帧与音频播放需要紧密配合。为了避免音频播放过程中出现问题,需要确保音频样本的写入比播放稍微提前一点。

  4. 缓冲区写入方式:由于直接写入缓冲区会与正在播放的音频产生冲突,因此需要提前写入数据,即写入播放位置之前的部分。这避免了写入过程中出现播放与写入冲突的问题。

  5. DirectSound:通过 DirectSound 进行音频播放,音频缓冲区会一直循环,直到程序结束或需要停止播放。

  6. 画面和音频的同步:在游戏循环中,视频帧和音频需要同时准备好并同步播放。这需要通过合适的缓冲区操作和页面翻转来实现。

下面开始写声音相关代码

DirectSoundCreate 是 DirectSound API 中的一个函数,用于创建一个新的 IDirectSound 对象,这是一个用于音频输出和音效控制的接口。它是 DirectSoundCreate8 函数的前身,主要用于 DirectX 7 及之前的版本。不过,DirectSoundCreateDirectSoundCreate8 都属于音频设备初始化的关键函数,在音频播放、处理和管理过程中都起着至关重要的作用。

函数声明:

HRESULT WINAPI DirectSoundCreate(_In_opt_ LPCGUID pcGuidDevice, //这个参数用来指定一个特定的音频设备。如果传入 `NULL`,则 DirectSound 会选择默认音频设备进行操作。_Outptr_ LPDIRECTSOUND *ppDS,//这是一个输出参数,调用成功时,它将指向创建的 `IDirectSound` 接口对象。该接口允许开发者控制音频设备、创建音频缓冲区等。//- 你通过这个接口进行音频播放、音效处理等操作。_Pre_null_ LPUNKNOWN pUnkOuter//通常为 `NULL`,因为 DirectSound API 并不需要外部聚合对象。一般情况下,传入 `NULL` 即可。
);

返回值:

  • HRESULT:返回操作结果的状态代码。
    • DS_OK:表示函数成功执行并创建了 IDirectSound 对象。
    • 如果返回其他错误代码(如 E_OUTOFMEMORYDSERR_NODRIVER 等),表示函数执行失败,通常是由于设备不可用或内存不足等原因。

功能概述:

DirectSoundCreate 的主要功能是初始化音频设备并返回一个 IDirectSound 接口。该接口用于进一步控制音频设备,如设置设备的合作级别、创建音频缓冲区、播放音频数据等。

使用步骤:

  1. 创建音频设备对象
    通过调用 DirectSoundCreate 创建一个 IDirectSound 对象,这个对象用于后续的音频控制。

  2. 设置合作级别
    使用 IDirectSound::SetCooperativeLevel 设置音频设备的合作级别,指定程序与设备的交互方式。例如,可以设置音频播放的优先级(DSSCL_PRIORITY)或允许后台播放等。

  3. 创建音频缓冲区
    使用 IDirectSound 接口可以创建主缓冲区(Primary Sound Buffer)和辅助缓冲区(Secondary Sound Buffers)。这些缓冲区用于存储和播放音频数据。

  4. 播放音频
    通过填充音频缓冲区并播放它们,可以实现游戏或应用中的音效和背景音乐的播放。

示例代码:

#include <windows.h>
#include <dsound.h>LPDIRECTSOUND pDS = NULL;HRESULT CreateDirectSound()
{HRESULT hr = DirectSoundCreate(NULL, &pDS, NULL);if (FAILED(hr)){// 错误处理return hr;}// 设置合作级别,指定音频设备的优先级hr = pDS->SetCooperativeLevel(hWnd, DSSCL_PRIORITY);if (FAILED(hr)){// 错误处理return hr;}// 可以继续创建音频缓冲区并播放return S_OK;
}

关键点总结:

  • DirectSoundCreate 是 DirectSound API 中创建音频设备接口的函数,适用于 DirectX 7 及更早版本。
  • 它返回一个 IDirectSound 接口,通过该接口可以控制音频输出、设置音频设备属性、创建音频缓冲区等。
  • 参数 pcGuidDevice 用于选择音频设备,如果为空则选择默认设备;ppDS 用于返回创建的接口;pUnkOuter 一般为 NULL
  • 返回值 HRESULT 表示函数是否执行成功。

DirectSoundCreate8 的区别:

  • DirectSoundCreate 用于旧版本的 DirectX(如 DirectX 7),而 DirectSoundCreate8 是 DirectX 8 中的新版本,提供了更多的功能和增强的兼容性。
  • DirectSoundCreate8 创建的是 IDirectSound8 接口,而 DirectSoundCreate 创建的是 IDirectSound 接口。IDirectSound8 接口提供了更多的功能,例如更好的硬件加速支持和更多的音频控制选项。

DirectSoundCreate 是较早版本 DirectX 中使用的函数,适用于较老的音频应用,但仍然在某些需要兼容旧版 DirectX 的环境中使用。

在这里插入图片描述

定义初始化声音的函数

在这里插入图片描述

对应的函数也是冲动态库中加载
现在定义DirectSoundCreate函数,类似之前的XInput的写法
在这里插入图片描述

SetCooperativeLevel

DirectSound::SetCooperativeLevel 是 DirectSound 中用于设置音频设备与应用程序之间协作级别的函数。它定义了应用程序与音频硬件之间的交互方式,特别是如何与其他正在运行的应用程序进行协调,尤其是音频播放时的优先级、资源访问等。

函数原型

HRESULT SetCooperativeLevel(HWND hwnd,  // 传入窗口句柄,通常是应用程序的主窗口DWORD dwLevel  // 设置的协作级别
);

参数解析:

  • hwnd:窗口句柄(HWND)。这个窗口句柄指定了应用程序与音频硬件的交互是与哪个窗口相关的。通常,你传入主窗口的句柄(GetConsoleWindow()GetActiveWindow())。

  • dwLevel:协作级别(DWORD),指定应用程序希望与音频设备的合作方式。协作级别会影响应用程序对音频硬件的访问权限和控制方式,通常有以下几种常见的协作级别:

常见的协作级别

  1. DSSCL_NORMAL
    - 表示应用程序请求正常的协作级别。
    - 这是最常用的级别,允许应用程序在系统内运行时独占访问音频硬件。
    - 应用程序可以播放音频,同时还允许其他应用程序同时访问音频设备。

  2. DSSCL_PRIORITY
    - 表示应用程序请求优先级较高的协作级别。
    - 该级别允许应用程序获得更高的音频资源访问优先级,因此应用程序可能会占用更多的音频资源,其他应用程序对音频的访问会被限制或阻塞。

  3. DSSCL_EXCLUSIVE
    - 表示应用程序请求独占访问音频设备。
    - 在此级别下,应用程序将独占音频硬件资源,其他应用程序不能访问音频设备。这通常用于需要高性能音频处理的场景,比如游戏应用,确保音频播放不被其他程序的干扰。

  4. DSSCL_WRITEPRIMARY
    - 在主缓冲区的写入权限下运行应用程序。

使用场景:

  • SetCooperativeLevel 在应用程序启动时,通常是在初始化 DirectSound 之后调用。它告知 DirectSound 音频设备如何与应用程序交互,特别是如何在多个应用程序之间共享音频资源。
    • 游戏应用程序:通常会使用 DSSCL_EXCLUSIVE 协作级别来确保音频硬件的独占访问,这样可以避免其他程序的音频干扰。
    • 音乐播放器或系统音频应用程序:可能会使用 DSSCL_NORMALDSSCL_PRIORITY 协作级别来获得相对较低的优先级访问,以便可以同时播放音乐而不打断系统或其他应用程序的声音。

示例:

// 例如,设置协作级别为优先级级别
HRESULT hr = DirectSound->SetCooperativeLevel(hwnd, DSSCL_PRIORITY);
if (FAILED(hr)) {// 错误处理printf("Failed to set cooperative level\n");
}

在这里插入图片描述

调用创建缓冲区

CreateSoundBuffer 是 DirectSound 中用于创建音频缓冲区的方法,用于分配一个声音缓冲区,音频数据可以通过这个缓冲区进行播放。缓冲区可以是一个 主缓冲区(primary buffer)或者 次缓冲区(secondary buffer)。主缓冲区用于控制音频播放的总体设置,而次缓冲区用于存储音频数据并进行实际播放。
以下是 CreateSoundBuffer 的基本用法和参数说明。

函数原型

HRESULT CreateSoundBuffer(LPCDSBUFFERDESC pcDSBufferDesc,    // 指向缓冲区描述结构体的指针LPDIRECTSOUNDBUFFER *ppDSBuffer,  // 指向创建的缓冲区对象的指针LPUNKNOWN pUnkOuter               // 外部未知接口,通常传入 NULL
);

在这里插入图片描述

  • pcDSBufferDesc:指向一个 DSBUFFERDESC 结构的指针,这个结构定义了缓冲区的属性,比如缓冲区的大小、格式、是否循环播放等。
    DSBUFFERDESC 结构体的两个参数需要设置
    初始化 DSBUFFERDESC:需要为缓冲区描述符结构体中的各个字段进行初始化,特别是 dwFlags 和 dwSize。
typedef struct _DSBUFFERDESC {DWORD           dwSize;             // 结构的大小DWORD           dwFlags;            // 缓冲区的标志,例如 DSBCAPS_PRIMARYBUFFER, DSBCAPS_STATICDWORD           dwBufferBytes;      // 缓冲区的大小(字节数)DWORD           dwReserved;         // 保留LPWAVEFORMATEX  lpwfxFormat;        // 音频数据的格式(例如 WAVEFORMATEX 结构)
} DSBUFFERDESC;

dwFlags常见标志

  • DSBCAPS_PRIMARYBUFFER:如果是主缓冲区,必须设置此标志。
  • DSBCAPS_STATIC:缓冲区的数据在初始化后不可更改。适用于需要静态音频数据的场景。
  • DSBCAPS_CTRLVOLUME:允许控制音量。
  • DSBCAPS_CTRLPAN:允许控制声音的左右平衡。
  • DSBCAPS_CTRLFREQUENCY:允许控制播放频率。

在这里插入图片描述

缓冲区创建成功之后要设置对应的音频格式

在这里插入图片描述

这是 WAVEFORMATEX 结构体的定义,它用于描述音频数据的格式,通常在 Windows 中用于音频播放和录制。以下是结构体成员的详细注释:

typedef struct tWAVEFORMATEX
{WORD        wFormatTag;         // 格式类型,指定音频格式。例如,WAVE_FORMAT_PCM 表示 PCM 格式。WORD        nChannels;          // 声道数(例如,1 表示单声道,2 表示立体声)DWORD       nSamplesPerSec;     // 采样率,音频每秒钟采样的次数。例如,44100 表示 44.1 kHz 采样率。DWORD       nAvgBytesPerSec;    // 平均每秒字节数,用于缓冲区估算。等于采样率 * 每个样本的字节数。WORD        nBlockAlign;        // 数据块大小。每个音频帧的字节数。通常等于:nChannels * (wBitsPerSample / 8)。WORD        wBitsPerSample;     // 每个样本的位数。例如,16 表示每个样本 16 位深。WORD        cbSize;             // 额外信息的大小(字节数)。通常为 0,除非格式为扩展类型(如 WAVE_FORMAT_EXTENSIBLE)。
}
WAVEFORMATEX, *PWAVEFORMATEX, NEAR *NPWAVEFORMATEX, FAR *LPWAVEFORMATEX;

示例代码

下面是一个简单的例子,演示如何使用 CreateSoundBuffer 创建一个次缓冲区并填充音频数据:

#include <dsound.h>// 定义音频缓冲区描述
DSBUFFERDESC bufferDesc;
ZeroMemory(&bufferDesc, sizeof(DSBUFFERDESC));
bufferDesc.dwSize = sizeof(DSBUFFERDESC);
bufferDesc.dwFlags = DSBCAPS_STATIC; // 这是一个静态缓冲区
bufferDesc.dwBufferBytes = 44100 * 2 * 2; // 1秒的音频数据(假设16位立体声)// 设置音频格式
WAVEFORMATEX wfx;
ZeroMemory(&wfx, sizeof(WAVEFORMATEX));
wfx.wFormatTag = WAVE_FORMAT_PCM;
wfx.nChannels = 2;           // 立体声
wfx.nSamplesPerSec = 44100;  // 44.1kHz 采样率
wfx.wBitsPerSample = 16;     // 16位音频
wfx.nBlockAlign = (wfx.nChannels * wfx.wBitsPerSample) / 8;
wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;
bufferDesc.lpwfxFormat = &wfx;// 创建音频缓冲区
LPDIRECTSOUNDBUFFER pDSBuffer = NULL;
HRESULT hr = pDirectSound->CreateSoundBuffer(&bufferDesc, &pDSBuffer, NULL);
if (FAILED(hr)) {// 错误处理printf("Failed to create sound buffer\n");
} else {// 成功创建缓冲区printf("Sound buffer created successfully\n");// 使用音频缓冲区填充数据并播放// pDSBuffer->Play(...);
}

对代码的修改

在这里插入图片描述

在这里插入图片描述

关于缓冲区大小的解释

缓冲区大小设为 48000 * sizeof(int16) * 2 是因为它包含了音频缓冲区所需的字节数,用来存储一秒钟的音频数据。让我们来具体分析这个计算:

  1. 48000(采样率):每秒采集的音频样本数,也叫做采样率。这里的采样率是 48000,表示每秒需要采集 48000 个样本。

  2. sizeof(int16):每个样本的位深是 16 位(或 2 字节),因此 sizeof(int16) 返回 2 字节。

  3. 2(声道数):音频是立体声(双声道),包含左声道和右声道,意味着每次采样会有两个数据点(一个用于左声道,一个用于右声道)。

结合这些因素,缓冲区的大小为:
48000 samples/sec × 2 bytes/sample × 2 channels = 192 , 000 bytes 48000 \, \text{samples/sec} \times 2 \, \text{bytes/sample} \times 2 \, \text{channels} = 192,000 \, \text{bytes} 48000samples/sec×2bytes/sample×2channels=192,000bytes
这表示缓冲区大小 48000 * sizeof(int16) * 2(192,000 字节)可容纳 1 秒钟的立体声音频数据。

// game.cpp : Defines the entry point for the application.
//#include <cstdint>
#include <dsound.h>
#include <stdint.h>
#include <windows.h>
#include <winerror.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;
typedef int32 bool32;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 boolGloblaRunning; // 用于控制程序运行的全局布尔变量,通常用于循环条件
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 的打桩函数,返回值为
// ERROR_DEVICE_NOT_CONNECTED,表示设备未连接
X_INPUT_GET_STATE(XInputGetStateStub) { //return (ERROR_DEVICE_NOT_CONNECTED);
}// 定义一个 XInputSetState 的打桩函数,返回值为
// ERROR_DEVICE_NOT_CONNECTED,表示设备未连接
X_INPUT_SET_STATE(XInputSetStateStub) { //return (ERROR_DEVICE_NOT_CONNECTED);
}// 设置全局变量 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");if (!XInputLibrary) {// 如果无法加载 xinput1_4.dll,则回退到 xinput1_3.dllXInputLibrary = LoadLibrary("xinput1_3.dll");} else {// TODO:Diagnostic}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;}} else {// TODO:Diagnostic}
}#define DIRECT_SOUND_CREATE(name)                                              \HRESULT WINAPI name(LPCGUID pcGuidDevice, LPDIRECTSOUND *ppDS,               \LPUNKNOWN pUnkOuter);
// 定义一个宏,用于声明 DirectSound 创建函数的原型typedef DIRECT_SOUND_CREATE(direct_sound_create);
// 定义一个类型别名 direct_sound_create,代表
// DirectSound 创建函数internal void Win32InitDSound(HWND window, int32 SamplesPerSecond,int32 BufferSize) {// 注意: 加载 dsound.dll 动态链接库HMODULE DSoundLibrary = LoadLibraryA("dsound.dll");if (DSoundLibrary) {// 注意: 获取 DirectSound 创建函数的地址// 通过 GetProcAddress 函数查找 "DirectSoundCreate" 函数在 dsound.dll// 中的地址,并将其转换为 direct_sound_create 类型的函数指针direct_sound_create *DirectSoundCreate =(direct_sound_create *)GetProcAddress(DSoundLibrary,"DirectSoundCreate");// 定义一个指向 IDirectSound 接口的指针,并初始化为 NULLIDirectSound *DirectSound = NULL;if (DirectSoundCreate && SUCCEEDED(DirectSoundCreate(0,// 传入 0 作为设备 GUID,表示使用默认音频设备&DirectSound,// 将创建的 DirectSound 对象的指针存储到// DirectSound 变量中0// 传入 0 作为外部未知接口指针,通常为 NULL))) //{// clang-format offWAVEFORMATEX WaveFormat = {};WaveFormat.wFormatTag = WAVE_FORMAT_PCM; // 设置格式标签为 WAVE_FORMAT_PCM,表示使用未压缩的 PCM 格式WaveFormat.nChannels = 2;          // 设置声道数为 2,表示立体声(两个声道:左声道和右声道)WaveFormat.nSamplesPerSec = SamplesPerSecond; // 采样率 表示每秒钟的样本数,常见值为 44100 或 48000 等WaveFormat.wBitsPerSample = 16;    // 16位音频 设置每个样本的位深为 16 位WaveFormat.nBlockAlign = (WaveFormat.nChannels * WaveFormat.wBitsPerSample) / 8;// 计算数据块对齐大小,公式为:nBlockAlign = nChannels * (wBitsPerSample / 8)// 这里除以 8 是因为每个样本的大小是按字节来计算的,nChannels 是声道数// wBitsPerSample 是每个样本的位数,除以 8 转换为字节WaveFormat.nAvgBytesPerSec =  WaveFormat.nSamplesPerSec * WaveFormat.nBlockAlign;// 计算每秒的平均字节数,公式为:nAvgBytesPerSec = nSamplesPerSec * nBlockAlign// 这表示每秒音频数据流的字节数,它帮助估算缓冲区大小// clang-format on// 函数用于设置 DirectSound 的协作等级if (SUCCEEDED(DirectSound->SetCooperativeLevel(window, DSSCL_PRIORITY))) {// 注意: 创建一个主缓冲区// 使用 DirectSoundCreate 函数创建一个 DirectSound// 对象,并初始化主缓冲区 具体的实现步骤可以根据实际需求补充DSBUFFERDESC BufferDescription = {};BufferDescription.dwSize = sizeof(BufferDescription); // 结构的大小// dwFlags:设置为// DSBCAPS_PRIMARYBUFFER,指定我们要创建的是主缓冲区,而不是次缓冲区。BufferDescription.dwFlags = DSBCAPS_PRIMARYBUFFER;LPDIRECTSOUNDBUFFER PrimaryBuffer = NULL;if (SUCCEEDED(DirectSound->CreateSoundBuffer(&BufferDescription, // 指向缓冲区描述结构体的指针&PrimaryBuffer,     // 指向创建的缓冲区对象的指针NULL                // 外部未知接口,通常传入 NULL))) {if (SUCCEEDED(PrimaryBuffer->SetFormat(&WaveFormat))) {// NOTE:we have finally set the formatOutputDebugString("SetFormat 成功");} else {// NOTE:OutputDebugString("SetFormat 失败");}} else {}} else {}// 注意: 创建第二个缓冲区// 创建次缓冲区来承载音频数据,并在播放时使用// 对象,并初始化主缓冲区 具体的实现步骤可以根据实际需求补充DSBUFFERDESC BufferDescription = {};BufferDescription.dwSize = sizeof(BufferDescription); // 结构的大小// dwFlags:设置为// DSBCAPS_GETCURRENTPOSITION2 |// DSBCAPS_GLOBALFOCUS两个标志会使次缓冲区在播放时更加精确,同时在应用失去焦点时保持音频输出BufferDescription.dwFlags =DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_GLOBALFOCUS;BufferDescription.dwBufferBytes = BufferSize; // 缓冲区大小BufferDescription.lpwfxFormat = &WaveFormat; // 指向音频格式的指针LPDIRECTSOUNDBUFFER SecondBuffer = NULL;if (SUCCEEDED(DirectSound->CreateSoundBuffer(&BufferDescription, // 指向缓冲区描述结构体的指针&SecondBuffer,      // 指向创建的缓冲区对象的指针NULL                // 外部未知接口,通常传入 NULL))) {OutputDebugString("SetFormat 成功");} else {OutputDebugString("SetFormat 失败");}// 注意: 开始播放!// 调用相应的 DirectSound API 开始播放音频} else {}} else {}
}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: 处理错误,用重建窗口GloblaRunning = 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);bool32 AltKeyWasDown = (LParam & (1 << 29)); // 检查Alt键是否被按下// bool AltKeyWasDown = ((LParam & (1 << 29)) != 0); //// 检查Alt键是否被按下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) {}}if ((VKCode == VK_F4) && AltKeyWasDown) {GloblaRunning = false;}} break;case WM_CLOSE: { // 窗口关闭时的消息// TODO: 像用户发送消息进行处理GloblaRunning = 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;Win32InitDSound(Window, 48000, 48000 * sizeof(int16) * 2);GloblaRunning = true;while (GloblaRunning) { // 启动一个无限循环,等待和处理消息MSG Message;          // 声明一个 MSG 结构体,用于接收消息while (PeekMessage(&Message,// 指向一个 `MSG` 结构的指针。`PeekMessage`// 将在 `lpMsg` 中填入符合条件的消息内容。0,// `hWnd` 为`NULL`,则检查当前线程中所有窗口的消息;// 如果设置为特定的窗口句柄,则只检查该窗口的消息。0, //0, // 用于设定消息类型的范围PM_REMOVE // 将消息从消息队列中移除,类似于 `GetMessage` 的行为。)) {if (Message.message == WM_QUIT) {GloblaRunning = 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.rhkb.cn/news/469898.html

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

相关文章

计算机的错误计算(一百五十二)

摘要 探讨 MATLAB 中双曲正弦函数 sinh(x)与双曲余弦函数 cosh(x)的计算精度问题。 例1. 已知 计算 以及 直接贴图吧&#xff1a; 另外&#xff0c;16或17位的正确值分别为 0.5168712762709208e1、0.52645599648397069e1、0.2140244217618104e247 与 0.2140244217…

【C++】详解RAII思想与智能指针

&#x1f308; 个人主页&#xff1a;谁在夜里看海. &#x1f525; 个人专栏&#xff1a;《C系列》《Linux系列》 ⛰️ 丢掉幻想&#xff0c;准备斗争 目录 引言 内存泄漏 内存泄漏的危害 内存泄漏的处理 一、RAII思想 二、智能指针 1.auto_ptr 实现原理 模拟实现 弊端…

JDBC-Dao层模式

分层思维 分层思维是软件架构设计的一种重要思想&#xff0c;它通过将应用程序划分为多个相互独立且依赖关系的层。 通常分为以下三层关系。 web层&#xff1a;主要负责与用户进行交互&#xff0c;处理请求。 service层&#xff1a;业务逻辑层&#xff0c;主要负责处理应用程序…

三种单例实现

1、不继承Mono的单例 实现 使用 注&#xff1a; 使用需要继承BaseManager 泛型填写自己本身 需要实现无参构造函数 2、挂载式的Mono单例 实现 使用 注&#xff1a; 使用需要继承SingletonMono 泛型填写自己本身 需要挂载在unity引擎面板 3、不用挂载式的单例 实现 使…

Flink API 的层次结构

Apache Flink 提供了多层 API&#xff0c;每层 API 针对不同的抽象层次和用途&#xff0c;使得开发者可以根据具体需求选择合适的 API 层次。以下是 Flink API 的层次结构及其简要说明&#xff1a;

DHCP与DNS安全管理

一、DHCPSnooping的攻击防范功能配置&#xff08;路由器&#xff09; 1.启动设备 2.将pc设为DHCP获取IP地址 3.配置DHCP [AR1]dhcp enable //启动DHCP服务 [AR1]ip pool aaa //设置地址池 [AR1-ip-pool-aaa]network 192.168.10.0 mask 24 //设置地址范围 [AR1-ip-poo…

51c视觉~合集6

我自己的原文哦~ https://blog.51cto.com/whaosoft/11603901 #CSWin-UNet 将自注意力机制集成到UNet中&#xff01;CSWin-UNet&#xff1a;U型分割方法&#xff0c;显著提高计算效率和感受野交互&#xff01;本文提出了CSWin-UNet&#xff0c;这是一种新颖的U型分割方法&…

深度学习服务器租赁AutoDL

省钱绝招 #AutoDL #GPU #租显卡

设备接入到NVR管理平台EasyNVR多品牌NVR管理工具/设备的音视频配置参考

NVR管理平台EasyNVR是一款功能强大的安防视频监控平台&#xff0c;能够轻松实现视频流的导入、录像、存储和回放等功能。在将设备接入到海康NVR管理平台EasyNVR时&#xff0c;视音频配置是确保视频监控效果的重要步骤。本文将详细介绍如何将设备接入到EasyNVR平台&#xff0c;并…

35.3K+ Star!PhotoPrism:一款基于AI的开源照片管理工具

PhotoPrism 简介 PhotoPrism[1] 是一个为去中心化网络设计的AI照片应用,它利用最新技术自动标记和查找图片,实现自动图像分类与本地化部署,你可以在家中、私有服务器或云端运行它。 项目特点 主要特点 浏览所有照片和视频,无需担心RAW转换、重复项或视频格式。 使用强大的…

HTML之列表

练习题&#xff1a; 图所示为一个问卷调查网页&#xff0c;请制作出来。要求&#xff1a;大标题用h1标签&#xff1b;小题目用h3标签&#xff1b;前两个问题使用有序列表&#xff1b;最后一个问题使用无序列表。 代码&#xff1a; <!DOCTYPE html> <html> <he…

redis实现消息队列的几种方式

一、了解 众所周知&#xff0c;redis是我们日常开发过程中使用最多的非关系型数据库&#xff0c;也是消息中间件。实际上除了常用的rabbitmq、rocketmq、kafka消息队列&#xff08;大家自己下去研究吧~模式都是通用的&#xff09;&#xff0c;我们也能使用redis实现消息队列。…

Linux下MySQL的简单使用

Linux下MySQL的简单使用 导语MySQL安装与配置MySQL安装密码设置 MySQL管理命令myisamchkmysql其他 常见操作 C语言访问MYSQL连接例程错误处理使用SQL 总结参考文献 导语 这一章是MySQL的使用&#xff0c;一些常用的MySQL语句属于本科阶段内容&#xff0c;然后是C语言和MySQl之…

即插即用篇 | YOLOv8 引入 代理注意力 AgentAttention

Transformer模型中的注意力模块是其核心组成部分。虽然全局注意力机制具有很强的表达能力,但其高昂的计算成本限制了在各种场景中的应用。本文提出了一种新的注意力范式,称为“代理注意力”(Agent Attention),以在计算效率和表示能力之间取得平衡。代理注意力使用四元组(Q…

从0开始学PHP面向对象内容之(常用魔术方法续一)

常用魔术方法&#xff08;续&#xff09; 上期我们讲到几个常用的魔术方法&#xff0c;但是由于篇幅过程且全是文字性质地东西&#xff0c;就没写完&#xff0c;篇幅太长也会丧失阅读兴趣&#xff0c;我尽量控制一篇文章在5000字左右 一、__isset()&&__unset() 1、在…

【MySQL】数据库知识突破:数据类型全解析与详解

前言&#xff1a;本节内容讲述MySQL的数据类型&#xff0c; 我们在学习之前的建表的时候已经用过各种各样的数据类型。 比如int、varchar、char类型等等。其中它们是对表的结构的操作&#xff0c; 并没有对数据的内容进行操作&#xff0c;所以它叫做DDL。另外&#xff0c;还有…

windows 11编译安装ffmpeg(包含ffplay)

一、源码及安装包下载 1.1&#xff0c;ffmpeg源码包下载 下载地址&#xff1a;Download FFmpeg 1.2&#xff0c;mysys下载 下载地址&#xff1a;MSYS2 1.3&#xff0c;libx264源码包下载 下载地址&#xff1a;x264, the best H.264/AVC encoder - VideoLAN 二、软件安装 2.1&…

从0开始深度学习(28)——序列模型

序列模型是指一类特别设计来处理序列数据的神经网络模型。序列数据指的是数据中的每个元素都有先后顺序&#xff0c;比如时间序列数据&#xff08;股票价格、天气变化等&#xff09;、自然语言文本&#xff08;句子中的单词顺序&#xff09;、语音信号等。 1 统计工具 前面介绍…

【考研数学:高数2】数列极限

目录 前言 一、数列极限的概念 1.常见前n项和 2.等差、等比数列 3.数列的性质 &#xff08;1&#xff09;单调性 &#xff08;2&#xff09;有界性 二、数列极限的定义 三、收敛数列的性质 1.概念 2.例题 四、极限的四则运算 五、海涅定理&#xff08;归结原则&…

计算机网络分析题

网络的布置 根据具体需求布置网络 第二小题、网络的划分 根据路由表作出路由器拓扑图 ARP跨网络寻址 TCP报文段格式概念 网桥的转发表与动作 网络嗅探报文 十六进制化作十进制 嗅探以太网帧首部 除MAC帧以外&#xff0c;其他各层协议数据单元都是源地址在前&#xff0c;目…