视频参考:https://www.bilibili.com/video/BV1QFmhYcE69
ERROR_DEVICE_NOT_CONNECTED
是一个错误代码,通常在调用 XInputGetState
或 XInputSetState
函数时返回,表示指定的设备未连接。通常会出现以下几种情况:
-
未连接控制器:如果尝试访问的设备索引(如 0、1、2、3)没有相应的 XInput 控制器连接,调用
XInputGetState
时会返回ERROR_DEVICE_NOT_CONNECTED
,说明该设备未连接或不可用。 -
无效的玩家索引:XInput 支持的玩家索引范围是 0 到 3,表示最多支持 4 个设备。如果尝试访问的索引超出此范围,也可能导致该错误。
-
设备连接问题:如果控制器没有正确连接到电脑(比如 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.dll
和 xinput1_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.dll
XInputLibrary = 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_SYSKEYDOWN
和 WM_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键是否被按下
这两种写法有一个重要的区别在于运算符的优先级:
-
bool AltKeyWasDown = ((LParam & (1 << 29)) != 0);
- 这个写法使用了外层的括号,确保了(LParam & (1 << 29))
会首先进行计算,然后再判断是否不等于0
。
- 正确结果:先进行位与运算,再进行不等于0
的判断。 -
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
的类型
LParam
是 long
类型,通常在 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 键是否被按下。
问题:
LParam
是 long
类型,通常在 32 位系统中是 32 位。而 LParam & (1 << 29)
会得到一个结果,这个结果仍然是 long
类型,并且该值的大小可能是 long
类型的大小。比较 != 0
本身并不会有问题,因为最终比较的结果是布尔值。
4. 为什么要改成 bool32
和修改代码
修改:
typedef int32 bool32;
bool32 AltKeyWasDown = (LParam & (1 << 29)); // 检查Alt键是否被按下
这个修改实际上简化了 AltKeyWasDown
的存储类型和代码,避免了不必要的比较:
- 不需要
!= 0
:
LParam & (1 << 29)
结果是一个整数,LParam
的按位操作结果已经是非零或者零,直接用bool32
类型来存储这个值,并可以让它显式地表示为布尔值。
- 如果 `(LParam & (1 << 29))` 为零,`AltKeyWasDown` 会被赋值为 `0`。
- 如果 `(LParam & (1 << 29))` 为非零,`AltKeyWasDown` 会被赋值为非零值,通常表示 `true`。
bool32
是一个 32 位整数:
bool32
是int32
类型,这与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_SYSKEYUP
和 WM_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. 如何提高性能
-
减少轮询频率:你可以通过增加轮询控制器的间隔来减少 CPU 占用。例如,每隔几帧轮询一次控制器,而不是每帧都轮询。
-
优化控制器检查:如果你能够预知哪些控制器是插入的,可以将已连接控制器的索引缓存起来,从而避免每次都需要进行连接检查。
-
异步处理:如果需要多个控制器数据并且不希望阻塞主线程,可以考虑使用线程或异步方式来处理
XInputGetState
的调用。
总结:
- 通过
XInputGetState
返回值判断控制器是否连接,如果未连接,则跳过不需要轮询的控制器。 - 优化轮询频率或只处理已连接的控制器,可以有效减少 CPU 占用,避免性能下降。
这样做可以显著提升你的程序性能,避免不必要的 XInputGetState
调用。
关于音频
这段文字描述了音频缓冲区的基本工作原理,以及如何通过 DirectSound 来处理音频输出。主要内容可以理解为:
-
缓冲区的分配与循环播放:描述了音频缓冲区的作用,并指出缓冲区是循环的,这意味着音频数据会不断从缓冲区中读取并播放。当音频播放到缓冲区末尾时,它会回到开头,继续循环播放。
-
缓冲区大小和采样率:音频采样率是 48 kHz,这意味着每秒钟有 48,000 个音频样本。假设缓冲区大小是 2 秒,那么缓冲区中的样本数大约为 96,000 个。每帧画面需要输出 800 个音频样本,这样可以确保音频和视频同步。
-
音频同步问题:提到音频与视频的同步是一个挑战,特别是在游戏中,视频帧与音频播放需要紧密配合。为了避免音频播放过程中出现问题,需要确保音频样本的写入比播放稍微提前一点。
-
缓冲区写入方式:由于直接写入缓冲区会与正在播放的音频产生冲突,因此需要提前写入数据,即写入播放位置之前的部分。这避免了写入过程中出现播放与写入冲突的问题。
-
DirectSound:通过 DirectSound 进行音频播放,音频缓冲区会一直循环,直到程序结束或需要停止播放。
-
画面和音频的同步:在游戏循环中,视频帧和音频需要同时准备好并同步播放。这需要通过合适的缓冲区操作和页面翻转来实现。
下面开始写声音相关代码
DirectSoundCreate
是 DirectSound API 中的一个函数,用于创建一个新的 IDirectSound
对象,这是一个用于音频输出和音效控制的接口。它是 DirectSoundCreate8
函数的前身,主要用于 DirectX 7 及之前的版本。不过,DirectSoundCreate
和 DirectSoundCreate8
都属于音频设备初始化的关键函数,在音频播放、处理和管理过程中都起着至关重要的作用。
函数声明:
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_OUTOFMEMORY
、DSERR_NODRIVER
等),表示函数执行失败,通常是由于设备不可用或内存不足等原因。
功能概述:
DirectSoundCreate
的主要功能是初始化音频设备并返回一个 IDirectSound
接口。该接口用于进一步控制音频设备,如设置设备的合作级别、创建音频缓冲区、播放音频数据等。
使用步骤:
-
创建音频设备对象:
通过调用DirectSoundCreate
创建一个IDirectSound
对象,这个对象用于后续的音频控制。 -
设置合作级别:
使用IDirectSound::SetCooperativeLevel
设置音频设备的合作级别,指定程序与设备的交互方式。例如,可以设置音频播放的优先级(DSSCL_PRIORITY
)或允许后台播放等。 -
创建音频缓冲区:
使用IDirectSound
接口可以创建主缓冲区(Primary Sound Buffer
)和辅助缓冲区(Secondary Sound Buffers
)。这些缓冲区用于存储和播放音频数据。 -
播放音频:
通过填充音频缓冲区并播放它们,可以实现游戏或应用中的音效和背景音乐的播放。
示例代码:
#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
),指定应用程序希望与音频设备的合作方式。协作级别会影响应用程序对音频硬件的访问权限和控制方式,通常有以下几种常见的协作级别:
常见的协作级别
-
DSSCL_NORMAL
:
- 表示应用程序请求正常的协作级别。
- 这是最常用的级别,允许应用程序在系统内运行时独占访问音频硬件。
- 应用程序可以播放音频,同时还允许其他应用程序同时访问音频设备。 -
DSSCL_PRIORITY
:
- 表示应用程序请求优先级较高的协作级别。
- 该级别允许应用程序获得更高的音频资源访问优先级,因此应用程序可能会占用更多的音频资源,其他应用程序对音频的访问会被限制或阻塞。 -
DSSCL_EXCLUSIVE
:
- 表示应用程序请求独占访问音频设备。
- 在此级别下,应用程序将独占音频硬件资源,其他应用程序不能访问音频设备。这通常用于需要高性能音频处理的场景,比如游戏应用,确保音频播放不被其他程序的干扰。 -
DSSCL_WRITEPRIMARY
:
- 在主缓冲区的写入权限下运行应用程序。
使用场景:
SetCooperativeLevel
在应用程序启动时,通常是在初始化 DirectSound 之后调用。它告知 DirectSound 音频设备如何与应用程序交互,特别是如何在多个应用程序之间共享音频资源。- 游戏应用程序:通常会使用
DSSCL_EXCLUSIVE
协作级别来确保音频硬件的独占访问,这样可以避免其他程序的音频干扰。 - 音乐播放器或系统音频应用程序:可能会使用
DSSCL_NORMAL
或DSSCL_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_STATIC
DWORD 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
是因为它包含了音频缓冲区所需的字节数,用来存储一秒钟的音频数据。让我们来具体分析这个计算:
-
48000(采样率):每秒采集的音频样本数,也叫做采样率。这里的采样率是
48000
,表示每秒需要采集 48000 个样本。 -
sizeof(int16):每个样本的位深是 16 位(或 2 字节),因此
sizeof(int16)
返回 2 字节。 -
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 bool
GloblaRunning; // 用于控制程序运行的全局布尔变量,通常用于循环条件
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 的打桩函数,返回值为
// 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.dll
XInputLibrary = 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 接口的指针,并初始化为 NULL
IDirectSound *DirectSound = NULL;
if (DirectSoundCreate && SUCCEEDED(DirectSoundCreate(
0,
// 传入 0 作为设备 GUID,表示使用默认音频设备
&DirectSound,
// 将创建的 DirectSound 对象的指针存储到
// DirectSound 变量中
0
// 传入 0 作为外部未知接口指针,通常为 NULL
))) //
{
// clang-format off
WAVEFORMATEX 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 format
OutputDebugString("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, // 要释放的大小(字节),对部分释放有效,整体释放则设为 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: 处理错误,用重建窗口
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)或 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;
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;
}