1. 项目概述:为什么你的Update里总是一团糟?
干了这么多年Unity开发,我见过太多新手甚至一些工作一两年的朋友,写出来的代码里Update函数简直是个“垃圾场”——物理移动、输入检测、UI更新、相机跟随全塞在一起,运行起来不是抖动就是卡顿,帧率稍微波动一下整个游戏感觉都不对了。最要命的是,很多人对Time.deltaTime的理解还停留在“乘上它就帧率独立了”的层面,结果在FixedUpdate里也乱用一气,导致物理模拟时快时慢,角色移动飘忽不定。
这个问题的核心,在于没搞清楚Unity引擎底层那套“生命周期函数”到底是怎么协作的。Update、FixedUpdate、LateUpdate,这三个函数看似简单,但用错了地方,轻则性能浪费,重则直接破坏游戏体验。比如,你把需要稳定间隔的物理计算扔进Update,帧率一下降,你的角色跳跃高度就会变矮,子弹速度就会变慢,这绝对不是玩家想看到的。反过来,如果把每帧都需要响应的输入处理丢进FixedUpdate,玩家就会感觉操作有延迟,不跟手。
所以,今天我们就来彻底理清这团乱麻。目标很简单:让你写的每一行代码,都待在它该待的地方。物理归物理,渲染归渲染,输入归输入。我们会深入拆解这三个函数的调用时机、设计初衷,并结合最让人头疼的Time.deltaTime,给出清晰、可落地的使用规范。这不是一篇死记硬背的教程,而是我踩过无数坑后,总结出的“肌肉记忆”级最佳实践。无论你是刚入门的新手,还是想优化现有项目的老鸟,都能从这里找到立竿见影的解决方案。
2. 核心概念拆解:Update、FixedUpdate、LateUpdate到底在干嘛?
要正确使用,首先得知道它们是怎么被引擎调用的。很多人以为这三个函数是“并列”关系,其实它们处于一个非常精密的协作链条中。
2.1 Update:游戏世界的“心跳”
你可以把Update理解成游戏逻辑的“心跳”或“主循环”。它的调用频率是不固定的,直接取决于你游戏的当前帧率(FPS)。如果你的机器能跑120帧,它一秒就被调用120次;如果只能跑30帧,那就只调用30次。每次调用之间的时间间隔,就是Time.deltaTime。
Update的核心职责是什么?
- 处理玩家输入:比如
Input.GetKeyDown、Input.GetMouseButton。输入设备是随时等待用户操作的,必须每帧去查询,放在Update里才能获得最即时的反馈。 - 执行非物理的游戏逻辑:比如状态机切换、AI决策(非移动部分)、道具拾取判断、UI界面更新等。这些逻辑通常需要紧跟画面的每一帧变化。
- 执行非物理的、平滑的移动(使用Transform):如果你想手动控制一个物体平滑移动(比如菜单滑入、非物理拾取物的漂浮动画),并且希望这个移动速度不受帧率影响,你需要在Update里做,并乘以
Time.deltaTime。
注意:在Update里直接修改
Rigidbody的位置(rigidbody.position)或速度(rigidbody.velocity)是非常不推荐的。因为这相当于在物理引擎不知情的情况下“瞬移”了物体,会干扰物理引擎的连续碰撞检测和积分计算,可能导致物体穿透或其他诡异现象。物理对象的移动,应该交给物理引擎通过力(Force)或速度(Velocity)来驱动,而这正是FixedUpdate的舞台。
2.2 FixedUpdate:物理世界的“节拍器”
如果说Update是随性的心跳,那FixedUpdate就是精准的节拍器。它的调用频率是固定的,默认情况下每秒调用50次(即间隔0.02秒)。这个间隔就是Time.fixedDeltaTime。你可以在Edit -> Project Settings -> Time中修改Fixed Timestep的值来调整它。
FixedUpdate的核心职责是什么?
- 所有与物理引擎相关的操作:这是它的“主场”。包括:
- 施加力:
Rigidbody.AddForce - 设置速度:
Rigidbody.velocity - 施加扭矩:
Rigidbody.AddTorque - 查询物理状态(如射线检测用于物理判断)
- 施加力:
- 为什么物理计算必须固定间隔?物理模拟(比如牛顿力学、碰撞检测)是基于积分运算的。如果时间步长不固定,积分结果就会不稳定,导致模拟失真。想象一下计算抛物线,你用0.01秒和0.03秒作为步长算出来的轨迹肯定不一样。固定时间步长保证了无论画面帧率如何,物理世界都按照自己稳定、可预测的节奏运行,这是物理模拟准确性的基石。
一个关键误区:很多人以为FixedUpdate“一定”每0.02秒调用一次。其实不完全对。如果某一帧游戏逻辑(Update)卡顿了很长时间,引擎为了保证物理模拟的进度不落后于真实时间,可能会在一帧内连续调用多次FixedUpdate,直到“追上”当前时间。这就是为什么有时你会看到Time.deltaTime在FixedUpdate里可能小于Time.fixedDeltaTime的原因之一。
2.3 LateUpdate:收尾工作的“清道夫”
LateUpdate在所有Update函数执行完毕之后,但在渲染发生之前被调用。这个时机非常关键。
LateUpdate的核心职责是什么?
- 相机跟随:这是LateUpdate最经典、几乎唯一正确的用法。为什么?假设你的玩家角色在Update里移动了位置。如果你在同一个Update里就让相机去跟随,那么这一帧画面渲染时,相机是基于玩家移动前的位置计算的。而如果你在LateUpdate里处理相机跟随,此时玩家在本帧的所有移动都已经完成,相机就能基于玩家最终的位置进行计算和渲染,从而确保相机画面和玩家位置严格同步,避免出现抖动或拖影。
- 基于其他对象Update结果的操作:比如,一个UI血条需要跟随世界空间中的敌人。敌人在Update里移动了位置,血条应该在LateUpdate里更新坐标,以确保使用的是敌人本帧最终的位置。
2.4 三者的执行顺序与协作关系
一帧内,它们的典型执行顺序是这样的:
- 引擎处理输入。
- 调用所有脚本的Update方法(处理输入、游戏逻辑)。
- 如果需要,引擎会调用一次或多次FixedUpdate(处理物理),以追上当前时间。
- 调用所有脚本的LateUpdate方法(进行收尾工作,如相机更新)。
- 渲染画面。
理解这个顺序,你就能明白为什么相机跟随必须放在LateUpdate:因为它需要等待“演员”(GameObject)在Update里“走位”完成,才能在“摄影师”(Camera)就位前,找到最完美的拍摄位置。
3. Time.deltaTime深度剖析:帧率独立的“万能药”与“双刃剑”
Time.deltaTime可能是Unity里最常用也最容易被误解的变量之一。它的官方定义是“完成上一帧所用的时间(以秒为单位)”。但它在不同函数里的行为,才是问题的关键。
3.1 在Update中的行为:实现帧率独立移动
这是Time.deltaTime最广为人知的用途。原理很简单:距离 = 速度 * 时间。如果你的游戏目标是60帧,deltaTime大约是0.0167秒;如果是30帧,deltaTime大约是0.0333秒。
// 在Update中:实现每秒移动5个单位,不受帧率影响 void Update() { float moveSpeed = 5.0f; // 无论帧率高还是低,物体每秒都移动5个单位 transform.Translate(Vector3.forward * moveSpeed * Time.deltaTime); }如果不乘Time.deltaTime,那么物体每帧都会移动5个单位。60帧时一秒移动300单位,30帧时一秒只移动150单位,游戏体验会因设备性能差异而完全不同。
3.2 在FixedUpdate中的行为:一个常见的混淆点
这是争议和困惑的根源。根据Unity官方文档和引擎的实际行为:
- 在
FixedUpdate内部,Time.deltaTime会自动返回Time.fixedDeltaTime的值。 - 在
Update内部,Time.deltaTime返回的是上一帧实际消耗的时间。
这意味着什么?我们来看一个代码示例:
void FixedUpdate() { // 假设 fixedDeltaTime 默认是 0.02f Debug.Log("在FixedUpdate中,Time.deltaTime = " + Time.deltaTime); // 输出 0.02 Debug.Log("Time.fixedDeltaTime = " + Time.fixedDeltaTime); // 输出 0.02 } void Update() { // 假设游戏运行在约100FPS Debug.Log("在Update中,Time.deltaTime = " + Time.deltaTime); // 输出约 0.01 }那么,在FixedUpdate里到底要不要用Time.deltaTime?答案是:取决于你在做什么操作。
情况一:直接修改Transform的位置/旋转(非物理移动)如果你错误地在FixedUpdate里进行非物理的、直接的Transform操作(虽然这不推荐),那么你应该使用Time.deltaTime来保证移动速率与FixedUpdate的调用频率解耦。
// 不推荐的做法,但如果你这么做了: void FixedUpdate() { float speed = 2.0f; // 使用Time.deltaTime是安全的,它等于fixedDeltaTime transform.Translate(Vector3.up * speed * Time.deltaTime); }这里使用Time.deltaTime的好处是,如果未来你出于性能考虑修改了Fixed Timestep(比如从0.02改成0.04),这段代码的移动速度会自动适应,因为Time.deltaTime会变成0.04。如果你写死了speed * 0.02f,那么改变Timestep后速度就错了。
情况二:对Rigidbody施加力(Force)绝对不要在施加力的时候乘以Time.deltaTime!
void FixedUpdate() { // 正确:力是瞬时冲量,物理引擎会自己考虑时间步长进行积分 rigidbody.AddForce(Vector3.forward * 10f); // 错误:这会使得施加的力随着Fixed Timestep变化而改变,破坏物理模拟 // rigidbody.AddForce(Vector3.forward * 10f * Time.deltaTime); }Unity的物理引擎(如PhysX)在计算力产生的加速度和速度变化时,已经在内部积分中考虑了fixedDeltaTime。你再乘一次,就相当于多除了一次时间,导致力的大小错误。
情况三:直接设置Rigidbody的速度(Velocity)设置速度本身是瞬时的,但如果你希望速度值是一个“每秒移动多少单位”的速率概念,那么需要乘以Time.deltaTime。
void FixedUpdate() { float targetSpeed = 5.0f; // 意思是:我想让物体以每秒5米的速度移动 // 错误:这会将速度瞬间设为(0,0,5),物体每物理帧都移动5单位,速度过快且不固定 // rigidbody.velocity = new Vector3(0, 0, targetSpeed); // 正确:将“每秒速率”转换为“每物理帧的位移增量” rigidbody.velocity = new Vector3(0, 0, targetSpeed * Time.deltaTime / Time.fixedDeltaTime); // 更清晰的写法:直接使用每秒速率,但注意这仍然是“瞬时设置” // 通常更好的做法是用AddForce达到目标速度 }实际上,直接设置velocity来达成一个恒定移动速度,本身就不是物理风格的做法。更物理的方式是使用力来驱动。但如果你必须设置,要理解你设置的是瞬时速度向量,而不是速率。
核心结论: 在FixedUpdate中,对于非物理的、与帧率无关的标量计算(比如一个基于时间成长的计时器),使用Time.deltaTime是安全且推荐的,因为它能自适应fixedDeltaTime的变化。对于物理引擎相关的操作(AddForce等),遵循物理引擎的规则,不要额外乘Time.deltaTime。
3.3 Time.fixedDeltaTime 与 Time.deltaTime 的对比总结
| 特性 | Time.deltaTime | Time.fixedDeltaTime |
|---|---|---|
| 含义 | 上一帧的实际耗时 | 固定的物理更新间隔 |
| 在Update中 | 等于上一帧实际时间 | 固定值(如0.02) |
| 在FixedUpdate中 | 等于Time.fixedDeltaTime | 固定值(如0.02) |
| 主要用途 | Update中的帧率独立运算 | 配置物理模拟的步长,或在FixedUpdate中明确需要固定步长的计算 |
| 可变性 | 每帧都可能变化 | 在运行时可通过代码修改(Time.fixedDeltaTime = 0.01f),但修改会影响所有物理模拟 |
4. 实战应用:用FixedUpdate和LateUpdate重构你的代码
理论说再多,不如动手改一改。我们来看两个最常见的场景,看看如何把乱糟糟的Update清理干净。
4.1 场景一:玩家角色移动(物理版本)
假设我们有一个使用Rigidbody的玩家角色,需要接收WASD输入进行移动。
错误示范(全塞在Update里):
public float moveForce = 10f; private Rigidbody rb; void Start() { rb = GetComponent<Rigidbody>(); } void Update() { // 1. 获取输入(正确,应在Update) float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); // 2. 计算移动方向 Vector3 movement = new Vector3(h, 0f, v).normalized; // 3. 错误!在Update里对Rigidbody施加力 rb.AddForce(movement * moveForce); // 4. 错误!在Update里进行相机跟随(假设这是相机脚本) // transform.position = player.position + offset; }问题分析:
AddForce在Update中调用,调用频率不稳定。帧率高时力施加频繁,帧率低时施加稀疏,导致移动手感不一致,物理表现不稳定。- 相机跟随在Update中,可能基于玩家未完全被物理引擎处理的位置,导致画面抖动。
正确重构:
public float moveForce = 10f; private Rigidbody rb; private Vector3 movementInput; // 缓存输入 void Start() { rb = GetComponent<Rigidbody>(); } void Update() { // 1. 仅在Update中获取并缓存输入 float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); movementInput = new Vector3(h, 0f, v).normalized; } void FixedUpdate() { // 2. 在FixedUpdate中,使用缓存的输入施加物理力 // 注意:这里不乘Time.deltaTime! if (movementInput.magnitude > 0.1f) { rb.AddForce(movementInput * moveForce); } }关键点:
Update负责即时响应的输入采样。FixedUpdate负责稳定执行的物理计算(AddForce)。- 通过一个成员变量
movementInput在帧间传递数据。因为FixedUpdate可能在一帧内调用多次,而Update只调用一次,所以需要缓存,确保每次FixedUpdate都使用同一帧的输入。
4.2 场景二:平滑的相机跟随
相机跟随必须平滑且无抖动,LateUpdate是最佳选择。
错误示范(在Update中跟随):
public Transform target; public float smoothTime = 0.3f; private Vector3 velocity = Vector3.zero; void Update() { if (target != null) { Vector3 targetPosition = target.position + new Vector3(0, 5, -10); // 在Update中平滑移动 transform.position = Vector3.SmoothDamp(transform.position, targetPosition, ref velocity, smoothTime); } }潜在问题:如果目标物体(target)也在Update中移动,且它的移动顺序在本相机脚本的Update之前执行,那么可能没问题。但Unity不保证脚本执行顺序(除非手动设置)。如果相机脚本先执行,它就会用上一帧的目标位置来计算,导致相机永远慢一帧,产生细微的滞后或抖动。
正确重构(使用LateUpdate):
public Transform target; public float smoothTime = 0.3f; private Vector3 velocity = Vector3.zero; void LateUpdate() { if (target != null) { Vector3 targetPosition = target.position + new Vector3(0, 5, -10); // 在LateUpdate中,target.position已经是本帧最终位置 transform.position = Vector3.SmoothDamp(transform.position, targetPosition, ref velocity, smoothTime); } }进阶技巧:应对高速移动目标如果目标移动速度非常快(比如赛车游戏),简单的SmoothDamp可能会导致相机永远“追不上”目标,产生严重的延迟感。一个更好的方案是使用Vector3.Lerp或SmoothDamp,但基于预测位置。
void LateUpdate() { if (target != null && target.TryGetComponent<Rigidbody>(out Rigidbody rb)) { // 计算预测位置:当前位置 + 速度 * 一个预测时间(如0.1秒) Vector3 predictedPosition = target.position + rb.velocity * 0.1f; Vector3 desiredPosition = predictedPosition + offset; transform.position = Vector3.Lerp(transform.position, desiredPosition, smoothSpeed * Time.deltaTime); // 注意:这里在LateUpdate中,Time.deltaTime是上一帧的渲染时间,用于Lerp的插值是合适的。 } }4.3 一个综合案例:第三人称角色控制器
我们来组合一下,创建一个简单的、使用物理移动和相机跟随的第三人称控制器。
PlayerMovement.cs (挂在玩家角色上)
public class PlayerMovement : MonoBehaviour { public float moveSpeed = 5f; public float jumpForce = 5f; public float groundCheckDistance = 0.2f; public LayerMask groundLayer; private Rigidbody rb; private Vector2 inputVector; private bool isGrounded; private bool jumpPressed; void Start() { rb = GetComponent<Rigidbody>(); } void Update() { // 1. 获取移动输入 float horizontal = Input.GetAxis("Horizontal"); float vertical = Input.GetAxis("Vertical"); inputVector = new Vector2(horizontal, vertical); // 2. 获取跳跃输入(按下瞬间) if (Input.GetButtonDown("Jump") && isGrounded) { jumpPressed = true; } // 3. 地面检测(也可放在FixedUpdate,但放在Update响应更及时) RaycastHit hit; isGrounded = Physics.Raycast(transform.position, Vector3.down, out hit, groundCheckDistance, groundLayer); } void FixedUpdate() { // 4. 物理移动:将输入转换为世界空间方向 Vector3 cameraForward = Camera.main.transform.forward; cameraForward.y = 0; cameraForward.Normalize(); Vector3 moveDirection = (cameraForward * inputVector.y + Camera.main.transform.right * inputVector.x).normalized; // 计算目标速度,并施加力以达到它(这是一种速度控制方式) Vector3 targetVelocity = moveDirection * moveSpeed; Vector3 velocityChange = targetVelocity - new Vector3(rb.velocity.x, 0, rb.velocity.z); // 使用ForceMode.VelocityChange,忽略质量,直接改变速度 rb.AddForce(velocityChange, ForceMode.VelocityChange); // 5. 处理跳跃 if (jumpPressed) { rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); // Impulse是瞬时力 jumpPressed = false; // 重置标志 } } }CameraFollow.cs (挂在相机上)
public class CameraFollow : MonoBehaviour { public Transform target; public Vector3 offset = new Vector3(0, 2, -5); public float followSpeed = 10f; public float rotationSpeed = 5f; void LateUpdate() { if (target == null) return; // 计算期望的相机位置和旋转 Vector3 desiredPosition = target.position + target.TransformDirection(offset); Quaternion desiredRotation = Quaternion.LookRotation(target.position - transform.position + Vector3.up * 1f); // 平滑插值 transform.position = Vector3.Lerp(transform.position, desiredPosition, followSpeed * Time.deltaTime); transform.rotation = Quaternion.Slerp(transform.rotation, desiredRotation, rotationSpeed * Time.deltaTime); } }这个案例清晰地展示了职责分离:
PlayerMovement.Update: 处理输入和即时状态检测。PlayerMovement.FixedUpdate: 执行物理移动和跳跃。CameraFollow.LateUpdate: 基于玩家最终位置进行平滑跟随。
5. 高级议题与性能优化
当你掌握了基本用法后,下面这些进阶知识能帮你写出更高效、更稳定的代码。
5.1 关于FixedUpdate的调用频率与性能
FixedUpdate的调用是“尽力保证”的。如果游戏卡顿严重,引擎为了不让物理模拟落后太多,会在一帧内多次调用FixedUpdate,这被称为“追赶循环”(Catch-up Loop)。这可能导致单帧计算量暴增,进一步加剧卡顿。
监控与诊断: 你可以在Update里打印Time.timeScale和Time.deltaTime来观察。如果一帧内Time.deltaTime很大(比如>0.1s),而FixedUpdate里的逻辑又很复杂,就可能出现问题。
优化建议:
- 精简FixedUpdate内的逻辑:只做必须的物理相关操作。复杂的AI计算、路径寻找等,应该移到
Update中,或者通过协程分帧处理。 - 调整Fixed Timestep:在
Project Settings -> Time中,增大Fixed Timestep(比如从0.02调到0.04),会降低FixedUpdate的调用频率(从50Hz降到25Hz),提升性能,但会降低物理模拟的精度。对于移动端或非核心物理游戏,这是一个有效的权衡。 - 使用
Maximum Allowed Timestep:这个设置(默认0.333)限制了引擎在一帧内用于处理FixedUpdate和物理模拟的最大时间。如果一帧的物理计算超过了这个时间,引擎会暂停物理模拟,直接进行下一帧渲染,防止游戏完全卡死。你可以适当调低这个值来保证帧率,但物理可能会变得“慢动作”。
5.2 Update、FixedUpdate与协程(Coroutine)的协作
协程是处理跨帧行为的强大工具,但它和Update的生命周期紧密相关。yield return new WaitForFixedUpdate()可以让协程在FixedUpdate之后恢复执行,这非常有用。
应用场景:你想在施加一个力之后,等待物理引擎模拟几步,再检查结果。
IEnumerator JumpAndCheck() { // 在Update或FixedUpdate中启动这个协程 rb.AddForce(Vector3.up * 10f, ForceMode.Impulse); // 等待下一个FixedUpdate之后 yield return new WaitForFixedUpdate(); // 再等待几个物理帧 yield return new WaitForFixedUpdate(); yield return new WaitForFixedUpdate(); // 现在检查物理状态 if (rb.velocity.y < 0) { Debug.Log("开始下落了"); } }5.3 在FixedUpdate中处理输入的特殊情况
我们之前说输入在Update中获取。但如果你的游戏是纯物理的,且FixedUpdate频率远高于Update(比如144Hz的FixedUpdate对比60Hz的渲染),你可能会担心输入响应不够细腻。
实际上,Unity的Input类在内部已经做了处理。Input.GetAxis返回的是经过平滑滤波的值,而Input.GetButton等状态查询在帧间是持续有效的。所以,在FixedUpdate里读取Update中采样到的输入,对于绝大多数游戏来说已经足够及时。
对于要求极高的格斗或竞技游戏,可以考虑:
- 使用
Input System包:新的Input System提供了更灵活、更低延迟的输入处理方式。 - 在
FixedUpdate中直接使用Input:虽然不典型,但也是可行的。只是要注意GetButtonDown这种“按下瞬间”的事件,在FixedUpdate里可能会被漏掉(如果按下发生在两次FixedUpdate调用之间)。更可靠的做法是在Update中捕获事件,设置一个标志位,在FixedUpdate中消费。
6. 常见问题排查与实战技巧
这里记录了一些我实际开发中遇到的坑和解决方案。
6.1 问题排查清单
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 物理对象移动抖动、不平滑 | 1. 在Update中修改Rigidbody.position/rotation。2. 相机跟随在 Update中,且与物理对象更新顺序错乱。 | 1. 物理移动改用AddForce或设置velocity,并在FixedUpdate中进行。2. 相机脚本移至 LateUpdate。 |
| 移动速度时快时慢 | 1. 在Update中做物理移动且未乘Time.deltaTime。2. 在 FixedUpdate中对力乘了Time.deltaTime。 | 1.Update中的非物理移动必须乘Time.deltaTime。2. FixedUpdate中的AddForce不要乘Time.deltaTime。 |
| 相机跟随有拖影或抖动 | 相机跟随逻辑写在Update中,且执行顺序在玩家移动之前。 | 将相机跟随逻辑毫无例外地移到LateUpdate中。 |
| 跳跃手感不一致,有时跳不高 | 跳跃检测(如GetButtonDown)和跳跃力施加(AddForce)不在同一帧处理,或者FixedUpdate频率波动。 | 在Update中检测跳跃并设置一个bool标志,在FixedUpdate中检查该标志并施加力,然后重置标志。 |
| 游戏在低帧率时变慢(物理时间变慢) | Time.timeScale被降低,或者Maximum Allowed Timestep设置过小,导致物理模拟追赶不上真实时间。 | 检查代码中是否有修改Time.timeScale。增大Maximum Allowed Timestep(但会降低帧率稳定性底线)。优化FixedUpdate和物理计算性能。 |
6.2 实战技巧与心得
为你的脚本排序:在Unity的
Edit -> Project Settings -> Script Execution Order中,可以设置脚本的执行顺序。对于有依赖关系的脚本(比如PlayerInput脚本必须在PlayerMovement之前执行),这是一个很好的管理手段。但不要滥用,良好的架构设计应尽量减少这种硬性依赖。使用
[RequireComponent(typeof(Rigidbody))]:如果你的脚本必须依赖Rigidbody,在类上方加上这个属性。当脚本被挂载时,如果物体上没有Rigidbody,Unity会自动添加一个,避免空引用错误。在FixedUpdate中谨慎使用
GetComponent和Find:这些函数开销较大。如果FixedUpdate调用频繁,应将其结果在Start或Awake中缓存。调试利器:
OnDrawGizmos:在FixedUpdate中计算的一些数据(如射线检测的起点终点、施加力的方向)可以用OnDrawGizmos画出来,在Scene视图中可视化,对于调试物理行为至关重要。void OnDrawGizmos() { Gizmos.color = Color.red; // 画出地面检测射线 Gizmos.DrawLine(transform.position, transform.position + Vector3.down * groundCheckDistance); }理解“时间缩放”(Time Scale)的影响:当
Time.timeScale = 0时,Update和LateUpdate不再被调用,但FixedUpdate仍然会以真实时间被调用,只是Time.deltaTime和Time.fixedDeltaTime会乘以timeScale(即为0)。如果你的游戏有暂停功能,并且希望在暂停时完全停止物理模拟,你需要额外判断Time.timeScale或使用自己的暂停标志。
说到底,理清Update、FixedUpdate和LateUpdate的关系,本质上是理解Unity引擎如何管理“逻辑更新”、“物理模拟”和“渲染准备”这三个核心循环。把它们混为一谈,代码就会变成一团难以维护和调试的乱麻。而一旦你习惯了这种分离的思维,写出的代码不仅运行更稳定,性能更好,结构也会清晰得多。下次当你动手写一个新的MonoBehaviour时,先花三秒钟想一下:这部分逻辑属于哪个循环?这一个小小的习惯,就是专业与业余的分水岭。