游戏引擎学习第17天

发布于:2024-11-23 ⋅ 阅读:(15) ⋅ 点赞:(0)

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

回顾上一天的内容


1. 整体目标

  • 处理键盘输入:将键盘输入的处理逻辑从平台特定的代码中分离出来,放入更独立的函数中以便管理。
  • 优化消息循环:确保消息循环能够有效处理 Windows 消息,同时避免与窗口回调函数绑定过多逻辑。

2. 拆分消息处理逻辑

  • 将消息循环从主程序的 WinMain 中提取出来,放入一个独立函数 Win32ProcessPendingMessages
  • 消息循环中使用 PeekMessage 检查是否有待处理的消息,然后依次处理。

3. 避免直接使用窗口回调处理键盘输入

  • 窗口回调函数:Windows 系统会自动调用窗口回调函数以处理某些消息(例如 WM_PAINT),这些消息可能绕过主消息循环。
  • 键盘输入:不同于绘图消息,键盘消息总是通过标准的消息循环到达,因此无需依赖窗口回调函数处理键盘事件。

4. 消息循环的作用

  • 常规消息处理
    • 在消息循环中使用 PeekMessage 从消息队列中提取消息。
    • 根据需要决定是否将消息派发给窗口处理例程。
  • 直接消息调用
    • 某些情况下,Windows 会直接调用窗口回调函数而不经过消息队列(例如某些系统事件),开发者无法完全控制。

5. 为什么在消息循环中处理键盘输入

  • 消息循环提供了更好的控制:
    • 开发者可以明确选择处理哪些消息。
    • 消息循环位于代码的主控制流中,逻辑清晰,易于维护。
  • 避免了依赖窗口回调函数的复杂性:
    • 窗口回调函数的调用由 Windows 控制,开发者无法完全掌控其调用时机或传递的参数。
    • 消息循环中,键盘消息总是有序到达,这使得处理逻辑更加一致和直观。

6. 功能性与代码设计

  • 功能性设计:代码保持干净整洁,逻辑集中在消息循环中,而非分散在多个回调函数中。
  • 代码控制:通过手动选择处理消息的时机和方式,可以确保代码流畅且可预测。

函数式编程简介

上面是一段关于函数式编程命令式编程的对比以及函数是否具有副作用的讨论。以下是内容的整理和总结:


  1. 函数的两种类型

    • 函数式函数(Functional Function)
      这是没有副作用的函数,其行为接近数学意义上的函数:

      • 输入:仅接受参数作为输入,不涉及全局变量或可修改的内存。
      • 输出:返回计算结果,而不修改程序的任何状态。
      • 特点:调用函数的顺序不影响程序状态,可交换顺序调用,结果一致。

      示例:

      internal int FunctionalFunction(int x, int y) {
          int Result = x + y;
          return Result; // 返回计算结果,不改变外部状态
      }
      
    • 具有副作用的函数(Function with Side Effects)
      这种函数会修改程序的状态或影响外部变量:

      • 输入:可能操作全局变量、指针或引用。
      • 输出:除了返回结果,还可能通过修改外部变量改变程序状态。
      • 特点:调用顺序可能影响结果,复杂性增加。

      示例:

      internal void FunctionWithSideEffects(int X, int Y, foo *Foo) {
          Foo->Result = X + Y; // 修改外部结构体的数据
          if (Foo->Bar.z == 5) {
              // 某些条件下执行额外操作
          }
      }
      
  2. 副作用的定义与影响

    • 副作用:函数调用过程中对外部可变状态的更改。例如修改全局变量、指针内容或文件、数据库等。
    • 影响:副作用会使函数的行为依赖于外部环境,理解和调试程序的难度增大。
  3. 函数式编程的优势

    • 可靠性:由于没有副作用,函数式程序更容易推导、测试和验证。
    • 可理解性:程序员只需关注参数输入和返回值,不必考虑复杂的状态变化。
    • 灵活性:函数式函数的调用顺序可以自由调整,而不会引入不一致。
  4. 编程语言与函数式风格

    • 一些语言(如 MLHaskell)注重函数式编程,以避免副作用。
    • 函数式编程虽然对编程方式有一定限制,但也提供了更高的代码安全性和可维护性。
  5. 总结

    • 在开发中,尽量避免不必要的副作用可以提高程序的可维护性。
    • 在可能的情况下,将函数设计为函数式的(无副作用)通常是有益的。

上面说了什么内容

让我们的程序更具函数式特性的实现方法

上述内容是关于编写更具函数式特性的代码的方法和思考,演讲者分享了自己的编程习惯和目标。以下是内容的核心要点翻译总结:


让程序更具函数式特性的实践方法

  1. 减少对全局状态的依赖

    • 使用参数传递数据,而不是直接依赖全局状态。
  2. 优先按值传递参数

    • 如果可能,尽量按值传递参数,而不是按引用传递,确保函数不会修改原始数据。
  3. 限制函数的访问范围

    • 函数只访问它绝对需要的数据,以便代码更易于理解。
  4. 逐步提高代码的函数式特性

    • 不追求代码完全函数化,而是在不影响效率的前提下逐步优化。
  5. 关注代码的可读性和维护性

    • 通过减少副作用和复杂性来降低潜在错误的风险,同时提高代码的可读性。
  6. 减少未来复杂性的积累

    • 每一个小的优化都能减少未来可能需要处理的复杂问题。

将键盘处理从 Win32MainWindowCallback() 中移出的理由

  1. 程序流的复杂性
    在现有设计中,Win32MainWindowCallback() 需要直接处理键盘消息。如果继续保留这种设计,函数不得不存储键盘操作的结果。这意味着它需要访问输入结构(input structure),但由于 Windows 系统的限制,这种访问无法通过直接传参实现,必须借助全局变量或窗口本地存储。这种设计增加了程序的隐式状态,不符合函数式编程的原则。

  2. 全局变量的缺点
    使用全局变量来存储键盘结果会导致代码依赖隐式状态,其他函数必须知道全局变量的位置和作用域。这不仅降低了代码的可维护性,还增加了调试的复杂性。

  3. 模块化和可控性
    将键盘处理逻辑移出 Win32MainWindowCallback(),并封装到独立的函数中,可以使代码更加模块化。这种方式让键盘处理逻辑变得更容易理解,同时确保可以明确地控制输入和输出。

  4. 可重用性和灵活性
    新的设计允许调用方明确指定键盘消息的处理结果应该写入的位置。任何需要此功能的代码都可以调用这个独立的函数,而无需依赖隐式的全局状态或复杂的上下文设置。

  5. 提高代码的功能性
    虽然整体设计仍不完全符合函数式编程(如仍然依赖消息队列),但通过显式传递输入和输出,代码的功能性和逻辑清晰度得到了提升。这种改进减少了未来程序维护中的潜在错误。



1. 保留键盘状态

  • 问题:
    当前实现中,每帧都会清零控制器的状态。这意味着如果某个按键在前一帧是按下状态,但本帧未收到相关消息(例如 WM_KEYDOWNWM_KEYUP),程序将无法正确反映按键的实际状态。

  • 目标:

    • 确保按键的 “结束状态”EndedDown)在帧与帧之间得以保留。
    • 清除 “半转换计数”HalfTransitionCount),因为该计数只与当前帧有关。

2. 状态复制逻辑

  • 作者计划用旧的键盘控制器状态更新新的键盘控制器状态。
  • 具体步骤:
    1. 遍历每个按键的状态。
    2. 将旧状态的 EndedDown 值复制到新的控制器状态中。
    3. 清零新的 HalfTransitionCount

这使得新状态能够准确反映按键是否被按下,同时重新初始化帧内的过渡计数。


3. 实现思路

  • 逻辑框架:

    1. 定义新的键盘控制器对象,将其初始化为默认状态(例如清零)。
    2. 遍历旧控制器的每个按键,将 EndedDown 状态复制到新对象。
    3. 在输入处理逻辑中,根据接收到的键盘消息动态更新新的控制器状态:
      • 如果按键状态发生改变(例如被按下或释放),增加相应按键的 HalfTransitionCount
  • 代码片段(概念化实现):

       game_controller_input *OldKeyboardController = &OldInput->Controllers[0];
      game_controller_input *NewKeyboardController = &NewInput->Controllers[0];
      // TODO: 我们不能把所有东西都置零,因为上下状态会不正确!!!
      game_controller_input ZeroController = {};
      *NewKeyboardController = ZeroController;
      for (int ButtonIndex = 0;
            ButtonIndex < ArrayCount(NewKeyboardController->Buttons);
            ++ButtonIndex) {
        NewKeyboardController->Buttons[ButtonIndex].EndedDown =
          OldKeyboardController->Buttons[ButtonIndex].EndedDown;
      }
      Win32ProcessPendingMessages(NewKeyboardController);
    

4. 键盘消息处理

  • 在消息处理循环中,对于每个按键事件:

    • 根据消息内容(WM_KEYDOWNWM_KEYUP)设置 ended_down
    • 如果状态发生变化,更新 HalfTransitionCount
  • 示例代码:

} else if (VKCode == VK_DOWN) { // 按下下箭头时,处理下键
Win32ProcessKeyboardMessage(&KeyboardController->Down, IsDown);
}

// 处理单个按键的状态更新
internal void Win32ProcessKeyboardMessage(game_button_state *NewState,
                                          bool32 IsDown) {
  Assert(NewState->EndedDown != IsDown);
  // 更新按钮的状态(是否按下)
  NewState->EndedDown = IsDown; // 将按钮的状态设置为按下(IsDown 为
                                // true)或松开(IsDown 为 false)
  // 增加按键状态变化的计数
  ++NewState->HalfTransitionCount; // 每次按键状态变化时,半次状态转换计数增加 1
}

5. 确保跨帧状态更新正确

  • 通过这种方法,程序能够准确跟踪按键的持续按下状态,同时对每帧的按键变化进行记录。

在这里插入图片描述

这段代码定义了 XInput(Xbox 控制器输入接口)中一些用于处理游戏手柄输入的阈值。它们的目的是为了过滤掉手柄输入中的微小噪声或无效信号,提供更稳定的用户体验。


具体阈值含义

在这里插入图片描述

1. XINPUT_GAMEPAD_LEFT_THUMB_DEADZONEXINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE
  • 定义:

    • 左摇杆和右摇杆的“死区”(Deadzone)值,分别是 78498689
    • 摇杆输入范围是 [-32768, 32767](16 位有符号整数)。
    • 这些阈值表示:当摇杆的输入值(x 或 y 方向的值)小于对应的死区值时,认为摇杆没有被显著移动。
  • 目的:

    1. 摇杆在未被推动时可能会产生轻微的偏移,这是硬件特性造成的。
    2. 为了避免游戏角色“自动移动”或响应微小偏移,将摇杆输入限制在死区内的值直接视为 0。
  • 效果:

    • 用户只有在推动摇杆超出死区后,输入才会被认为有效。
    • 提高了输入的稳定性和用户体验。
2. XINPUT_GAMEPAD_TRIGGER_THRESHOLD
  • 定义:

    • 扳机键(Trigger)的阈值为 30
    • 扳机的输入范围是 [0, 255](8 位无符号整数)。
    • 这个值表示:当扳机的按压力度小于 30 时,视为没有按下。
  • 目的:

    1. 确保微小的触发不会被误认为是用户的输入。
    2. 滤除无意的轻微接触或硬件噪声。
  • 效果:

    • 用户只有在按压扳机的力度超过 30 后,游戏才会认为是有效输入。

应用场景

  • 游戏开发:
    这些阈值可以用来避免“虚假输入”,例如角色移动、射击等操作不符合用户预期。

  • 自定义调整:

    • 如果玩家觉得输入过于灵敏或不灵敏,可以通过调整这些阈值优化手感。
    • 不同游戏可能对灵敏度的要求不同,因此可以为这些值设置可调选项。

代码逻辑举例

if (abs(Gamepad.sThumbLX) > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE ||
    abs(Gamepad.sThumbLY) > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) {
    // 左摇杆移动超出死区,处理移动逻辑
}

if (Gamepad.bLeftTrigger > XINPUT_GAMEPAD_TRIGGER_THRESHOLD) {
    // 左扳机按下力度超过阈值,处理射击逻辑
}

总结

这些阈值是为了处理硬件输入中的噪声,确保只有用户明确的输入才会被捕捉和响应。在实际开发中,可以根据需求调整这些值,以平衡输入的灵敏度和稳定性。

这段代码和描述围绕游戏手柄输入的“死区”(Dead Zone)进行了解释,其中详细阐述了控制器摇杆的容差和如何在代码中处理这些输入数据的不准确性。

主要内容解读

  1. 死区的含义:

    • 死区是指摇杆在中心位置附近的小范围移动被忽略的区域。
    • 由于控制器硬件的精度限制,摇杆在“中立”状态时,读取的数值通常并不是完美的零,而是带有噪声的一个小范围值。
    • 为了避免游戏误判这些噪声为有效输入,开发者会为摇杆设置一个死区,当输入值在死区范围内时,将其视为零
  2. 具体数值的来源:

    • XINPUT_GAMEPAD_LEFT_THUMB_DEADZONEXINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE 定义了左摇杆和右摇杆的死区范围,分别为 78498689
    • 输入值范围是 [-32768, 32767](16 位有符号整数)。
    • 如果摇杆的 x 或 y 轴的值落在 [-7849, 7849][-8689, 8689] 之间,就将其视为没有移动。
  3. 右摇杆死区更大的原因:

    • 右摇杆死区(8689)略大于左摇杆死区(7849),可能是因为右摇杆的功能更倾向于控制视角或准星,对稳定性的要求更高。
  4. 硬件和死区的关系:

    • 描述中提到控制器内部的电子元件(如电位计)并不精确,因此存在噪声。这种硬件限制导致了死区的存在。
    • 游戏开发者需要通过软件逻辑弥补这些硬件上的不完美。

代码实现中的关键点

描述中提到了处理死区的方法:

  1. 判断是否在死区内:
    如果摇杆的输入值落在死区范围内,则将其归零。

    示例代码:

    int deadZone = XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE;
    int x = Gamepad.sThumbLX; // 获取左摇杆 x 轴的值
    int y = Gamepad.sThumbLY; // 获取左摇杆 y 轴的值
    
    if (abs(x) < deadZone) x = 0; // 如果 x 在死区内,设为 0
    if (abs(y) < deadZone) y = 0; // 如果 y 在死区内,设为 0
    
  2. 归一化处理:
    如果输入值超出死区范围,则需要重新映射到 [0, 1] 的范围,便于进一步计算。

    示例代码:

    float normalizeInput(int value, int deadZone) {
        int range = 32767 - deadZone; // 剩余有效输入范围
        if (abs(value) < deadZone) return 0.0f; // 在死区内视为 0
        return (float)(value - (value > 0 ? deadZone : -deadZone)) / range;
    }
    

结论

  • 死区的核心作用: 是为了抵消控制器硬件带来的噪声,提升用户体验。
  • 代码中的处理逻辑: 检测输入值是否落在死区内,如果是,则将其视为零;否则将值重新映射到正常范围。
  • 硬件设计的权衡: 摇杆的精度和成本之间的平衡,使得死区的设置变得必要。游戏开发者通过代码优化用户的操作感。

通过这些处理,游戏可以实现流畅的控制体验,同时避免硬件限制引起的不必要问题。

描述了一段关于处理游戏手柄摇杆输入值的代码逻辑。这段代码的目的是对游戏手柄的摇杆输入值进行去死区(deadzone)处理,并将其归一化为一个[-1.0, 1.0]的浮点数值范围,以便后续游戏逻辑可以方便地使用这些输入值。

以下是对代码的理解和解析:


1. 核心函数 Win32ProcessXinputStickValue

这是一个用来处理摇杆输入的核心函数,它接收两个参数:

  • Value: 当前摇杆的原始输入值,范围是一个 SHORT 类型,值域为[-32768, 32767]。
  • DeadZoneThreshold: 死区阈值,表示在这个阈值范围内的输入值将被视为零输入。
逻辑:
  • 如果 Value 小于 -DeadZoneThreshold,说明摇杆向左或向下偏移,且超出了死区范围,此时将 Value 映射到[-1.0, 0)。
  • 如果 Value 大于 DeadZoneThreshold,说明摇杆向右或向上偏移,且超出了死区范围,此时将 Value 映射到(0, 1.0]。
  • 如果 Value 在死区范围内(即 -DeadZoneThreshold <= Value <= DeadZoneThreshold),直接返回 0

代码实现如下:

internal real32 Win32ProcessXinputStickValue(SHORT Value, SHORT DeadZoneThreshold) {
    real32 Result = 0;
    if (Value < -DeadZoneThreshold) {
        Result = Value / -32768.0f; // 归一化到[-1.0, 0)范围
    } else if (Value > DeadZoneThreshold) {
        Result = Value / 32767.0f;  // 归一化到(0, 1.0]范围
    }
    return Result;
}

2. 在主逻辑中调用

在主代码中:

  • 调用了 Win32ProcessXinputStickValue,对摇杆的水平(X)和垂直(Y)输入值进行了处理。
  • 处理后的值被分别赋值给 NewController->MinX, NewController->MaxX, NewController->EndX 等。
示例代码:
real32 X = Win32ProcessXinputStickValue(
    Pad->sThumbLX, // 摇杆的水平输入
    XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE); // 死区阈值
NewController->MinX = NewController->MaxX = NewController->EndX = X;

real32 Y = Win32ProcessXinputStickValue(
    Pad->sThumbLY, // 摇杆的垂直输入
    XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE); // 死区阈值
NewController->MinY = NewController->MaxY = NewController->EndY = Y;
替代的注释代码:

注释代码中直接使用了手动判断和归一化的逻辑,但其本质与 Win32ProcessXinputStickValue 函数实现的内容是一样的。


3. 为什么要使用死区?

  • 死区(Deadzone) 是为了避免在摇杆未完全归位时,产生细微的抖动影响输入。这个处理可以提高手柄输入的稳定性。

4. 代码的优化点

  • 将重复逻辑提取到函数 Win32ProcessXinputStickValue 中,可以避免重复代码,提高可维护性。
  • 主逻辑中只需要调用一次函数即可,保持代码简洁。

上面的内容讲述了一个游戏代码片段,目的是处理多个控制器的输入,并根据输入调整游戏中的某些参数(如音调频率和颜色偏移)。下面是详细的说明:


1. 循环遍历所有控制器

通过 for 循环遍历输入的所有控制器:

for (int ControllerIndex = 0; ControllerIndex < ArrayCount(Input->Controllers); ++ControllerIndex) {
  • ArrayCount(Input->Controllers) 用于获取控制器数组的大小。
  • 每次循环都会取出一个控制器,并使用指针 Controller 进行操作。

2. 区分模拟和数字输入

判断当前控制器是 模拟输入 还是 数字输入

if (Controller->IsAnalog) {
- 模拟输入处理:
  • 根据控制器的 EndX 值(X轴的输入偏移量)动态调整音调频率:
    GameState->ToneHz = 256 + (int)(128.0f * (Controller->EndX));
    

    例如:EndX 为正值时,音调频率增加;为负值时,音调频率减少。

  • 根据 EndY 值(Y轴的输入偏移量)调整蓝色偏移量:
    GameState->BlueOffset += (int)4.0f * (int)(Controller->EndY);
    

    蓝色偏移量随 Y轴输入成比例变化。

- 数字输入处理:
  • 如果是数字输入,目前只处理按钮事件。例如,可能会有具体的按键逻辑(这里尚未实现)。

3. 处理“Down”按钮

无论是模拟输入还是数字输入,都检查当前控制器是否按下了“Down”按钮:

if (Controller->Down.EndedDown) {
  GameState->GreenOffset += 1;
}
  • 如果按下了“Down”按钮,绿色分量的偏移量会增加 1。

4. 讨论的额外说明

  • 覆盖问题:当前实现会覆盖最后一个控制器的输入,意味着最后一个报告的模拟输入会覆盖音调频率的设置。但目前开发阶段并不关心这个问题。
  • 未来改进方向:将来可能会完善每个控制器输入的独立处理逻辑。

上面描述了一种对游戏控制器和键盘输入的改进思路,以及对应的实现代码。具体内容包括以下几个方面:

1. 问题和思考:

  • 现状反思:
    目前的控制逻辑处理方式在某些情况下不够简洁,尤其是对于模拟输入(如操控杆)和离散输入(如按钮)间的处理,存在优化空间。
  • 优化目标:
    希望以一种更智能的方式来统一处理操控杆和按钮输入,使两者在功能上更具一致性,同时保留其独特特性。
  • 灵感来源:
    在思考过程中意识到,操控杆的平滑模拟值和离散按钮的简单状态,可以通过增加额外的数据(如平均值或过渡计数)更高效地结合。

2. 优化思路:

  • 对操控杆和按钮的重新定义:
    • 将操控杆的运动方向视为“按钮”(上、下、左、右);
    • 为操控杆增加额外的数据(如平均值),便于捕捉模拟输入的平滑特性。
  • 明确输入动作和方向:
    • 方向输入(如移动上、下、左、右)作为“按钮”;
    • 行为输入(如“动作上”或“动作左”)也统一为按钮处理。
  • 过渡计数:
    引入“半过渡计数”来捕捉快速切换动作(如双击)的特性,方便游戏判断快速移动或冲刺。

3. 代码实现:

结构体设计:

game_controller_input 结构体重新定义了操控杆和按钮输入,包括:

  • IsAnalog 表示是否为模拟输入;
  • StickAverageX/Y 用于存储操控杆的平均位置;
  • 按钮包括移动方向(MoveUp、MoveDown 等)和动作按钮(ActionUp、ActionLeft 等),通过数组和结构体联合体来简化管理。
输入处理:
  • 键盘输入:
    使用 Windows 消息机制处理键盘事件,根据按键更新控制器按钮状态。
  • 按键状态变化:
    通过 WasDownIsDown 检测按键状态变化,仅在变化时更新。
  • 按键映射:
    为每个按键(如 W、A、S、D)绑定对应的移动方向或动作按钮。

4. 改进的意义:

  • 一致性:
    将操控杆方向和按钮统一视为“按钮”,简化了输入处理逻辑。
  • 扩展性:
    通过引入平均值和过渡计数,支持更复杂的输入行为(如快速冲刺)。
  • 优化开发效率:
    这种设计可以使后续的功能扩展更直观、更容易。

从代码结构和功能的叙述来看,您的目标似乎是重构键盘和手柄的输入逻辑,使其更简洁并适配新的框架。在这个过程中涉及的关键点如下:

  1. 去掉中间无用的计算:如中提到的 min max 之类的逻辑被完全移除,因为它们已经不再必要。
  2. 直接处理摇杆输入:将摇杆的 X 和 Y 轴的平均值直接作为输入,而无需中间复杂的处理。
  3. 调整按钮的触发逻辑:通过调用一个通用的处理函数,例如 processDigitalButtons,来决定是否触发某个按钮。这种方式统一了输入处理,简化了代码。
  4. 清理和重用代码:将一些通用的初始化和清理代码提取出来,便于重复使用和维护。

重构的优势:

  1. 清晰的模块化:输入的处理被分成独立的方法,逻辑清晰易懂。
  2. 扩展性:如果需要支持新的输入类型(如触摸屏),可以轻松添加新功能。
  3. 便于调试和维护:减少了重复代码和复杂的条件判断。

如果需要更深入的调整或实现,请告诉我具体细节,例如需要支持的输入类型或平台!

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

其他补充和后面Q&A的补充

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

对死区修改
在这里插入图片描述

在这里插入图片描述

这个函数的目的是对 XInput 摇杆的输入值进行处理和映射,将其从原始范围(-32768 到 32767)转换到一个标准化的范围(-1.0 到 1.0),并考虑摇杆的死区阈值(DeadZoneThreshold)。这种映射可以更好地反映用户实际的摇杆输入,忽略轻微的偏移。


输入参数说明

  1. Value

    • 表示当前摇杆的原始输入值,范围为 -3276832767
    • 正值表示向右或向上,负值表示向左或向下。
  2. DeadZoneThreshold

    • 表示死区的阈值,摇杆输入值在该范围内时被视为无效,避免摇杆微小偏移导致的噪声输入。
    • 通常设置为较小的正整数,例如 8000。

算法工作原理

  1. 死区内的处理

    • Value 的绝对值小于 DeadZoneThreshold 时,返回值保持为 0(不在代码中显式处理,但通过条件逻辑实现)。
  2. 负值处理

    • 如果 Value < -DeadZoneThreshold,说明摇杆已经向左或向下偏移到死区之外。

      • 减去 DeadZoneThreshold,将输入调整为以 DeadZoneThreshold 为起点。
      • 除以 (32768.0f - DeadZoneThreshold),将范围映射到 -1.00.0

      计算公式:
      Result = Value + DeadZoneThreshold 32768.0 − DeadZoneThreshold \text{Result} = \frac{\text{Value} + \text{DeadZoneThreshold}}{32768.0 - \text{DeadZoneThreshold}} Result=32768.0DeadZoneThresholdValue+DeadZoneThreshold

  3. 正值处理

    • 如果 Value > DeadZoneThreshold,说明摇杆已经向右或向上偏移到死区之外。

      • 同样加上 DeadZoneThreshold,将输入调整为以 DeadZoneThreshold 为起点。
      • 除以 (32767.0f - DeadZoneThreshold),将范围映射到 0.01.0

      计算公式:
      Result = Value − DeadZoneThreshold 32767.0 − DeadZoneThreshold \text{Result} = \frac{\text{Value} - \text{DeadZoneThreshold}}{32767.0 - \text{DeadZoneThreshold}} Result=32767.0DeadZoneThresholdValueDeadZoneThreshold

  4. 返回映射值

    • 根据上述逻辑返回处理后的 Result,范围在 [-1.0, 1.0] 之间。

核心逻辑总结

  • 死区忽略:当摇杆值在 [-DeadZoneThreshold, DeadZoneThreshold] 之间时,将其视为无效输入,返回 0。
  • 负值区间映射:将小于 -DeadZoneThreshold 的值压缩到 -1.00.0 的范围。
  • 正值区间映射:将大于 DeadZoneThreshold 的值压缩到 0.01.0 的范围。

优化的效果

  1. 避免死区噪声
    通过死区阈值忽略小范围的无效输入,避免微小的硬件漂移导致的干扰。

  2. 线性映射
    映射函数将摇杆的偏移值以线性方式转换为标准化范围,使得逻辑处理和用户输入感受更一致。


示例计算

假设 DeadZoneThreshold = 8000,摇杆原始值为:

  • 输入值 Value = -16000
    • Result = − 16000 + 8000 32768.0 − 8000 ≈ − 0.31 \text{Result} = \frac{-16000 + 8000}{32768.0 - 8000} \approx -0.31 Result=32768.0800016000+80000.31
  • 输入值 Value = 16000
    • Result = 16000 − 8000 32767.0 − 8000 ≈ 0.31 \text{Result} = \frac{16000 - 8000}{32767.0 - 8000} \approx 0.31 Result=32767.080001600080000.31
  • 输入值 Value = 4000
    • Result = 0 \text{Result} = 0 Result=0(死区内)

注意事项

  • 使用 DeadZoneThreshold 时需要根据硬件调整适当的阈值,避免设定过小导致噪声问题。
  • 代码中的常量 32768.0f32767.0f 是 XInput 的摇杆范围上下界,分别对应负值和正值极限。

手柄 Deadzone 算法 是在处理游戏控制器(例如摇杆、触控板等)输入时常用的技术,旨在消除小幅度误输入对游戏或应用程序的干扰。以下是算法的核心内容和实现目标:


什么是 Deadzone?

  • Deadzone(死区)指的是一个输入值范围。在这个范围内,控制器的输入被视为无效或被忽略。
  • 原因:
    1. 控制器硬件可能在空闲或未操作时输出一些微小的偏移。
    2. 避免微小的摇杆移动导致不必要的游戏行为(例如视角晃动)。

算法目标

  1. 忽略输入值落在死区范围内的噪声。
  2. 对超出死区的值进行调整,使其在 0 到 ±1 的范围内连续分布。
  3. 保持映射的平滑性,确保输入的变化与实际操作一致。

算法过程

  1. 输入检测

    • 首先,检查输入值 Value 是否在正负死区范围内:
      − DeadZoneThreshold ≤ Value ≤ DeadZoneThreshold -\text{DeadZoneThreshold} \leq \text{Value} \leq \text{DeadZoneThreshold} DeadZoneThresholdValueDeadZoneThreshold
      如果在这个范围内,认为是无效输入,结果为 0
  2. 线性映射

    • 如果输入值超出死区:
      • Value > DeadZoneThreshold:按以下公式映射为正值:
        Result = Value − DeadZoneThreshold MaxValue − DeadZoneThreshold \text{Result} = \frac{\text{Value} - \text{DeadZoneThreshold}}{\text{MaxValue} - \text{DeadZoneThreshold}} Result=MaxValueDeadZoneThresholdValueDeadZoneThreshold
      • Value < -DeadZoneThreshold:按以下公式映射为负值:
        Result = Value + DeadZoneThreshold MaxValue − DeadZoneThreshold \text{Result} = \frac{\text{Value} + \text{DeadZoneThreshold}}{\text{MaxValue} - \text{DeadZoneThreshold}} Result=MaxValueDeadZoneThresholdValue+DeadZoneThreshold
  3. 边界处理

    • 如果摇杆移动到极限值(如 32767-32768),则映射结果为 1.0-1.0

实现伪代码

real32 ProcessStickValue(SHORT Value, SHORT DeadZoneThreshold) {
    real32 Result = 0;

    if (Value < -DeadZoneThreshold) {
        Result = (real32)(Value + DeadZoneThreshold) / (32768.0f - DeadZoneThreshold);
    } else if (Value > DeadZoneThreshold) {
        Result = (real32)(Value - DeadZoneThreshold) / (32767.0f - DeadZoneThreshold);
    }

    return Result;
}

优点

  1. 消除了硬件噪声对游戏体验的影响。
  2. 提高了玩家控制的精确度,尤其是在需要微调输入的场景(如射击游戏瞄准)。
  3. 简单高效,可适配各种输入设备。

实际应用

Deadzone 算法广泛用于:

  • 游戏手柄的摇杆和触控板输入。
  • 模拟赛车中的方向盘。
  • 虚拟现实设备中的手势输入。

你可以结合实际需求调整 DeadZoneThreshold 的大小,确保算法能为玩家提供最佳的操作体验。

Circular Deadzone(圆形死区)是一种改进的手柄输入死区算法,与传统的 方形死区 方法相比,它更贴近实际使用场景和游戏体验的需求。下面是其详细介绍:


什么是 Circular Deadzone?

  • Circular Deadzone 中,死区范围是以摇杆的中心为圆心的一个圆,而非传统方法中的正方形区域。
  • 只有当摇杆位置超出这个圆形区域时,输入才会被视为有效。

优点

  1. 更加自然的操作
    • 摇杆通常是圆形运动,圆形死区更符合其物理形状。
    • 避免了方形死区造成的“斜角更敏感”的问题。
  2. 一致性
    • 在任意方向上的灵敏度保持一致,不受对角线或轴线位置的影响。
  3. 广泛适用
    • 适用于需要高精度输入的游戏(如射击游戏和竞速游戏)。

算法核心

  1. 定义死区半径

    • 使用一个常量(DeadZoneRadius)定义圆形死区的半径。
  2. 计算摇杆偏移向量的长度

    • 根据摇杆的 X 和 Y 坐标计算偏移向量的模长:
      r = x 2 + y 2 r = \sqrt{x^2 + y^2} r=x2+y2
      其中 ( r ) 表示摇杆的当前位置到中心的距离。
  3. 判断是否在死区内

    • 如果 r ≤ DeadZoneRadius r \leq \text{DeadZoneRadius} rDeadZoneRadius,则摇杆输入被忽略,视为无效输入。
    • 如果 r > DeadZoneRadius r > \text{DeadZoneRadius} r>DeadZoneRadius,则摇杆输入有效。
  4. 重新映射有效输入

    • 当输入超出死区时,重新计算其有效范围,将摇杆值归一化到 [0, 1]:
      mapped_x = x r ⋅ r − DeadZoneRadius 1 − DeadZoneRadius \text{mapped\_x} = \frac{x}{r} \cdot \frac{r - \text{DeadZoneRadius}}{1 - \text{DeadZoneRadius}} mapped_x=rx1DeadZoneRadiusrDeadZoneRadius
      mapped_y = y r ⋅ r − DeadZoneRadius 1 − DeadZoneRadius \text{mapped\_y} = \frac{y}{r} \cdot \frac{r - \text{DeadZoneRadius}}{1 - \text{DeadZoneRadius}} mapped_y=ry1DeadZoneRadiusrDeadZoneRadius

实现伪代码

struct StickInput {
    float X;
    float Y;
};

StickInput ProcessCircularDeadzone(float InputX, float InputY, float DeadZoneRadius) {
    StickInput Result = {0, 0};

    // 计算摇杆偏移向量长度
    float Length = sqrt(InputX * InputX + InputY * InputY);

    if (Length > DeadZoneRadius) {
        // 将偏移归一化到 [0, 1]
        float Normalized = (Length - DeadZoneRadius) / (1.0f - DeadZoneRadius);
        Result.X = (InputX / Length) * Normalized;
        Result.Y = (InputY / Length) * Normalized;
    }

    return Result;
}

Circular Deadzone 与 Square Deadzone 的对比

特点 Circular Deadzone Square Deadzone
死区形状 圆形 方形
对角线灵敏度 灵敏度一致 较高
实现复杂度 较高(需要计算平方根) 较低
玩家体验 更自然,符合摇杆物理行为 可能导致斜角偏移更敏感

实际应用场景

  1. 第一人称射击游戏
    圆形死区可以更好地控制准星移动,减少无效的漂移。
  2. 赛车类游戏
    在模拟方向盘操作时,圆形死区提供更稳定的转向控制。
  3. 体育游戏
    实现更加精准的球员或球体方向控制。

注意事项

  • 性能:计算平方根会增加处理时间,尤其是较低性能的设备。
  • 适配性:根据不同游戏需求,可以动态调整 DeadZoneRadius 的大小,以平衡精度和用户体验。

通过 Circular Deadzone,可以显著改善玩家的操作感受,使手柄输入更加平滑和精准。

offsetof 的介绍

offsetof 是 C 和 C++ 标准库中的一个宏,用于获取结构体成员相对于结构体起始位置的字节偏移量。它定义在头文件 <stddef.h><cstddef> 中。


语法

#define offsetof(type, member) ((size_t)&(((type *)0)->member))
  • 参数说明:

    1. type:结构体的类型。
    2. member:结构体中成员的名称。
  • 返回值:
    返回的是一个 size_t 类型的值,表示 member 相对于 type 起始地址的偏移量(以字节为单位)。


用途

  1. 内存布局分析
    可用于查看结构体中成员的内存分布。

  2. 序列化与反序列化
    通过偏移量操作内存,访问指定成员。

  3. 动态内存管理
    配合内存池或二进制文件,灵活读取结构体成员。

  4. 简化宏定义
    与容器类型(如链表)结合使用,快速定位结构体的起始地址。


工作原理

通过将结构体指针设置为 0(空指针),然后访问指定成员的地址,计算其相对于结构体起始地址的偏移量。

核心表达式:

&(((type *)0)->member)
  • (type *)0:创建一个指向结构体类型的空指针。
  • ->member:访问空指针上的成员(不会实际解引用,只用于地址计算)。
  • &:获取该成员的地址。
  • 结果即为成员的偏移量。

示例代码

#include <stddef.h>
#include <stdio.h>

// 定义一个结构体
typedef struct {
    int a;
    char b;
    float c;
} MyStruct;

int main() {
    printf("Offset of 'a': %zu\n", offsetof(MyStruct, a)); // 输出 0
    printf("Offset of 'b': %zu\n", offsetof(MyStruct, b)); // 输出 4(因为 int 对齐为 4 字节)
    printf("Offset of 'c': %zu\n", offsetof(MyStruct, c)); // 输出 8(受对齐规则影响)
    return 0;
}

输出:

Offset of 'a': 0
Offset of 'b': 4
Offset of 'c': 8

注意事项

  1. 字节对齐:
    偏移量可能受系统的字节对齐规则影响,结果因平台和编译器选项不同而有所变化。

  2. 未定义行为:
    如果使用了未标准化的语法(例如非常规的类型强制转换),可能会导致未定义行为。

  3. 只能用于 POD 类型:
    在 C++ 中,offsetof 只能用于标准布局类型(POD,Plain Old Data)。对于包含虚函数或继承的复杂类型,使用 offsetof 会引发编译错误。


实际应用场景

1. 内存管理
typedef struct {
    void *next;
    int data;
} Node;

void *get_node_address(void *data_address) {
    return (void *)((char *)data_address - offsetof(Node, data));
}

该代码通过已知成员的地址,反推出整个结构体的起始地址。

2. 动态解析数据结构

在读取二进制文件或网络数据包时,通过 offsetof 定位数据字段,避免写死偏移量。

3. 联合(Union)与嵌套结构

在复杂嵌套结构中,确定内存中的具体偏移量,便于调试和优化。


offsetof 是一个简单而强大的工具,特别适合低级内存操作场景。掌握它对于深入理解结构体的内存布局和高性能编程至关重要。