游戏引擎学习第13天

news2024/11/18 16:27:47

视频参考:https://www.bilibili.com/video/BV1QQUaYMEEz/
改代码的地方尽量一张图说清楚吧,懒得浪费时间
在这里插入图片描述

game.h

#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

#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

#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


/**
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” 可能意味着根据不同的按键状态(按下与否)来调整运动控制的方式。

这段字幕讲解了帧处理与用户输入之间的关系,重点在于帧率对响应速度的影响。以下是关键点的总结:

  1. 帧的计算与显示

    • 每一帧的计算与显示存在时间差。
    • 用户实际看到的帧是之前已经计算好的,这意味着输入和响应之间总有一定的延迟。
  2. 帧率与延迟的关系

    • 高帧率可以降低延迟,因为每帧的间隔更短。
    • 低帧率会导致更大的输入延迟。例如:
      • 在 60 FPS(每秒60帧)的情况下,延迟约为 1/60 秒。
      • 在 30 FPS 时,延迟增大到 1/30 秒。
  3. 输入的滞后性

    • 用户的输入(例如操纵杆的移动或按钮的按下)会在下一帧或之后的帧中体现。
    • 如果帧率较低,输入响应可能显得更迟钝。
  4. 极端情况下的效果

    • 假设帧率仅为 1 FPS(每秒1帧),用户在这一秒内可能多次按下按钮,但只有在下一秒的帧中才能体现出来。
    • 这种情况下,用户体验会非常差。
  5. 设计决策的重要性

    • 开发者需要优化帧率和输入处理的时序,尽量减少延迟以提升用户体验。
    • 同时要考虑硬件限制以及平衡处理负载。

这种分析对于游戏开发特别重要,帧率的提高不仅能让画面更流畅,还能直接提升用户输入的响应速度,进而增强交互体验。

关于半过渡 HalfTransitionCount; // 按钮状态变化的次数(用于处理按钮的按下和释放事件)

  1. 编码按钮状态变化

    • 通过记录按钮从“向上”(未按下)到“向下”(按下)或者从“向下”到“向上”的转换次数,可以有效地编码按钮的操作。
    • 一个完整的状态转换(按下再释放)可以被视为两个“半过渡”。
  2. 优化状态记录

    • 为了减少数据量,只需记录两个信息:
      • 按钮的最终状态(当前是否被按下)。
      • 整个帧中发生了多少次状态转换。
    • 这样可以推断出按钮在帧开始时的状态。
  3. 讨论精度

    • 讨论是否需要区分非常短时间的按压(如1/120秒或1/240秒)与稍长的按压(如1/60秒)。作者认为:
      • 玩家无法感知如此精细的时间差别。
      • 关注点应放在较长的持续时间上,例如按钮按下持续超过1/8秒或1秒时的变化。
  4. 实际应用的重点

    • 更关心按钮在多个帧内的状态(如“被按住”)而非短时间的微小差异。
    • 这种处理方法更贴近实际应用需求,也简化了数据处理。

在这里插入图片描述

关于game_controller_input 中的union

 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(联合体)允许多个成员共享同一段内存区域,因此 所有成员共用同一个存储空间。在此结构体中:

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]:按索引访问所有按钮状态,数组形式通常便于循环处理(如游戏事件轮询)。
  • 命名成员(UpDown 等):按语义访问具体的按钮状态,这种形式更直观,更适合特定逻辑或按键绑定。

注意:由于是 unionButton[6]struct 中的各个成员 共享同一段内存,因此修改其中一个会影响另一个。


为何使用 union

  1. 节省内存

    • union 中的所有成员共用内存,其大小取决于最大成员的大小。
    • 如果改为直接定义 Button[6] 和独立的命名成员,内存会增加一倍(Button[6]struct 各占用一段内存)。
  2. 灵活访问

    • 当需要按按钮编号遍历状态时,可以使用 Button[6]
    • 当需要访问具体按键(例如 UpLeftShoulder)时,可以使用命名成员。

代码示例

使用 Button[6] 遍历按键状态
for (int i = 0; i < 6; i++) {
    if (controller.Button[i].IsPressed) {
        printf("Button %d is pressed\n", i);
    }
}
使用命名成员处理特定按键逻辑
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
在这里插入图片描述

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

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

相关文章

C++11(五)----lambda表达式

文章目录 lambda表达式 lambda表达式 lambda表达式可以看作一个匿名函数 语法 [capture-list] (parameters) mutable -> return-type { statement } auto func1 [](int a, int b) mutable -> int {return a b; }; *capture-list&#xff1a;捕捉列表。编译器根据[]来 判…

CSS基础知识05(弹性盒子、布局详解,动画,3D转换,calc)

目录 0、弹性盒子、布局 0.1.弹性盒子的基本概念 0.2.弹性盒子的主轴和交叉轴 0.3.弹性盒子的属性 flex-direction row row-reverse column column-reverse flex-wrap nowrap wrap wrap-reverse flex-dirction和flex-wrap的组合简写模式 justify-content flex-s…

【Qt聊天室】客户端实现总结

目录 1. 项目概述 2. 功能实现 2.1 主窗口设计 2.2 功能性窗口 2.3 主界面功能实现 2.4 聊天界面功能实现 2.5 个人信息功能开发 2.6 用户信息界面设置功能 2.7 单聊与群聊 2.8 登录窗口 2.9 消息功能 3. 核心设计逻辑 3.1 核心类 3.2 前后端交互与DataCenter 4…

RK3568平台开发系列讲解(高级字符设备篇)IO 模型引入实验

🚀返回专栏总目录 文章目录 一、IO 的概念二、IO 执行过程三、IO 模型的分类阻塞 IO非阻塞 IOIO 多路复用信号驱动异步 IO沉淀、分享、成长,让自己和他人都能有所收获!😄 一、IO 的概念 IO 是英文 Input 和 Output 的首字母, 代表了输入和输出, 当然这样的描述有一点点…

简单实现QT对象的[json]序列化与反序列化

简单实现QT对象的[json]序列化与反序列化 简介应用场景qt元对象系统思路实现使用方式题外话 简介 众所周知json作为一种轻量级的数据交换格式&#xff0c;在开发中被广泛应用。因此如何方便的将对象数据转为json格式和从json格式中加载数据到对象中就变得尤为重要。 在python类…

【qt】控件2

1.frameGeometry和Geometry区别 frameGeometry是开始从红圈开始算&#xff0c;Geometry从黑圈算 程序证明&#xff1a;使用一个按键&#xff0c;当按键按下,qdebug打印各自左上角的坐标&#xff08;相当于屏幕左上角&#xff09;&#xff0c;以及窗口大小 Widget::Widget(QWid…

LeetCode654.最大二叉树

LeetCode刷题记录 文章目录 &#x1f4dc;题目描述&#x1f4a1;解题思路⌨C代码 &#x1f4dc;题目描述 给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点&#xff0c;其值为 nums 中的最大值。 递归地在最大值 左边 的 子…

华为欧拉系统使用U盘制作引导安装华为欧拉操作系统

今天记录一下通过U盘来安装华为欧拉操作系统 华为欧拉操作系统是国产的一个类似于Centos的Linus系统 具体实现操作步骤&#xff1a; 先在官网下载欧拉系统镜像点击跳转到下载 准备好一个大于16g的U盘 &#xff0c;用于制作U盘启动 下载一个引导程序制作工具&#xff0c;我使用…

软考教材重点内容 信息安全工程师 第 3 章 密码学基本理论

&#xff08;本章相对老版本极大的简化&#xff0c;所有与算法相关的计算全部删除&#xff0c;因此考试需要了解各个常 用算法的基本参数以及考试中可能存在的古典密码算法的计算&#xff0c;典型的例子是 2021 和 2022 年分别考了 DES 算法中的 S 盒计算&#xff0c;RSA 中的已…

如何让手机ip变成动态

在数字化浪潮中&#xff0c;手机已成为我们日常生活中不可或缺的一部分。无论是浏览网页、使用社交媒体还是进行在线购物&#xff0c;手机都扮演着举足轻重的角色。然而&#xff0c;在享受网络带来的便利时&#xff0c;我们也需要关注网络安全和隐私保护。静态IP地址可能让手机…

64位程序调用32位dll解决方案

最近在做64位代码移植&#xff0c;发现很多老代码使用到了第三方的32位dll;而且这些第三方32位dll库已经年代久远&#xff0c;原开发商已不再了&#xff1b;所以急切的需要在64位主程序 中使用老的32位dll;查询很多解决方案 发现目前只有使用com 进程外组件的方法可以解决此问题…

无人机挂载超细干粉灭火装置技术详解

无人机挂载超细干粉灭火装置技术是一种创新的灭火方式&#xff0c;结合了无人机的远程操控能力和超细干粉灭火剂的高效灭火性能。以下是对该技术的详细解析&#xff1a; 一、技术背景与原理 背景&#xff1a;高层建筑灭火救援困难一直是公认的世界性难题。无人机技术的发展为…

信号-3-信号处理

main 信号捕捉的操作 sigaction struct sigaction OS不允许信号处理方法进行嵌套&#xff1a;某一个信号正在被处理时&#xff0c;OS会自动block改信号&#xff0c;之后会自动恢复 同理&#xff0c;sigaction.sa_mask 为捕捉指定信号后临时屏蔽的表 pending什么时候清零&…

Linux的指令(三)

1.grep指令 功能&#xff1a; 在文件中搜索字符串&#xff0c;将找到的行打印出来 -i&#xff1a;忽略大小写的不同&#xff0c;所以大小写视为一样 -n&#xff1a;顺便输出行号 -v:反向选择&#xff0c;就是显示出没有你输入要搜索内容的内容 代码示例&#xff1a; roo…

onvif协议相关:4.1.7 Digest方式云台控制停止

背景 关于onvif的其实很早之前我已经在专栏中写了不少了, 使用onvif协议操作设备 但最近有陆陆续续的粉丝问我, 希望我在写一些关于 onvif的设备自动发现、预置位跳转、云台操作的博客。 满足粉丝的需求,安排。 今天我们来实现 设备云台的控制(启动) 实现 1.在ONVIF Devi…

【机器学习】数学知识:标准差,方差,协方差,平均数,中位数,众数

标准差、方差和协方差是统计学中重要的概念&#xff0c;用于描述数据的分散程度和变量之间的关系。以下是它们的定义和公式&#xff1a; 1. 标准差 (Standard Deviation) 标准差是方差的平方根&#xff0c;表示数据的分散程度&#xff0c;以与数据相同的单位表示。 公式&…

数据结构习题——有效的括号(栈),栈与队列和互相实现,循环队列的实现

文章目录 前言1、有效的括号题目思路代码 2、用队列实现栈题目思路代码 3、用栈实现对列题目思路代码 4、设计循环队列4.1循环队列的概念和了解题目思路代码 总结 前言 继上篇博客学习了栈与队列之后&#xff0c;今天我们来尝试着使用他们来写一些题目&#xff0c;话不多说&…

Java连接MySQL(测试build path功能)

Java连接MySQL&#xff08;测试build path功能&#xff09; 实验说明下载MySQL的驱动jar包连接测试的Java代码 实验说明 要测试该情况&#xff0c;需要先安装好MySQL的环境&#xff0c;其实也可以通过测试最后提示的输出来判断build path是否成功&#xff0c;因为如果不成功会直…

计算机组成原理——高速缓存

标记表示——主存块号和缓存块之前的一一对应关系

Java面试之多线程并发篇(5)

前言 本来想着给自己放松一下&#xff0c;刷刷博客&#xff0c;突然被几道面试题难倒&#xff01;常用的线程池有哪些&#xff1f;简述一下你对线程池的理解&#xff1f;Java程序是如何执行的&#xff1f;锁的优化机制了解吗&#xff1f;说说进程和线程的区别&#xff1f;似乎…