游戏引擎学习第八天

news2024/11/18 5:38:24

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

理解下面的代码

在这里插入图片描述

关于虚函数

代码分解

  1. 结构体 foo 的定义

    struct foo {
      int32 X;
      int64 Y;
    
      virtual void Bar(int c);
    };
    
    • foo 结构体有两个成员变量:Xint32 类型)和 Yint64 类型)。
    • foo 还定义了一个虚拟函数 Bar(int c)。在 C++ 中,虚拟函数会在类的对象中创建虚拟函数表(vtable)的一部分。
  2. 虚拟函数 Bar 的定义

    void foo::Bar(int c){
      c += this->X;
    }
    
    • 这是 foo 结构体中的虚拟函数 Bar 的实现。在函数中,c 变量的值会增加 this->X 的值。this 指针指向当前对象,因此 this->X 访问的是当前对象的成员变量 X
  3. 创建 foo 类型的对象并调用 Bar 函数

    Foo foo;
    foo.Bar(5);
    
    • 创建一个 foo 类型的对象 foo
    • 调用 foo.Bar(5),这将触发虚函数机制。虚函数 Bar 会通过 foo 对象的虚拟函数表来调用。

虚函数表(vtable)机制

  • 虚函数表(vtable):每个包含虚拟函数的类在运行时都会创建一个虚函数表,虚函数表的每个条目对应类中定义的虚拟函数。类的每个对象会持有一个指向虚函数表的指针(通常被称为 vptr)。

  • vtable 在上面代码中的作用:当你调用 foo.Bar(5) 时,C++ 会查找 foo 对象的虚拟函数表(vtable)。因为 Bar 是一个虚函数,所以它会根据 foo 对象的 vptr 查找 Bar 的地址,并执行相应的函数体。这里,Bar 通过 vtable 被动态查找并执行。

vtable 查找过程

在内存中,编译器会为包含虚拟函数的类分配一个虚函数表(vtable)。每个类的对象会有一个指向其虚函数表的指针 vptr,指向这个类的虚拟函数表。虚函数表的内容是类中每个虚拟函数的地址。

  1. 创建对象时,编译器为 foo 类型的对象 foo 插入一个指针 vptr,该指针指向 foo 类型的 vtable。

  2. 调用 foo.Bar(5),编译器使用对象 foovptr 查找 Bar 函数的地址,并执行 Bar 函数。

this->X 和 vtable 结合

  • this->X 是通过 this 指针访问当前对象的成员变量 X。当 foo.Bar(5) 被调用时,this 指针指向的是当前 foo 类型的对象,因此 this->X 访问的是 foo 对象中的成员 X

总结

  • vtable 是 C++ 用于支持多态的机制。每个包含虚拟函数的类都有一个虚拟函数表,表中存储了虚拟函数的地址。
  • foo 对象有一个指向 vtable 的指针,这个指针使得虚函数能够被动态查找和调用。
  • 当调用 foo.Bar(5) 时,C++ 会通过 foo 对象的 vtable 查找并调用 Bar 函数。

在这里插入图片描述

在这里插入图片描述

写入声音数据通常是将数据放入 GlobalSecondaryBuffer 中

在这里插入图片描述

生成方波数据

要用到的变量

在这里插入图片描述

锁定缓冲区

以下是带有注释的 Lock 函数声明和解释:

STDMETHOD(Lock) (
    THIS_ 
    DWORD dwOffset,                            // 缓冲区偏移量,指定从缓冲区起始位置偏移多少字节开始锁定区域
    DWORD dwBytes,                             // 锁定的字节数,指定要锁定多少字节的数据
    _Outptr_result_bytebuffer_(*pdwAudioBytes1) LPVOID *ppvAudioPtr1,  // 输出参数,返回锁定区域的内存指针,应用程序可以访问此内存区进行读取或写入
    _Out_ LPDWORD pdwAudioBytes1,              // 输出参数,返回第一个锁定区域实际锁定的字节数
    _Outptr_opt_result_bytebuffer_(*pdwAudioBytes2) LPVOID *ppvAudioPtr2,  // 可选的输出参数,返回第二个锁定区域的内存指针(用于双缓冲或环形缓冲区)
    _Out_opt_ LPDWORD pdwAudioBytes2,         // 可选的输出参数,返回第二个锁定区域实际锁定的字节数
    DWORD dwFlags                             // 标志参数,用于指定锁定缓冲区时的行为,如锁定写光标位置、读取光标位置或环形缓冲区等
) PURE;  // 纯虚函数,需要在实际的派生类中实现

在音频缓冲区中,DirectSound 使用一种“环形缓冲区”机制来允许应用程序连续不断地填充音频数据,避免播放的中断。Region1Region2 是 DirectSound 锁定(Lock)时返回的两个数据区域,用于在不同情形下灵活填充音频数据。

环形缓冲区与 Region1 和 Region2 的含义

在 DirectSound 中,环形缓冲区是一个逻辑上连续但物理上循环的缓冲区。我们可以把它想象成一个圆形的数据结构,数据在头尾连接起来,因此称为环形。

当需要写入的区域未跨越缓冲区尾部
  1. Region1 和 Region2 的理解

    • BytesToLockBytesToWrite(需要写入的数据量)都在缓冲区的中间位置,并未跨越缓冲区尾部时,DirectSound 的 Lock 操作仅返回一个写入区域 Region1,并且 Region2 的大小为零。
    • 此时,Region1 包含所有需要写入的数据的空间。

    这种情况相当于只需要在缓冲区中间写入,不需要“环回”。

  2. 代码示例

    • 当需要写入的数据量没有跨越缓冲区的尾部时,Lock 会将 Region1 设置为包含全部写入数据的区域。
当需要写入的区域跨越缓冲区尾部
  1. Region1 和 Region2 的使用

    • BytesToLockBytesToWrite 跨越缓冲区尾部时,DirectSound 将返回两个区域:Region1Region2
    • Region1BytesToLock 开始,到缓冲区末尾结束。
    • Region2 则是缓冲区的开头到所需数据写入完毕的区域,连接着 Region1 的结束位置,形成环形的写入。
  2. 代码意义

    • 通过这种方式,Region1Region2 实际上可以有效地填充到缓冲区的尾部并从头开始填充,确保数据的无缝循环。
    • 这在实时音频流播放中十分重要,因为它可以有效避免音频播放的中断,实现无缝的声音输出。

小结

  • Region1Region2 的分区方式帮助代码处理缓冲区的“尾部到开头”的情况,使得数据填充可以跨越缓冲区的边界,保证连续不断的音频输出。
  • 这样做确保了缓冲区中的音频数据在循环播放时的流畅性,避免音频在缓冲区边界处产生中断和延迟。

在这里插入图片描述

在这里插入图片描述

像返回的Region1 和 Region2 中写入方波数据

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

播放音频

Play 方法用于启动音频缓冲区的播放。它是 DirectSound 接口中的一个方法,允许开发者控制音频缓冲区的播放行为。

STDMETHOD(Play) (THIS_ DWORD dwReserved1, DWORD dwPriority, DWORD dwFlags) PURE;

参数解释

  • dwReserved1:保留参数,通常设置为 0,因为 DirectSound 对该参数未作具体定义。

  • dwPriority:缓冲区播放的优先级。此参数仅在使用声音管理的环境中有意义。若不使用声音管理,一般也设置为 0

  • dwFlags:用于控制播放模式的标志。常用标志有:

    • DSBPLAY_LOOPING:让缓冲区中的音频数据循环播放,即当音频播放到结尾时会自动返回开头重新播放,适合背景音乐或持续播放的音效。
    • DSBPLAY_TERMINATEBY_PRIORITY:此标志用于声音管理模式下,通过优先级中止其他音频缓冲区的播放。未使用声音管理时通常不设置。

使用场景

调用 Play 方法后,缓冲区开始播放指定的音频数据,应用场景包括:

  • 音效播放:用于播放短暂的音效,比如按键声或提示音,通常是非循环播放。
  • 背景音乐:通过 DSBPLAY_LOOPING 标志,让背景音乐循环播放。

代码示例

假设我们要在一个音频缓冲区中播放背景音乐,可以按如下设置:

GlobalSecondaryBuffer->Play(0, 0, DSBPLAY_LOOPING);
  • 解释
    • 0 表示 dwReserved1dwPriority 不使用特定设置。
    • DSBPLAY_LOOPING 让音频循环播放。

修改bug

在这里插入图片描述

// game.cpp : Defines the entry point for the application.
//

#include <cstdint>
#include <dsound.h>
#include <minwindef.h>
#include <processenv.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;
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;
}

// 渲染一个奇异的渐变图案
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;

      // 音频测试
      uint32 RunningSampleIndex = 0; // 样本索引
      int SquareWaveCounter = 0;     // 方波数
      int16 ToneVolume = 3000;       // 音量
      int SamplesPerSecond = 48000;  // 采样率:每秒采样48000次
      int ToneHz = 256;              // 方波频率:256 Hz
      int SquareWavePeriod = SamplesPerSecond / ToneHz; // 方波周期(样本数)
      int HalfSquareWavePeriod = SquareWavePeriod / 2; // 方波半周期(样本数)
      int BytesPerSample = sizeof(int16) * 2;          // 一个样本的大小
      int SecondaryBufferSize = SamplesPerSecond * BytesPerSample; // 缓冲区大小
      bool32 SoundIsPlaying = false;
      Win32InitDSound(Window, SamplesPerSecond, SecondaryBufferSize);
      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 {
          }
        }
        DWORD PlayCursor = 0;  // 播放游标,指示当前播放位置
        DWORD WriteCursor = 0; // 写入游标,指示当前写入位置

        // 获取音频缓冲区的当前播放和写入位置
        if (SUCCEEDED(GlobalSecondaryBuffer->GetCurrentPosition(
                &PlayCursor, &WriteCursor))) {
          // 计算需要锁定的字节位置,基于当前样本索引和每样本字节数
          DWORD BytesToLock =
              RunningSampleIndex * BytesPerSample % SecondaryBufferSize;
          DWORD BytesToWrite = 0; // 需要写入的字节数

          // 判断 BytesToLock 与 PlayCursor 的位置关系以确定写入量
          if (BytesToLock == PlayCursor) {
            // 如果锁定位置正好等于播放位置,写入整个缓冲区
            if (!SoundIsPlaying) {
              BytesToWrite = SecondaryBufferSize;
            }
          } else if (BytesToLock > PlayCursor) {
            // 如果锁定位置在播放位置之后,写入从锁定位置到缓冲区末尾,再加上开头到播放位置的字节数
            BytesToWrite = (SecondaryBufferSize - BytesToLock) + PlayCursor;
          } else {
            // 如果锁定位置在播放位置之前,写入从锁定位置到播放位置之间的字节数
            BytesToWrite = PlayCursor - BytesToLock;
          }

          VOID *Region1; // 第一段区域指针,用于存放锁定后的首部分缓冲区地址
          DWORD Region1Size; // 第一段区域的大小(字节数)
          VOID *Region2; // 第二段区域指针,用于存放锁定后的剩余部分缓冲区地址
          DWORD Region2Size; // 第二段区域的大小(字节数)
          if (SUCCEEDED(GlobalSecondaryBuffer->Lock(
                  BytesToLock, // 缓冲区偏移量,指定开始锁定的字节位置
                  BytesToWrite, // 锁定的字节数,指定要锁定的区域大小
                  &Region1, // 输出,返回锁定区域的内存指针(第一个区域)
                  &Region1Size, // 输出,返回第一个锁定区域的实际字节数
                  &Region2, // 输出,返回第二个锁定区域的内存指针(可选,双缓冲或环形缓冲时使用)
                  &Region2Size, // 输出,返回第二个锁定区域的实际字节数
                  0 // 标志,控制锁定行为(如从光标位置锁定等)
                  ))) {
            // int16 int16 int16
            // 左 右 左 右 左 右 左 右 左 右
            DWORD Region1SampleCount =
                Region1Size / BytesPerSample; // 计算第一段区域中的样本数量
            int16 *SampleOut = (int16 *)Region1; // 将第一段区域指针转换为 16
                                                 // 位整型指针,准备写入样本数据

            if (Region2Size > 48000 && BytesToLock != PlayCursor) {
              OutputDebugStringA("test");
            }

            // 循环写入样本到第一段区域
            for (DWORD SampleIndex = 0; SampleIndex < Region1SampleCount;
                 ++SampleIndex) {
              // 计算每个样本的值,使用方波产生音频数据
              // RunningSampleIndex++ / HalfSquareWavePeriod) % 2
              // 用于生成方波,隔半个周期翻转振幅
              int16 SampleValue =
                  ((RunningSampleIndex++ / HalfSquareWavePeriod) % 2)
                      ? ToneVolume   // 如果为偶数周期,输出正振幅
                      : -ToneVolume; // 如果为奇数周期,输出负振幅
              *SampleOut++ = SampleValue; // 左声道
              *SampleOut++ = SampleValue; // 右声道
            }

            DWORD Region2SampleCount =
                Region2Size / BytesPerSample; // 计算第二段区域中的样本数量
            SampleOut = (int16 *)Region2; // 将第二段区域指针转换为 16
                                          // 位整型指针,准备写入样本数据

            // 循环写入样本到第二段区域
            for (DWORD SampleIndex = 0; SampleIndex < Region2SampleCount;
                 ++SampleIndex) {
              // 使用相同逻辑生成方波样本数据
              int16 SampleValue =
                  ((RunningSampleIndex++ / HalfSquareWavePeriod) % 2)
                      ? ToneVolume        // 偶数周期,输出正振幅
                      : -ToneVolume;      // 奇数周期,输出负振幅
              *SampleOut++ = SampleValue; // 左声道
              *SampleOut++ = SampleValue; // 右声道
            }

            // 解锁音频缓冲区,将数据提交给音频设备
            GlobalSecondaryBuffer->Unlock(Region1, Region1Size, Region2,
                                          Region2Size);
          }
        }
        if (!SoundIsPlaying) {
          GlobalSecondaryBuffer->Play(0, 0, DSBPLAY_LOOPING);
          SoundIsPlaying = true;
        }
        RenderWeirdGradient(GlobalBackbuffer, xOffset, yOffset);
        // 这个地方需要渲染一下不然是黑屏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);
        }
        ++xOffset;
      }
    } else { // 如果窗口创建失败
             // 这里可以处理窗口创建失败的逻辑
             // 比如输出错误信息,或退出程序等
             // TODO:
    }
  } else { // 如果窗口类注册失败
           // 这里可以处理注册失败的逻辑
           // 比如输出错误信息,或退出程序等
           // TODO:
  }

  return 0;
}

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

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

相关文章

Vue2教程002:Vue指令

文章目录 2、Vue指令2.1 开发者工具2.2 v-html2.3 v-show和v-if2.4 v-else和v-else-if2.5 v-on2.5.1 内联语句2.5.2 methods 2、Vue指令 2.1 开发者工具 通过谷歌应用商店安装&#xff08;需要科学上网&#xff09;通过极简插件安装 2.2 v-html Vue会根据不同的指令&#x…

计算机视觉 ---图像模糊

1、图像模糊的作用&#xff1a; 减少噪声&#xff1a; 在图像获取过程中&#xff0c;例如通过相机拍摄或者传感器采集&#xff0c;可能会受到各种因素的干扰&#xff0c;从而引入噪声。这些噪声在图像上表现为一些孤立的、不符合图像主体内容的像素变化&#xff0c;如椒盐噪声&…

[JAVA]MyBatis框架—如何获取SqlSession对象实现数据交互(基础篇)

假设我们要查询数据库的用户信息&#xff0c;在MyBatis框架中&#xff0c;首先需要通过SqlSessionFactory创建SqlSession&#xff0c;然后才能使用SqlSession获取对应的Mapper接口&#xff0c;进而执行查询操作 在前一章我们学习了如何创建MyBatis的配置文件mybatis.config.xm…

node.js下载安装步骤整理

>> 进入node.js下载页面下载 | Node.js 中文网 >>点击 全部安装包 >>删除网址node后面部分&#xff0c;只保留如图所示部分&#xff0c;回车 >>点击进入v11.0.0/版本 >>点击下载node-v11.0.0-win-x64.zip(电脑是windows 64位操作系统适用) >…

解决IntelliJ IDEA的Plugins无法访问Marketplace去下载插件

勾选Auto-detect proxy setting并填入 https://plugins.jetbrains.com 代理URL&#xff0c;可以先做检查连接&#xff1a;

解决Jenkins使用 Git 参数插件拉取 commit 列表缓慢问题

Jenkins使用 Git 参数插件拉取 commit 列表缓慢问题 项目问题问题描述解决方案具体实现 项目问题 在 Jenkins 中使用 Git 参数插件 进行参数化构建&#xff0c;具有多方面的重要性和好处。这不仅提高了构建的灵活性和透明度&#xff0c;还能大大提升开发和运维效率。以下是使用…

Pytest-Bdd-Playwright 系列教程(9):使用 数据表(DataTable 参数) 来传递参数

Pytest-Bdd-Playwright 系列教程&#xff08;9&#xff09;&#xff1a;使用 数据表&#xff08;DataTable 参数&#xff09; 来传递参数 前言一、什么是 datatable 参数&#xff1f;Gherkin 表格示例 二、datatable 参数的基本使用三、完整代码和运行效果完整的测试代码 前言 …

Windows内核编程准备

Windows内核编程 驱动 本课程涉及的驱动&#xff0c;是利用驱动程序的高级权限&#xff0c;去做一些用户层不能做的工作&#xff0c;严格来说&#xff0c;这些写出来的东西不能算是驱动&#xff0c;因为它不会驱动任何设备 权限 内核程序是运行在R0级的&#xff0c;因此比应…

uni-app快速入门(八)--常用内置组件(上)

uni-app提供了一套基础组件&#xff0c;类似HTML里的标签元素&#xff0c;不推荐在uni-app中使用使用div等HTML标签。在uni-app中&#xff0c;对应<div>的标签是view&#xff0c;对应<span>的是text&#xff0c;对应<a>的是navigator&#xff0c;常用uni-app…

【代码大模型】Compressing Pre-trained Models of Code into 3 MB论文阅读

Compressing Pre-trained Models of Code into 3 MB key word: code PLM, compression, GA算法 论文&#xff1a;https://dl.acm.org/doi/pdf/10.1145/3551349.3556964 代码&#xff1a;https://github.com/soarsmu/Compressor.git 【why】 1.问题描述&#xff1a; code LLM …

论文《基于现实迷宫地形的电脑鼠设计》深度分析——智能车驱动算法

论文概述 《基于现实迷宫地形的电脑鼠设计》是由吴润强、庹忠曜、刘文杰、项璟晨、孙科学等人于2023年发表的一篇优秀期刊论文。其针对现阶段电脑鼠计算量庞大且不适用于现实迷宫地形的问题&#xff0c;特基于超声波测距与传统迷宫算法原理&#xff0c;设计出一款可在现实迷宫地…

PG-DERN 解读:少样本学习、 双视角编码器、 关系图学习网络

本文提出了一种用于 分子属性预测 的 少样本学习&#xff08;Few-shot Learning&#xff09; 模型—— PG-DERN&#xff0c;该模型结合了 双视角编码器&#xff08;Dual-view Encoder&#xff09; 和 关系图学习网络&#xff08;Relation Graph Learning Network&#xff09; 双…

w039基于Web足球青训俱乐部管理后台系统开发

&#x1f64a;作者简介&#xff1a;多年一线开发工作经验&#xff0c;原创团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339;赠送计算机毕业设计600个选题excel文…

H3C NX30Pro刷机教程-2024-11-16

H3C NX30Pro刷机教程-2024-11-16 ref: http://www.ttcoder.cn/index.php/2024/11/03/h3c-nx30pro亲测无需分区备份 路由器-新机初始化设置路由器登录密码telnet进入路由器后台 刷机上传uboot到路由器后台在Windows环境下解压后的软件包中打开 tftpd64.exe在NX30Pro环境下通过以…

[2024最新] java八股文实用版(附带原理)---java集合篇

介绍一下常见的list实现类&#xff1f; ArrayList 线程不安全&#xff0c;内部是通过数组实现的&#xff0c;继承了AbstractList&#xff0c;实现了List&#xff0c;适合随机查找和遍历&#xff0c;不适合插入和删除。排列有序&#xff0c;可重复&#xff0c;当容量不够的时候…

python 异步编程之协程

最近在学习python的异步编程&#xff0c;这里就简单记录一下&#xff0c;免得日后忘记。 首先&#xff0c;python异步实现大概有三种方式&#xff0c;多进程&#xff0c;多线程和协程&#xff1b;多线程和多进程就不用多说了&#xff0c;基本上每种语言都会有多进行和多线程的…

20241112-Pycharm使用托管的Anaconda的Jupyter Notebook

Pycharm使用托管的Anaconda的Jupyter Notebook 要求 不要每次使用 Pycharm 运行 Jupyter 文件时都要手动打开 Anaconda 的 Jupyter Notebook 正文 pycharm中配置好会自动安装的&#xff0c;有的要自己配置 Pycharm中配置 文件 ——> 设置 ——> 语言和框架……&am…

Android 无签名系统 debug 版本APK push到设备引起的开机异常问题分析(zygote进程)

问题背景 前置操作&#xff1a; 替换原system/priv-app 目录下已有的应用包未未签名的debug版本&#xff0c;然后重启。 现象&#xff1a; 无法正常开机&#xff0c;卡在开机动画&#xff0c;并且pm没有起来&#xff0c;因为执行adb install 命令是返回“cmd: Cant find se…

【学习心得】数据分析三剑客跟学Gitee仓库

之前&#xff0c;自己在学习数据分析过程中的学习方法和思路&#xff0c;将那些摸索与实践中的心得体会分享出来&#xff0c;能够得到大家的喜欢、点赞我非常高兴&#xff0c;谢谢大家的支持&#xff01;这些正面的反馈对我来说&#xff0c;不仅是莫大的鼓励&#xff0c;更是持…

Vue 批量注册组件实现动态组件技巧

介绍 Vue 动态组件的应用场景很多,可应用于动态页签,动态路由等场景,其核心原理是批量注册。在Vue2和Vue3中实现原理相同,只是语法略有差异。 Vue2 实现 基于 webpack require.context() 是webpack提供的一个自动导入的API 参数1&#xff1a;加载的文件目录 参数2&#xff…