视频参考:https://www.bilibili.com/video/BV1QQUaYMEEz/
改代码的地方尽量一张图说清楚吧,懒得浪费时间
game.h
cpp
#pragma once
#include <cmath>
#include <cstdint>
#include <malloc.h>
#define internal static // 用于定义内翻译单元内部函数
#define local_persist static // 局部静态变量
#define global_variable static // 全局变量
#define Pi32 3.14159265359
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;
typedef float real32;
typedef double real64;
// NOTE: 平台层为游戏提供的服务
// NOTE: 游戏为平台玩家提供的服务
// (这个部分未来可能扩展------例如声音处理可能在单独的线程中)
// 四个主要功能 - 时间管理,控制器/键盘输入,位图缓冲区,声音缓冲区
struct game_offscreen_buffer {
// TODO(casey):未来,渲染将特别变成一个三层抽象!!!
void *Memory;
// 后备缓冲区的宽度和高度
int Width;
int Height;
int Pitch;
int BytesPerPixel;
};
struct game_sound_output_buffer {
int SamplesPerSecond; // 采样率:每秒采样48000次
int SampleCount;
int16 *Samples;
};
// 游戏更新和渲染的主函数
// 参数包含图像缓冲区和音频缓冲区
internal void GameUpdateAndRender(game_offscreen_buffer *Buffer,
game_sound_output_buffer *SoundBuffer);
// 三个主要功能:
// 1. 时间管理(Timing)
// 2. 控制器/键盘输入(Controller/Keyboard Input)
// 3. 位图输出(Bitmap Output)和声音(Sound)
// 使用的缓冲区(Buffer)
game.cpp
cpp
#include "game.h"
internal void GameOutputSound(game_sound_output_buffer *SoundBuffer,
int ToneHz) {
local_persist real32 tSine;
int16 ToneVolume = 3000;
int16 *SampleOut = SoundBuffer->Samples;
int WavePeriod = SoundBuffer->SamplesPerSecond / ToneHz;
// 循环写入样本到第一段区域
for (int SampleIndex = 0; SampleIndex < SoundBuffer->SampleCount;
++SampleIndex) {
real32 SineValue = sinf(tSine);
int16 SampleValue = (int16)(SineValue * ToneVolume);
*SampleOut++ = SampleValue; // 左声道
*SampleOut++ = SampleValue; // 右声道
tSine += 2.0f * (real32)Pi32 * 1.0f / (real32)WavePeriod;
}
}
// 渲染一个奇异的渐变图案
internal void
RenderWeirdGradient(game_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; // 移动到下一行
}
}
internal void GameUpdateAndRender(game_offscreen_buffer *Buffer,
game_sound_output_buffer *SoundBuffer) {
local_persist int BlueOffset = 0;
local_persist int GreenOffset = 0;
local_persist int ToneHz = 256;
GameOutputSound(SoundBuffer, ToneHz);
RenderWeirdGradient(Buffer, BlueOffset, GreenOffset);
}
win32_game.h
cpp
#pragma once
#include "game.h"
#include <dsound.h>
#include <windows.h>
#include <winnt.h>
#include <xinput.h>
// 添加这个去掉重复的冗余代码
struct win32_window_dimension {
int Width;
int Height;
};
struct win32_offscreen_buffer {
BITMAPINFO Info;
void *Memory;
// 后备缓冲区的宽度和高度
int Width;
int Height;
int Pitch;
int BytesPerPixel;
};
struct win32_sound_output {
// 音频测试
uint32 RunningSampleIndex; // 样本索引
int SamplesPerSecond; // 采样率:每秒采样48000次
int BytesPerSample; // 一个样本的大小
int SecondaryBufferSize; // 缓冲区大小
real32 tSine; // 保存当前的相位
int LatencySampleCount;
};
win32_game.cpp
cpp
/**
T这不是最终版本的平台层
1. 存档位置
2. 获取自己可执行文件的句柄
3. 资源加载路径
4. 线程(启动线程)
5. 原始输入(支持多个键盘)
6. Sleep/TimeBeginPeriod
7. ClipCursor()(多显示器支持)
8. 全屏支持
9. WM_SETCURSOR(控制光标可见性)
10. QueryCancelAutoplay
11. WM_ACTIVATEAPP(当我们不是活动应用程序时)
12. Blit速度优化(BitBlt)
13. 硬件加速(OpenGL或Direct3D或两者?)
14. GetKeyboardLayout(支持法语键盘、国际化WASD键支持)
只是一个部分清单
*/
#include "win32_game.h"
#include "game.cpp"
#include "game.h"
// TODO: 全局变量
// 用于控制程序运行的全局布尔变量,通常用于循环条件
global_variable bool GloblaRunning;
// 用于存储屏幕缓冲区的全局变量
global_variable win32_offscreen_buffer GlobalBackbuffer;
global_variable LPDIRECTSOUNDBUFFER GlobalSecondaryBuffer;
/**
* @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; // 指向音频格式的指针
if (SUCCEEDED(DirectSound->CreateSoundBuffer(
&BufferDescription, // 指向缓冲区描述结构体的指针
&GlobalSecondaryBuffer, // 指向创建的缓冲区对象的指针
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;
}
// 这个函数用于重新调整 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; // 返回处理结果
}
internal void Win32ClearBuffer(win32_sound_output *SoundOutput) {
VOID *Region1; // 第一段区域指针,用于存放锁定后的首部分缓冲区地址
DWORD Region1Size; // 第一段区域的大小(字节数)
VOID *Region2; // 第二段区域指针,用于存放锁定后的剩余部分缓冲区地址
DWORD Region2Size; // 第二段区域的大小(字节数)
if (SUCCEEDED(GlobalSecondaryBuffer->Lock(
0, // 缓冲区偏移量,指定开始锁定的字节位置
SoundOutput
->SecondaryBufferSize, // 锁定的字节数,指定要锁定的区域大小
&Region1, // 输出,返回锁定区域的内存指针(第一个区域)
&Region1Size, // 输出,返回第一个锁定区域的实际字节数
&Region2, // 输出,返回第二个锁定区域的内存指针(可选,双缓冲或环形缓冲时使用)
&Region2Size, // 输出,返回第二个锁定区域的实际字节数
0 // 标志,控制锁定行为(如从光标位置锁定等)
))) {
int8 *DestSample = (int8 *)Region1; // 将第一段区域指针转换为 16
// 位整型指针,准备写入样本数据
// 循环写入样本到第一段区域
for (DWORD ByteIndex = 0; ByteIndex < Region1Size; ++ByteIndex) {
*DestSample++ = 0;
}
for (DWORD ByteIndex = 0; ByteIndex < Region2Size; ++ByteIndex) {
*DestSample++ = 0;
}
GlobalSecondaryBuffer->Unlock(Region1, Region1Size, //
Region2, Region2Size);
}
}
internal void Win32FillSoundBuffer(win32_sound_output *SoundOutput,
DWORD ByteToLock, DWORD BytesToWrite,
game_sound_output_buffer *SourceBuffer) {
VOID *Region1; // 第一段区域指针,用于存放锁定后的首部分缓冲区地址
DWORD Region1Size; // 第一段区域的大小(字节数)
VOID *Region2; // 第二段区域指针,用于存放锁定后的剩余部分缓冲区地址
DWORD Region2Size; // 第二段区域的大小(字节数)
if (SUCCEEDED(GlobalSecondaryBuffer->Lock(
ByteToLock, // 缓冲区偏移量,指定开始锁定的字节位置
BytesToWrite, // 锁定的字节数,指定要锁定的区域大小
&Region1, // 输出,返回锁定区域的内存指针(第一个区域)
&Region1Size, // 输出,返回第一个锁定区域的实际字节数
&Region2, // 输出,返回第二个锁定区域的内存指针(可选,双缓冲或环形缓冲时使用)
&Region2Size, // 输出,返回第二个锁定区域的实际字节数
0 // 标志,控制锁定行为(如从光标位置锁定等)
))) {
// int16 int16 int16
// 左 右 左 右 左 右 左 右 左 右
DWORD Region1SampleCount =
Region1Size / SoundOutput->BytesPerSample; // 计算第一段区域中的样本数量
int16 *DestSample = (int16 *)Region1; // 将第一段区域指针转换为 16
// 位整型指针,准备写入样本数据
int16 *SourceSample = SourceBuffer->Samples;
// 循环写入样本到第一段区域
for (DWORD SampleIndex = 0; SampleIndex < Region1SampleCount;
++SampleIndex) {
*DestSample++ = *SourceSample++; // 左声道
*DestSample++ = *SourceSample++; // 右声道
SoundOutput->RunningSampleIndex++;
}
DWORD Region2SampleCount =
Region2Size / SoundOutput->BytesPerSample; // 计算第二段区域中的样本数量
DestSample = (int16 *)Region2; // 将第二段区域指针转换为 16
// 位整型指针,准备写入样本数据
// 循环写入样本到第二段区域
for (DWORD SampleIndex = 0; SampleIndex < Region2SampleCount;
++SampleIndex) {
// 使用相同逻辑生成方波样本数据
*DestSample++ = *SourceSample++; // 左声道
*DestSample++ = *SourceSample++; // 右声道
SoundOutput->RunningSampleIndex++;
}
// 解锁音频缓冲区,将数据提交给音频设备
GlobalSecondaryBuffer->Unlock(Region1, Region1Size, Region2, Region2Size);
}
}
int CALLBACK WinMain(HINSTANCE hInst, HINSTANCE hInstPrev, //
PSTR cmdline, int cmdshow) {
LARGE_INTEGER PerfCountFrequencyResult;
QueryPerformanceFrequency(&PerfCountFrequencyResult);
int64 PerfCountFrequency = PerfCountFrequencyResult.QuadPart;
Win32LoadXInput(); // 加载 XInput 库,用于处理 Xbox 控制器输入
WNDCLASS WindowClass = {}; // 初始化窗口类结构,默认值为零
// 使用大括号初始化,所有成员都被初始化为零(0)或 nullptr
Win32ResizeDIBSection(&GlobalBackbuffer, 1280,
720); // 调整 DIB(设备独立位图)大小
// 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;
win32_sound_output SoundOutput = {}; // 初始化声音输出结构体
// 音频测试
SoundOutput.RunningSampleIndex = 0; // 样本索引
SoundOutput.SamplesPerSecond = 48000; // 采样率:每秒采样48000次
SoundOutput.BytesPerSample = sizeof(int16) * 2; // 一个样本的大小
SoundOutput.SecondaryBufferSize =
SoundOutput.SamplesPerSecond *
SoundOutput.BytesPerSample; // 缓冲区大小
SoundOutput.LatencySampleCount = SoundOutput.SamplesPerSecond / 15;
int16 *Samples = (int16 *)VirtualAlloc(0, 48000 * 2 * sizeof(int16),
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE); //[48000 * 2];
Win32InitDSound(Window, SoundOutput.SamplesPerSecond,
SoundOutput.SecondaryBufferSize); // 初始化 DirectSound
Win32ClearBuffer(&SoundOutput);
bool32 SoundIsPlaying = false;
GloblaRunning = true;
LARGE_INTEGER LastCounter; // 保留上次计数器的值
QueryPerformanceCounter(&LastCounter);
int64 LastCycleCount = __rdtsc();
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);
// 获取摇杆的 X 和 Y 坐标值(-32768 到 32767)
int16 StickX = Pad->sThumbLX;
int16 StickY = Pad->sThumbLY;
// 根据摇杆的 Y 坐标值调整音调和声音
xOffset += StickX >> 12;
yOffset += StickY >> 12;
}
}
DWORD ByteToLock;
DWORD PlayCursor = 0; // 播放游标,指示当前播放位置
DWORD WriteCursor = 0; // 写入游标,指示当前写入位置
DWORD TargetCursor = 0;
bool32 SoundIsValid = false;
DWORD BytesToWrite = 0; // 需要写入的字节数
// 获取音频缓冲区的当前播放和写入位置
if (SUCCEEDED(GlobalSecondaryBuffer->GetCurrentPosition(
&PlayCursor, &WriteCursor))) {
ByteToLock =
((SoundOutput.RunningSampleIndex * SoundOutput.BytesPerSample) %
SoundOutput.SecondaryBufferSize);
TargetCursor = (PlayCursor + (SoundOutput.LatencySampleCount *
SoundOutput.BytesPerSample)) %
SoundOutput.SecondaryBufferSize;
// 判断 ByteToLock 与 TargetCursor 的位置关系以确定写入量
if (ByteToLock == TargetCursor) {
// 如果锁定位置正好等于播放位置,写入整个缓冲区
BytesToWrite = 0;
} else if (ByteToLock > TargetCursor) {
// 如果锁定位置在播放位置之后,写入从锁定位置到缓冲区末尾,再加上开头到播放位置的字节数
BytesToWrite =
(SoundOutput.SecondaryBufferSize - ByteToLock) + TargetCursor;
} else {
// 如果锁定位置在播放位置之前,写入从锁定位置到播放位置之间的字节数
BytesToWrite = TargetCursor - ByteToLock;
}
SoundIsValid = true;
}
if (!SoundIsPlaying) {
GlobalSecondaryBuffer->Play(0, 0, DSBPLAY_LOOPING);
SoundIsPlaying = true;
}
game_sound_output_buffer SoundBuffer = {};
SoundBuffer.SamplesPerSecond = SoundOutput.SamplesPerSecond;
SoundBuffer.SampleCount = BytesToWrite / SoundOutput.BytesPerSample;
SoundBuffer.Samples = Samples;
game_offscreen_buffer Buffer = {};
Buffer.Memory = GlobalBackbuffer.Memory;
Buffer.Width = GlobalBackbuffer.Width;
Buffer.Height = GlobalBackbuffer.Height;
Buffer.Pitch = GlobalBackbuffer.Pitch;
GameUpdateAndRender(&Buffer, &SoundBuffer);
if (SoundIsValid) {
Win32FillSoundBuffer(&SoundOutput, ByteToLock, BytesToWrite,
&SoundBuffer);
// 计算需要锁定的字节位置,基于当前样本索引和每样本字节数
}
// 这个地方需要渲染一下不然是黑屏a
{
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);
}
int64 EndCycleCount = __rdtsc();
LARGE_INTEGER EndCounter;
QueryPerformanceCounter(&EndCounter);
// TODO: 显示结果
int64 CyclesElapsed = EndCycleCount - LastCycleCount;
int64 CounterElapsed = EndCounter.QuadPart - LastCounter.QuadPart;
real32 MillisecondPerFrame =
(real32)((1000.f * (real32)CounterElapsed) /
(real32)PerfCountFrequency);
real32 FPS = (real32)PerfCountFrequency / (real32)CounterElapsed;
real32 MCPF = (real32)CyclesElapsed / (1000.0f * 1000.0f);
#if 0
char Buffer[256];
sprintf_s(Buffer, "%fms/f, %ff/s, %fmc/f\n", MillisecondPerFrame, FPS,
MCPF);
OutputDebugString(Buffer);
#endif
LastCounter = EndCounter;
LastCycleCount = EndCycleCount;
}
} else { // 如果窗口创建失败
// 这里可以处理窗口创建失败的逻辑
// 比如输出错误信息,或退出程序等
// TODO:
}
} else { // 如果窗口类注册失败
// 这里可以处理注册失败的逻辑
// 比如输出错误信息,或退出程序等
// TODO:
}
return 0;
}
上面代码修改完成,接下来是模拟数据输入
判断了 Input.IsAnalog
的值,如果为 true
,则表示输入是模拟信号(比如操控杆、触摸板等),否则为数字信号(如键盘按键、按钮等)。
具体来说:
-
模拟信号 (Analog Input):模拟输入通常表示一个连续的值,例如操控杆的移动可以提供一个范围的数值(例如,从 0 到 1),这允许更精确的控制。这里的 "analog movement tuning" 表示调整模拟输入的运动控制,比如改变运动的灵敏度或者响应方式。
-
数字信号 (Digital Input):数字输入通常是离散的,即只有开/关(例如键盘的按键,鼠标的按钮)。这里的 "digital movement tuning" 可能意味着根据不同的按键状态(按下与否)来调整运动控制的方式。
这段字幕讲解了帧处理与用户输入之间的关系,重点在于帧率对响应速度的影响。以下是关键点的总结:
-
帧的计算与显示
- 每一帧的计算与显示存在时间差。
- 用户实际看到的帧是之前已经计算好的,这意味着输入和响应之间总有一定的延迟。
-
帧率与延迟的关系
- 高帧率可以降低延迟,因为每帧的间隔更短。
- 低帧率会导致更大的输入延迟。例如:
- 在 60 FPS(每秒60帧)的情况下,延迟约为 1/60 秒。
- 在 30 FPS 时,延迟增大到 1/30 秒。
-
输入的滞后性
- 用户的输入(例如操纵杆的移动或按钮的按下)会在下一帧或之后的帧中体现。
- 如果帧率较低,输入响应可能显得更迟钝。
-
极端情况下的效果
- 假设帧率仅为 1 FPS(每秒1帧),用户在这一秒内可能多次按下按钮,但只有在下一秒的帧中才能体现出来。
- 这种情况下,用户体验会非常差。
-
设计决策的重要性
- 开发者需要优化帧率和输入处理的时序,尽量减少延迟以提升用户体验。
- 同时要考虑硬件限制以及平衡处理负载。
这种分析对于游戏开发特别重要,帧率的提高不仅能让画面更流畅,还能直接提升用户输入的响应速度,进而增强交互体验。
关于半过渡 HalfTransitionCount; // 按钮状态变化的次数(用于处理按钮的按下和释放事件)
-
编码按钮状态变化:
- 通过记录按钮从"向上"(未按下)到"向下"(按下)或者从"向下"到"向上"的转换次数,可以有效地编码按钮的操作。
- 一个完整的状态转换(按下再释放)可以被视为两个"半过渡"。
-
优化状态记录:
- 为了减少数据量,只需记录两个信息:
- 按钮的最终状态(当前是否被按下)。
- 整个帧中发生了多少次状态转换。
- 这样可以推断出按钮在帧开始时的状态。
- 为了减少数据量,只需记录两个信息:
-
讨论精度:
- 讨论是否需要区分非常短时间的按压(如1/120秒或1/240秒)与稍长的按压(如1/60秒)。作者认为:
- 玩家无法感知如此精细的时间差别。
- 关注点应放在较长的持续时间上,例如按钮按下持续超过1/8秒或1秒时的变化。
- 讨论是否需要区分非常短时间的按压(如1/120秒或1/240秒)与稍长的按压(如1/60秒)。作者认为:
-
实际应用的重点:
- 更关心按钮在多个帧内的状态(如"被按住")而非短时间的微小差异。
- 这种处理方法更贴近实际应用需求,也简化了数据处理。
关于game_controller_input 中的union
cpp
union {
game_button_state Button[6]; // 按钮状态(最多 6 个按钮)
struct {
game_button_state Up; // 上方向键状态
game_button_state Down; // 下方向键状态
game_button_state Left; // 左方向键状态
game_button_state Right; // 右方向键状态
game_button_state LeftShoulder; // 左肩键状态
game_button_state RightShoulder; // 右肩键状态
};
};
结构体 game_controller_input
中的 union
部分,其目的是为了提供 灵活访问 游戏控制器按钮状态的方式,同时节省内存。以下是具体解释:
union
的作用
union
(联合体)允许多个成员共享同一段内存区域,因此 所有成员共用同一个存储空间。在此结构体中:
cpp
union {
game_button_state Button[6]; // 按钮状态(最多 6 个按钮)
struct {
game_button_state Up; // 上方向键状态
game_button_state Down; // 下方向键状态
game_button_state Left; // 左方向键状态
game_button_state Right; // 右方向键状态
game_button_state LeftShoulder; // 左肩键状态
game_button_state RightShoulder; // 右肩键状态
};
};
Button[6]
:按索引访问所有按钮状态,数组形式通常便于循环处理(如游戏事件轮询)。- 命名成员(
Up
、Down
等):按语义访问具体的按钮状态,这种形式更直观,更适合特定逻辑或按键绑定。
注意 :由于是 union
,Button[6]
和 struct
中的各个成员 共享同一段内存,因此修改其中一个会影响另一个。
为何使用 union
-
节省内存:
union
中的所有成员共用内存,其大小取决于最大成员的大小。- 如果改为直接定义
Button[6]
和独立的命名成员,内存会增加一倍(Button[6]
和struct
各占用一段内存)。
-
灵活访问:
- 当需要按按钮编号遍历状态时,可以使用
Button[6]
。 - 当需要访问具体按键(例如
Up
或LeftShoulder
)时,可以使用命名成员。
- 当需要按按钮编号遍历状态时,可以使用
代码示例
使用 Button[6]
遍历按键状态
cpp
for (int i = 0; i < 6; i++) {
if (controller.Button[i].IsPressed) {
printf("Button %d is pressed\n", i);
}
}
使用命名成员处理特定按键逻辑
cpp
if (controller.Up.IsPressed) {
printf("Move character up\n");
}
if (controller.LeftShoulder.IsPressed) {
printf("Activate special ability\n");
}
总结
Button[6]
的作用:用于按数组索引访问按钮状态,适合循环处理或统一处理逻辑。Up
,Down
,Left
, 等命名成员的作用:提供清晰的按键语义,适合特定按键逻辑。- 使用
union
,既节省了内存,又允许开发者选择最适合场景的访问方式。
遇到的问题
游戏手柄模拟器Gaming Keyboard Splitter