
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.deltaTimevoid 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.deltaTimeTime.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 GetComponentRigidbody(); } 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 GetComponentRigidbody(); } 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不保证脚本执行顺序除非手动设置。如果相机脚本先执行它就会用上一帧的目标位置来计算导致相机永远慢一帧产生细微的滞后或抖动。正确重构使用LateUpdatepublic 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.TryGetComponentRigidbody(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 GetComponentRigidbody(); } 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在类上方加上这个属性。当脚本被挂载时如果物体上没有RigidbodyUnity会自动添加一个避免空引用错误。在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时先花三秒钟想一下这部分逻辑属于哪个循环这一个小小的习惯就是专业与业余的分水岭。