游戏引擎学习第17天
视频参考: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. 确保跨帧状态更新正确
- 通过这种方法,程序能够准确跟踪按键的持续按下状态,同时对每帧的按键变化进行记录。
这段代码定义了 XInput(Xbox 控制器输入接口)中一些用于处理游戏手柄输入的阈值。它们的目的是为了过滤掉手柄输入中的微小噪声或无效信号,提供更稳定的用户体验。
具体阈值含义
1. XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE
和 XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE
-
定义:
- 左摇杆和右摇杆的“死区”(Deadzone)值,分别是
7849
和8689
。 - 摇杆输入范围是 [-32768, 32767](16 位有符号整数)。
- 这些阈值表示:当摇杆的输入值(x 或 y 方向的值)小于对应的死区值时,认为摇杆没有被显著移动。
- 左摇杆和右摇杆的“死区”(Deadzone)值,分别是
-
目的:
- 摇杆在未被推动时可能会产生轻微的偏移,这是硬件特性造成的。
- 为了避免游戏角色“自动移动”或响应微小偏移,将摇杆输入限制在死区内的值直接视为 0。
-
效果:
- 用户只有在推动摇杆超出死区后,输入才会被认为有效。
- 提高了输入的稳定性和用户体验。
2. XINPUT_GAMEPAD_TRIGGER_THRESHOLD
-
定义:
- 扳机键(Trigger)的阈值为
30
。 - 扳机的输入范围是 [0, 255](8 位无符号整数)。
- 这个值表示:当扳机的按压力度小于
30
时,视为没有按下。
- 扳机键(Trigger)的阈值为
-
目的:
- 确保微小的触发不会被误认为是用户的输入。
- 滤除无意的轻微接触或硬件噪声。
-
效果:
- 用户只有在按压扳机的力度超过
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
,来决定是否触发某个按钮。这种方式统一了输入处理,简化了代码。 - 清理和重用代码:将一些通用的初始化和清理代码提取出来,便于重复使用和维护。
重构的优势:
- 清晰的模块化:输入的处理被分成独立的方法,逻辑清晰易懂。
- 扩展性:如果需要支持新的输入类型(如触摸屏),可以轻松添加新功能。
- 便于调试和维护:减少了重复代码和复杂的条件判断。
如果需要更深入的调整或实现,请告诉我具体细节,例如需要支持的输入类型或平台!
其他补充和后面Q&A的补充
对死区修改
这个函数的目的是对 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}} Result=32768.0−DeadZoneThresholdValue+DeadZoneThreshold - 减去
-
-
正值处理
-
如果
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}} Result=32767.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 Result=32768.0−8000−16000+8000≈−0.31
- 输入值
Value = 16000
:- Result = 16000 − 8000 32767.0 − 8000 ≈ 0.31 \text{Result} = \frac{16000 - 8000}{32767.0 - 8000} \approx 0.31 Result=32767.0−800016000−8000≈0.31
- 输入值
Value = 4000
:- Result = 0 \text{Result} = 0 Result=0(死区内)
注意事项
- 使用
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}} Result=MaxValue−DeadZoneThresholdValue−DeadZoneThreshold - 当
Value < -DeadZoneThreshold
:按以下公式映射为负值:
Result = Value + DeadZoneThreshold MaxValue − DeadZoneThreshold \text{Result} = \frac{\text{Value} + \text{DeadZoneThreshold}}{\text{MaxValue} - \text{DeadZoneThreshold}} Result=MaxValue−DeadZoneThresholdValue+DeadZoneThreshold
- 当
- 如果输入值超出死区:
-
边界处理
- 如果摇杆移动到极限值(如
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} r=x2+y2
其中 ( r ) 表示摇杆的当前位置到中心的距离。
- 根据摇杆的 X 和 Y 坐标计算偏移向量的模长:
-
判断是否在死区内
- 如果 r ≤ DeadZoneRadius r \leq \text{DeadZoneRadius} r≤DeadZoneRadius,则摇杆输入被忽略,视为无效输入。
- 如果 r > DeadZoneRadius r > \text{DeadZoneRadius} r>DeadZoneRadius,则摇杆输入有效。
-
重新映射有效输入
- 当输入超出死区时,重新计算其有效范围,将摇杆值归一化到 [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=rx⋅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_y=ry⋅1−DeadZoneRadiusr−DeadZoneRadius
- 当输入超出死区时,重新计算其有效范围,将摇杆值归一化到 [0, 1]:
实现伪代码
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 |
---|---|---|
死区形状 | 圆形 | 方形 |
对角线灵敏度 | 灵敏度一致 | 较高 |
实现复杂度 | 较高(需要计算平方根) | 较低 |
玩家体验 | 更自然,符合摇杆物理行为 | 可能导致斜角偏移更敏感 |
实际应用场景
- 第一人称射击游戏
圆形死区可以更好地控制准星移动,减少无效的漂移。 - 赛车类游戏
在模拟方向盘操作时,圆形死区提供更稳定的转向控制。 - 体育游戏
实现更加精准的球员或球体方向控制。
注意事项
- 性能:计算平方根会增加处理时间,尤其是较低性能的设备。
- 适配性:根据不同游戏需求,可以动态调整
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
只能用于标准布局类型(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
是一个简单而强大的工具,特别适合低级内存操作场景。掌握它对于深入理解结构体的内存布局和高性能编程至关重要。
相关文章:
游戏引擎学习第17天
视频参考:https://www.bilibili.com/video/BV1LPUpYJEXE/ 回顾上一天的内容 1. 整体目标: 处理键盘输入:将键盘输入的处理逻辑从平台特定的代码中分离出来,放入更独立的函数中以便管理。优化消息循环:确保消息循环能够有效处理 …...
【FFmpeg】FFmpeg 内存结构 ③ ( AVPacket 函数简介 | av_packet_ref 函数 | av_packet_clone 函数 )
文章目录 一、av_packet_ref 函数1、函数原型2、函数源码分析3、函数使用代码示例 二、av_packet_clone 函数1、函数原型2、函数源码分析 FFmpeg 4.0 版本源码地址 : GitHub : https://github.com/FFmpeg/FFmpeg/tree/release/4.0GitCode : https://gitcode.com/gh_mirrors/ff…...
【学习笔记】量化概述
Quantize量化概念与技术细节 题外话,在七八年前,一些关于表征的研究,会去做表征的压缩,比如二进制嵌入这种事情,其实做得很简单,无非是找个阈值,然后将浮点数划归为零一值,现在的Qu…...
同步互斥相关习题10道 附详解
PV操作 2016 某系统允许最多10个进程同时读文件F,当同时读文件F的进程不满10个时,欲读该文件的其他文件可立即读,当已有10个进程在读文件F时读,其他欲读文件F的进程必须等待,直至有进程读完后退出方可去读 在实现管…...
【Python · PyTorch】卷积神经网络 CNN(LeNet-5网络)
【Python PyTorch】卷积神经网络 CNN(LeNet-5网络) 1. LeNet-5网络※ LeNet-5网络结构 2. 读取数据2.1 Torchvision读取数据2.2 MNIST & FashionMNIST 下载解包读取数据 2. Mnist※ 训练 LeNet5 预测分类 3. EMnist※ 训练 LeNet5 预测分类 4. Fash…...
Git 拉取指定分支创建项目
一 背景 因为项目过大,只需要部分分支的代码即可。 二 实现 方法一:使用 --single-branch 参数 git clone 支持只拉取指定分支,而不是整个库的所有分支: git clone --branch <branch_name> --single-branch <reposi…...
CF862B Mahmoud and Ehab and the bipartiteness(二分图的性质)
思路:一个二分图是由两个集合组成的,同一个集合中的节点间不能连边,所以一个二分图最多有cnt[1]*cnt[2]条边,题目给出一个树的n-1条边,要我们添加最多的边数使他成为二分图,添加的边数就是cnt[1]*cnt[2]-n1…...
React Native 全栈开发实战班 :数据管理与状态之React Hooks 基础
在 React Native 应用中,数据管理与状态管理是构建复杂用户界面的关键。React 提供了多种工具和模式来处理数据流和状态管理,包括 React Hooks、Context API 以及第三方状态管理库(如 Redux)。本章节将详细介绍 React Hooks 的基础…...
传奇996_22——自动挂机
登录钩子函数中执行 callscript(actor, "../QuestDiary/主界面基础按钮/主界面基础按钮QM", "基础按钮QM")基础按钮QM执行了已下代码 #IF Equal <$CLIENTFLAG> 1 #ACT goto PC端面板加载#IF Equal <$CLIENTFLAG> 2 #ACT goto 移动端面板加载…...
faiss 提供了多种索引类型
faiss 多种索引类型 在 faiss 中,IndexFlatL2 是一个简单的基于 L2 距离(欧几里得距离)进行索引的索引类型,但实际上,faiss 提供了多种索引类型,支持不同的度量方式和性能优化,您可以根据需求选…...
比rsync更强大的文件同步工具rclone
背景 多个复制,拷贝,同步文件场景,最大规模的是每次几千万规模的小文件需要从云上对象存储中拉取到本地。其他的诸如定期数据备份,单次性数据备份。 rsync是单线程的,开源的mrsync是多线程的,但适用范围没…...
《业务流程--穿越从概念到实践的丛林》读后感一:什么是业务流程
1.1 流程和业务流程概念辨析 业务流程建模标准(BPMN)对于业务流程的定义:一个业务流程由为了配合一个组织性或技术环境而一系列活动组成。这些活动共同实现一个业务目标。 业务流程再造最有名的倡导者托马斯.H.达文波特对于流程和业务流程的定义:流程是一组结构化且可度量的…...
解决docker mysql命令行无法输入中文
docker启动时,设置支持中文 docker run --name mysql-container -e MYSQL_ROOT_PASSWORDroot -d mysql:5.7 --character-set-serverutf8mb4 --collation-serverutf8mb4_unicode_ci --default-time-zone8:00 进入docker时,指定LANG即可 docker exec -it …...
基于Java Springboot城市公交运营管理系统
一、作品包含 源码数据库设计文档万字PPT全套环境和工具资源部署教程 二、项目技术 前端技术:Html、Css、Js、Vue、Element-ui 数据库:MySQL 后端技术:Java、Spring Boot、MyBatis 三、运行环境 开发工具:IDEA/eclipse 数据…...
Lc70--319.两个数组的交集(二分查找)---Java版
1.题目描述 2.思路 用集合求交集,因为集合里面的元素要满足不重复、无序、唯一。使得集合在去重、查找和集合操作(如交集、并集、差集等)中非常高效和方便。 3.代码实现 class Solution {public int[] intersection(int[] nums1, int[] nu…...
亿咖通科技应邀出席微软汽车行业智享会,分享ECARX AutoGPT全新实践
11月14日,全球出行科技企业亿咖通科技(纳斯达克股票代码:ECX)应邀于广州参加由微软举行的汽车行业智享会,揭晓了亿咖通科技对“AI定义汽车”时代的洞察与技术布局,分享了亿咖通科技汽车垂直领域大模型ECARX…...
Python教程:运算符重载
在Python中,运算符重载是通过定义特殊方法(也称为魔术方法)来实现的,这些特殊方法允许类的实例像内置类型那样使用运算符。 Python提供了一系列这样的特殊方法,用于重载各种运算符。 以下是一些常见的运算符重载特殊…...
AWTK VSCode 实时预览插件端口冲突的解决办法
AWTK XML UI 预览插件:在 vscode 中实时预览 AWTK XML UI 文件,在 Copilot 的帮助下,可以大幅提高界面的开发效率。 主要特色: 真实的 UI 效果。可以设置主题,方便查看在不同主题下界面的效果。可以设置语言…...
【MySQL系列】深入理解MySQL中的存储、排序字符集
前言 在创建数据库时,我们经常会需要填写数据库的所用字符集、排序规则,字符集和排序规则是两个非常重要的概念,它们决定了数据库如何存储和比较字符串数据。在 MySQL 中,常用的存储字符集有 utf8、utf8mb4,而排序字符…...
RPC-健康检测机制
什么是健康检测? 在真实环境中服务提供方是以一个集群的方式提供服务,这对于服务调用方来说,就是一个接口会有多个服务提供方同时提供服务,调用方在每次发起请求的时候都可以拿到一个可用的连接。 健康检测,能帮助从连…...
关于Java处理Excel常规列表记录,并入库的操作
1.描述 对于常规的Excel列表(二维表格)的入库处理,一般的mysql连接工具,例如Navicat就支持。但是,因为业务需要,不想每次都去手动导入,所以这里采用编码且定时任务的形式来实现。 2.Excel常规列…...
深入理解 JavaScript 中的 Array.find() 方法:原理、性能优势与实用案例详解
目录 深入理解 JavaScript 中的 Array.find() 方法:原理、性能优势与实用案例详解 一、引言:为什么要使用Array.find() 二、Array.find()的使用与技巧 1、基础语法 2、返回值 3、使用技巧 三、Array.find()的优势与实际应用案例 1、利用返回引用…...
计算机网络安全 —— 对称加密算法 DES (一)
一、对称加密算法概念# 我们通过计算机网络传输数据时,如果无法防止他人窃听, 可以利用密码学技术将发送的数据变换成对任何不知道如何做逆变换的人都不可理解的形式, 从而保证了数据的机密性。这种变换被称为加密( encryptio…...
5. ARM_指令集
概述 分类 汇编中的符号: 指令:能够编译生成一条32位机器码,并且能被处理器识别和执行伪指令:本身不是指令,编译器可以将其替换成若干条指令伪操作:不会生成指令,只是在编译阶段告诉编译器怎…...
Jenkins的pipeline Script的 每个组件的详细讲解
在Jenkins的Pipeline脚本中,各个组件的配置和Groovy的一些常用函数起到了决定性的作用,帮助开发人员控制自动化流程的执行。以下是对Jenkins Pipeline的主要组件和Groovy常用函数的详细讲解: 1. Jenkins Pipeline主要组件 1.1 agent 功能&…...
Tomcat 和 Netty 的区别及应用场景分析
在 Java Web 开发中,Tomcat 和 Netty 都是常见的网络框架,它们各自有着不同的设计理念和适用场景。本文将通过详细的对比和实际场景示例,帮助你理解 Tomcat 和 Netty 在功能、性能、架构等方面的差异,帮助你在实际开发中做出更合理…...
6.C操作符详解,深入探索操作符与字符串处理
C操作符详解,深入探索操作符与字符串处理 C语言往期系列文章目录 往期回顾: C语言是什么?编程界的‘常青树’,它的辉煌你不可不知VS 2022 社区版C语言的安装教程,不要再卡在下载0B/s啦C语言入门:解锁基础…...
生数科技发布 Vidu 1.5 新版本,引领视频大模型新潮流
在国内视频大模型领域,生数科技一直以创新和突破而备受瞩目。近日,生数科技再度发力,发布了 Vidu 1.5 新版本,为视频创作带来了全新的变革与机遇。 Vidu 1.5 新版本在多个方面展现出了卓越的性能和创新的特点。首先,它…...
CentOS 7 aarch64停止更新后安装gcc8 —— 筑梦之路
CentOS 7.9非X86架构系统生命周期结束后(2024-6-30)配置在线可用yum源 —— 筑梦之路_centos7.9 arm-CSDN博客 以前的做法 sudo yum install centos-release-scl-rh sudo yum install devtoolset-8-buildsudo yum install devtoolset-8-gdb sudo yum i…...
WPF下 DataGrid加入序号列
先上代码: <DataGrid Name"DGV" AutoGenerateColumns"False" Grid.Row"0" Grid.Column"0" HorizontalGridLinesBrush"RoyalBlue" VerticalGridLinesBrush"Tomato" CanUserAddRows"False&qu…...
写作兼职网站/门户网站有哪些
F12后,切换到手机模式,方向没有鼠标,这对于调试前端页面来说无疑是一大难题,看不见只能盲点, 以为是浏览器问题,清理缓存,升级浏览器,清除插件等都不好使。 后来查到资料说是显卡问题…...
出台网站集约化建设通知/做网站价格
区别: #系统级环境配置文件 /etc/profile /etc/bashrc#用户级环境配置文件 ~/.bashrc ~/.profile/etc/profile: 用来设置系统环境参数,比如$PATH. 这里面的环境变量是对系统内所有用户生效的。 /etc/bashrc: 这个文件设置系统bash shell相关的东西&a…...
大连网站在哪备案/推广平台怎么做
问题描述 python中使用matplotlib包画图失败 文件开头导入matplotlib包 # 正负条形图 import matplotlib.pyplot as plt import numpy as np运行程序后报错如下: 引用文本 Matplotlib is currently using agg, which is a non-GUI backend, so cannot show the fi…...
公司做网站怎么构思/网络零售的优势有哪些
这篇文章主要为大家详细介绍了Android使用Recyclerview实现图片水平自动循环滚动效果,实现精彩的跑马灯效果,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 本篇博客主要介绍的是如何使用RecyclerView实现图片水平方向自动循环࿰…...
做展示型企业网站/广告网站建设网站排名优化
有一个文件夹/home/a, 里面有个模块叫b.py, 我怎么把他import到程序里? 1). import sys; sys.path.append("/home/a/") import b 2). 在目录里面增加__init__.py文件,里面可以写import时执行的代码,当然也可以留空就可以. import…...
有哪些ui的设计网站/谈谈你对网络营销的看法
目录一、算法思维导图二、算法分类三、冒泡排序1、基本思想2、动态效果图3、代码实现4、速度测试四、选择排序1、基本思想2、动态效果图3、代码实现4、速度测试五、插入排序1、基本思想2、动态效果图3、代码实现4、速度测试六、希尔排序1、基本思想2、效果图3、代码实例七、快速…...