数字化文化馆网站建设,网站建设都有什么栏目,网站建设有掏钱么,中国建设银行网站的发展视频参考:https://www.bilibili.com/video/BV1LPUpYJEXE/
回顾上一天的内容 1. 整体目标#xff1a;
处理键盘输入#xff1a;将键盘输入的处理逻辑从平台特定的代码中分离出来#xff0c;放入更独立的函数中以便管理。优化消息循环#xff1a;确保消息循环能够有效处理 …视频参考:https://www.bilibili.com/video/BV1LPUpYJEXE/
回顾上一天的内容 1. 整体目标
处理键盘输入将键盘输入的处理逻辑从平台特定的代码中分离出来放入更独立的函数中以便管理。优化消息循环确保消息循环能够有效处理 Windows 消息同时避免与窗口回调函数绑定过多逻辑。 2. 拆分消息处理逻辑
将消息循环从主程序的 WinMain 中提取出来放入一个独立函数 Win32ProcessPendingMessages。消息循环中使用 PeekMessage 检查是否有待处理的消息然后依次处理。 3. 避免直接使用窗口回调处理键盘输入
窗口回调函数Windows 系统会自动调用窗口回调函数以处理某些消息例如 WM_PAINT这些消息可能绕过主消息循环。键盘输入不同于绘图消息键盘消息总是通过标准的消息循环到达因此无需依赖窗口回调函数处理键盘事件。 4. 消息循环的作用
常规消息处理 在消息循环中使用 PeekMessage 从消息队列中提取消息。根据需要决定是否将消息派发给窗口处理例程。 直接消息调用 某些情况下Windows 会直接调用窗口回调函数而不经过消息队列例如某些系统事件开发者无法完全控制。 5. 为什么在消息循环中处理键盘输入
消息循环提供了更好的控制 开发者可以明确选择处理哪些消息。消息循环位于代码的主控制流中逻辑清晰易于维护。 避免了依赖窗口回调函数的复杂性 窗口回调函数的调用由 Windows 控制开发者无法完全掌控其调用时机或传递的参数。消息循环中键盘消息总是有序到达这使得处理逻辑更加一致和直观。 6. 功能性与代码设计
功能性设计代码保持干净整洁逻辑集中在消息循环中而非分散在多个回调函数中。代码控制通过手动选择处理消息的时机和方式可以确保代码流畅且可预测。 函数式编程简介
上面是一段关于函数式编程与命令式编程的对比以及函数是否具有副作用的讨论。以下是内容的整理和总结 函数的两种类型 函数式函数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) {// 某些条件下执行额外操作}
}副作用的定义与影响 副作用函数调用过程中对外部可变状态的更改。例如修改全局变量、指针内容或文件、数据库等。影响副作用会使函数的行为依赖于外部环境理解和调试程序的难度增大。 函数式编程的优势 可靠性由于没有副作用函数式程序更容易推导、测试和验证。可理解性程序员只需关注参数输入和返回值不必考虑复杂的状态变化。灵活性函数式函数的调用顺序可以自由调整而不会引入不一致。 编程语言与函数式风格 一些语言如 ML、Haskell注重函数式编程以避免副作用。函数式编程虽然对编程方式有一定限制但也提供了更高的代码安全性和可维护性。 总结 在开发中尽量避免不必要的副作用可以提高程序的可维护性。在可能的情况下将函数设计为函数式的无副作用通常是有益的。 上面说了什么内容
让我们的程序更具函数式特性的实现方法
上述内容是关于编写更具函数式特性的代码的方法和思考演讲者分享了自己的编程习惯和目标。以下是内容的核心要点翻译总结 让程序更具函数式特性的实践方法 减少对全局状态的依赖 使用参数传递数据而不是直接依赖全局状态。 优先按值传递参数 如果可能尽量按值传递参数而不是按引用传递确保函数不会修改原始数据。 限制函数的访问范围 函数只访问它绝对需要的数据以便代码更易于理解。 逐步提高代码的函数式特性 不追求代码完全函数化而是在不影响效率的前提下逐步优化。 关注代码的可读性和维护性 通过减少副作用和复杂性来降低潜在错误的风险同时提高代码的可读性。 减少未来复杂性的积累 每一个小的优化都能减少未来可能需要处理的复杂问题。 将键盘处理从 Win32MainWindowCallback() 中移出的理由 程序流的复杂性 在现有设计中Win32MainWindowCallback() 需要直接处理键盘消息。如果继续保留这种设计函数不得不存储键盘操作的结果。这意味着它需要访问输入结构input structure但由于 Windows 系统的限制这种访问无法通过直接传参实现必须借助全局变量或窗口本地存储。这种设计增加了程序的隐式状态不符合函数式编程的原则。 全局变量的缺点 使用全局变量来存储键盘结果会导致代码依赖隐式状态其他函数必须知道全局变量的位置和作用域。这不仅降低了代码的可维护性还增加了调试的复杂性。 模块化和可控性 将键盘处理逻辑移出 Win32MainWindowCallback()并封装到独立的函数中可以使代码更加模块化。这种方式让键盘处理逻辑变得更容易理解同时确保可以明确地控制输入和输出。 可重用性和灵活性 新的设计允许调用方明确指定键盘消息的处理结果应该写入的位置。任何需要此功能的代码都可以调用这个独立的函数而无需依赖隐式的全局状态或复杂的上下文设置。 提高代码的功能性 虽然整体设计仍不完全符合函数式编程如仍然依赖消息队列但通过显式传递输入和输出代码的功能性和逻辑清晰度得到了提升。这种改进减少了未来程序维护中的潜在错误。 1. 保留键盘状态 问题: 当前实现中每帧都会清零控制器的状态。这意味着如果某个按键在前一帧是按下状态但本帧未收到相关消息例如 WM_KEYDOWN 或 WM_KEYUP程序将无法正确反映按键的实际状态。 目标: 确保按键的 “结束状态”EndedDown在帧与帧之间得以保留。清除 “半转换计数”HalfTransitionCount因为该计数只与当前帧有关。 2. 状态复制逻辑
作者计划用旧的键盘控制器状态更新新的键盘控制器状态。具体步骤: 遍历每个按键的状态。将旧状态的 EndedDown 值复制到新的控制器状态中。清零新的 HalfTransitionCount。
这使得新状态能够准确反映按键是否被按下同时重新初始化帧内的过渡计数。 3. 实现思路 逻辑框架: 定义新的键盘控制器对象将其初始化为默认状态例如清零。遍历旧控制器的每个按键将 EndedDown 状态复制到新对象。在输入处理逻辑中根据接收到的键盘消息动态更新新的控制器状态 如果按键状态发生改变例如被按下或释放增加相应按键的 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_KEYDOWN 或 WM_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. 确保跨帧状态更新正确
通过这种方法程序能够准确跟踪按键的持续按下状态同时对每帧的按键变化进行记录。 这段代码定义了 XInputXbox 控制器输入接口中一些用于处理游戏手柄输入的阈值。它们的目的是为了过滤掉手柄输入中的微小噪声或无效信号提供更稳定的用户体验。 具体阈值含义 1. XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE 和 XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE 定义: 左摇杆和右摇杆的“死区”Deadzone值分别是 7849 和 8689。摇杆输入范围是 [-32768, 32767]16 位有符号整数。这些阈值表示当摇杆的输入值x 或 y 方向的值小于对应的死区值时认为摇杆没有被显著移动。 目的: 摇杆在未被推动时可能会产生轻微的偏移这是硬件特性造成的。为了避免游戏角色“自动移动”或响应微小偏移将摇杆输入限制在死区内的值直接视为 0。 效果: 用户只有在推动摇杆超出死区后输入才会被认为有效。提高了输入的稳定性和用户体验。
2. XINPUT_GAMEPAD_TRIGGER_THRESHOLD 定义: 扳机键Trigger的阈值为 30。扳机的输入范围是 [0, 255]8 位无符号整数。这个值表示当扳机的按压力度小于 30 时视为没有按下。 目的: 确保微小的触发不会被误认为是用户的输入。滤除无意的轻微接触或硬件噪声。 效果: 用户只有在按压扳机的力度超过 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进行了解释其中详细阐述了控制器摇杆的容差和如何在代码中处理这些输入数据的不准确性。
主要内容解读 死区的含义: 死区是指摇杆在中心位置附近的小范围移动被忽略的区域。由于控制器硬件的精度限制摇杆在“中立”状态时读取的数值通常并不是完美的零而是带有噪声的一个小范围值。为了避免游戏误判这些噪声为有效输入开发者会为摇杆设置一个死区当输入值在死区范围内时将其视为零。 具体数值的来源: XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE 和 XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE 定义了左摇杆和右摇杆的死区范围分别为 7849 和 8689。输入值范围是 [-32768, 32767]16 位有符号整数。如果摇杆的 x 或 y 轴的值落在 [-7849, 7849] 或 [-8689, 8689] 之间就将其视为没有移动。 右摇杆死区更大的原因: 右摇杆死区8689略大于左摇杆死区7849可能是因为右摇杆的功能更倾向于控制视角或准星对稳定性的要求更高。 硬件和死区的关系: 描述中提到控制器内部的电子元件如电位计并不精确因此存在噪声。这种硬件限制导致了死区的存在。游戏开发者需要通过软件逻辑弥补这些硬件上的不完美。 代码实现中的关键点
描述中提到了处理死区的方法 判断是否在死区内: 如果摇杆的输入值落在死区范围内则将其归零。 示例代码 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归一化处理: 如果输入值超出死区范围则需要重新映射到 [0, 1] 的范围便于进一步计算。 示例代码 float normalizeInput(int value, int deadZone) {int range 32767 - deadZone; // 剩余有效输入范围if (abs(value) deadZone) return 0.0f; // 在死区内视为 0return (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 消息机制处理键盘事件根据按键更新控制器按钮状态。按键状态变化 通过 WasDown 和 IsDown 检测按键状态变化仅在变化时更新。按键映射 为每个按键如 W、A、S、D绑定对应的移动方向或动作按钮。 4. 改进的意义
一致性 将操控杆方向和按钮统一视为“按钮”简化了输入处理逻辑。扩展性 通过引入平均值和过渡计数支持更复杂的输入行为如快速冲刺。优化开发效率 这种设计可以使后续的功能扩展更直观、更容易。 从代码结构和功能的叙述来看您的目标似乎是重构键盘和手柄的输入逻辑使其更简洁并适配新的框架。在这个过程中涉及的关键点如下
去掉中间无用的计算如中提到的 min max 之类的逻辑被完全移除因为它们已经不再必要。直接处理摇杆输入将摇杆的 X 和 Y 轴的平均值直接作为输入而无需中间复杂的处理。调整按钮的触发逻辑通过调用一个通用的处理函数例如 processDigitalButtons来决定是否触发某个按钮。这种方式统一了输入处理简化了代码。清理和重用代码将一些通用的初始化和清理代码提取出来便于重复使用和维护。
重构的优势
清晰的模块化输入的处理被分成独立的方法逻辑清晰易懂。扩展性如果需要支持新的输入类型如触摸屏可以轻松添加新功能。便于调试和维护减少了重复代码和复杂的条件判断。
如果需要更深入的调整或实现请告诉我具体细节例如需要支持的输入类型或平台 其他补充和后面QA的补充 对死区修改 这个函数的目的是对 XInput 摇杆的输入值进行处理和映射将其从原始范围-32768 到 32767转换到一个标准化的范围-1.0 到 1.0并考虑摇杆的死区阈值DeadZoneThreshold。这种映射可以更好地反映用户实际的摇杆输入忽略轻微的偏移。 输入参数说明 Value 表示当前摇杆的原始输入值范围为 -32768 到 32767。正值表示向右或向上负值表示向左或向下。 DeadZoneThreshold 表示死区的阈值摇杆输入值在该范围内时被视为无效避免摇杆微小偏移导致的噪声输入。通常设置为较小的正整数例如 8000。 算法工作原理 死区内的处理 当 Value 的绝对值小于 DeadZoneThreshold 时返回值保持为 0不在代码中显式处理但通过条件逻辑实现。 负值处理 如果 Value -DeadZoneThreshold说明摇杆已经向左或向下偏移到死区之外。 减去 DeadZoneThreshold将输入调整为以 DeadZoneThreshold 为起点。除以 (32768.0f - DeadZoneThreshold)将范围映射到 -1.0 到 0.0。 计算公式 Result Value DeadZoneThreshold 32768.0 − DeadZoneThreshold \text{Result} \frac{\text{Value} \text{DeadZoneThreshold}}{32768.0 - \text{DeadZoneThreshold}} Result32768.0−DeadZoneThresholdValueDeadZoneThreshold 正值处理 如果 Value DeadZoneThreshold说明摇杆已经向右或向上偏移到死区之外。 同样加上 DeadZoneThreshold将输入调整为以 DeadZoneThreshold 为起点。除以 (32767.0f - DeadZoneThreshold)将范围映射到 0.0 到 1.0。 计算公式 Result Value − DeadZoneThreshold 32767.0 − DeadZoneThreshold \text{Result} \frac{\text{Value} - \text{DeadZoneThreshold}}{32767.0 - \text{DeadZoneThreshold}} Result32767.0−DeadZoneThresholdValue−DeadZoneThreshold 返回映射值 根据上述逻辑返回处理后的 Result范围在 [-1.0, 1.0] 之间。 核心逻辑总结
死区忽略当摇杆值在 [-DeadZoneThreshold, DeadZoneThreshold] 之间时将其视为无效输入返回 0。负值区间映射将小于 -DeadZoneThreshold 的值压缩到 -1.0 到 0.0 的范围。正值区间映射将大于 DeadZoneThreshold 的值压缩到 0.0 到 1.0 的范围。 优化的效果 避免死区噪声 通过死区阈值忽略小范围的无效输入避免微小的硬件漂移导致的干扰。 线性映射 映射函数将摇杆的偏移值以线性方式转换为标准化范围使得逻辑处理和用户输入感受更一致。 示例计算
假设 DeadZoneThreshold 8000摇杆原始值为
输入值 Value -16000 Result − 16000 8000 32768.0 − 8000 ≈ − 0.31 \text{Result} \frac{-16000 8000}{32768.0 - 8000} \approx -0.31 Result32768.0−8000−160008000≈−0.31 输入值 Value 16000 Result 16000 − 8000 32767.0 − 8000 ≈ 0.31 \text{Result} \frac{16000 - 8000}{32767.0 - 8000} \approx 0.31 Result32767.0−800016000−8000≈0.31 输入值 Value 4000 Result 0 \text{Result} 0 Result0死区内 注意事项
使用 DeadZoneThreshold 时需要根据硬件调整适当的阈值避免设定过小导致噪声问题。代码中的常量 32768.0f 和 32767.0f 是 XInput 的摇杆范围上下界分别对应负值和正值极限。
手柄 Deadzone 算法 是在处理游戏控制器例如摇杆、触控板等输入时常用的技术旨在消除小幅度误输入对游戏或应用程序的干扰。以下是算法的核心内容和实现目标 什么是 Deadzone?
Deadzone死区指的是一个输入值范围。在这个范围内控制器的输入被视为无效或被忽略。原因 控制器硬件可能在空闲或未操作时输出一些微小的偏移。避免微小的摇杆移动导致不必要的游戏行为例如视角晃动。 算法目标
忽略输入值落在死区范围内的噪声。对超出死区的值进行调整使其在 0 到 ±1 的范围内连续分布。保持映射的平滑性确保输入的变化与实际操作一致。 算法过程 输入检测 首先检查输入值 Value 是否在正负死区范围内 − DeadZoneThreshold ≤ Value ≤ DeadZoneThreshold -\text{DeadZoneThreshold} \leq \text{Value} \leq \text{DeadZoneThreshold} −DeadZoneThreshold≤Value≤DeadZoneThreshold 如果在这个范围内认为是无效输入结果为 0。 线性映射 如果输入值超出死区 当 Value DeadZoneThreshold按以下公式映射为正值 Result Value − DeadZoneThreshold MaxValue − DeadZoneThreshold \text{Result} \frac{\text{Value} - \text{DeadZoneThreshold}}{\text{MaxValue} - \text{DeadZoneThreshold}} ResultMaxValue−DeadZoneThresholdValue−DeadZoneThreshold当 Value -DeadZoneThreshold按以下公式映射为负值 Result Value DeadZoneThreshold MaxValue − DeadZoneThreshold \text{Result} \frac{\text{Value} \text{DeadZoneThreshold}}{\text{MaxValue} - \text{DeadZoneThreshold}} ResultMaxValue−DeadZoneThresholdValueDeadZoneThreshold 边界处理 如果摇杆移动到极限值如 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;
}优点
消除了硬件噪声对游戏体验的影响。提高了玩家控制的精确度尤其是在需要微调输入的场景如射击游戏瞄准。简单高效可适配各种输入设备。 实际应用
Deadzone 算法广泛用于
游戏手柄的摇杆和触控板输入。模拟赛车中的方向盘。虚拟现实设备中的手势输入。
你可以结合实际需求调整 DeadZoneThreshold 的大小确保算法能为玩家提供最佳的操作体验。
Circular Deadzone圆形死区是一种改进的手柄输入死区算法与传统的 方形死区 方法相比它更贴近实际使用场景和游戏体验的需求。下面是其详细介绍 什么是 Circular Deadzone?
在 Circular Deadzone 中死区范围是以摇杆的中心为圆心的一个圆而非传统方法中的正方形区域。只有当摇杆位置超出这个圆形区域时输入才会被视为有效。 优点
更加自然的操作 摇杆通常是圆形运动圆形死区更符合其物理形状。避免了方形死区造成的“斜角更敏感”的问题。 一致性 在任意方向上的灵敏度保持一致不受对角线或轴线位置的影响。 广泛适用 适用于需要高精度输入的游戏如射击游戏和竞速游戏。 算法核心 定义死区半径 使用一个常量DeadZoneRadius定义圆形死区的半径。 计算摇杆偏移向量的长度 根据摇杆的 X 和 Y 坐标计算偏移向量的模长 r x 2 y 2 r \sqrt{x^2 y^2} rx2y2 其中 ( r ) 表示摇杆的当前位置到中心的距离。 判断是否在死区内 如果 r ≤ DeadZoneRadius r \leq \text{DeadZoneRadius} r≤DeadZoneRadius则摇杆输入被忽略视为无效输入。如果 r DeadZoneRadius r \text{DeadZoneRadius} rDeadZoneRadius则摇杆输入有效。 重新映射有效输入 当输入超出死区时重新计算其有效范围将摇杆值归一化到 [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_xrx⋅1−DeadZoneRadiusr−DeadZoneRadius mapped_y y r ⋅ r − DeadZoneRadius 1 − DeadZoneRadius \text{mapped\_y} \frac{y}{r} \cdot \frac{r - \text{DeadZoneRadius}}{1 - \text{DeadZoneRadius}} mapped_yry⋅1−DeadZoneRadiusr−DeadZoneRadius 实现伪代码
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 DeadzoneSquare Deadzone死区形状圆形方形对角线灵敏度灵敏度一致较高实现复杂度较高需要计算平方根较低玩家体验更自然符合摇杆物理行为可能导致斜角偏移更敏感 实际应用场景
第一人称射击游戏 圆形死区可以更好地控制准星移动减少无效的漂移。赛车类游戏 在模拟方向盘操作时圆形死区提供更稳定的转向控制。体育游戏 实现更加精准的球员或球体方向控制。
注意事项
性能计算平方根会增加处理时间尤其是较低性能的设备。适配性根据不同游戏需求可以动态调整 DeadZoneRadius 的大小以平衡精度和用户体验。
通过 Circular Deadzone可以显著改善玩家的操作感受使手柄输入更加平滑和精准。
offsetof 的介绍
offsetof 是 C 和 C 标准库中的一个宏用于获取结构体成员相对于结构体起始位置的字节偏移量。它定义在头文件 stddef.h 或 cstddef 中。 语法
#define offsetof(type, member) ((size_t)(((type *)0)-member))参数说明: type结构体的类型。member结构体中成员的名称。 返回值: 返回的是一个 size_t 类型的值表示 member 相对于 type 起始地址的偏移量以字节为单位。 用途 内存布局分析 可用于查看结构体中成员的内存分布。 序列化与反序列化 通过偏移量操作内存访问指定成员。 动态内存管理 配合内存池或二进制文件灵活读取结构体成员。 简化宏定义 与容器类型如链表结合使用快速定位结构体的起始地址。 工作原理
通过将结构体指针设置为 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)); // 输出 0printf(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注意事项 字节对齐: 偏移量可能受系统的字节对齐规则影响结果因平台和编译器选项不同而有所变化。 未定义行为: 如果使用了未标准化的语法例如非常规的类型强制转换可能会导致未定义行为。 只能用于 POD 类型: 在 C 中offsetof 只能用于标准布局类型PODPlain 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 是一个简单而强大的工具特别适合低级内存操作场景。掌握它对于深入理解结构体的内存布局和高性能编程至关重要。