1. 项目概述为什么这三个Update是Unity开发者的必修课刚接触Unity的新手或者是从其他引擎转过来的朋友第一次打开脚本看到Update、FixedUpdate和LateUpdate这三个方法时大概率会有点懵。这不就是三个定时执行的函数吗随便用一个Update不就行了为什么还要分三个这恰恰是很多项目在后期出现“玄学”Bug的根源——比如角色移动一卡一卡的、物理碰撞时灵时不灵、摄像机跟着角色抖动等等。这些问题十有八九都和这三个方法的滥用或误用有关。简单来说这三个方法是Unity MonoBehavior生命周期中用于处理游戏逻辑更新的核心钩子但它们各自服务的场景、执行时机和底层原理天差地别。Update与游戏帧率绑定处理玩家输入、非物理动画和游戏状态逻辑是它的主战场。FixedUpdate则独立于帧率以固定的时间步长运行是物理计算如Rigidbody移动、碰撞检测的“安全屋”。LateUpdate在Update之后执行专门用来处理那些需要基于当前帧所有Update结果才能进行的操作比如摄像机跟随。理解它们不仅仅是记住定义更是要掌握在什么场景下该用哪个以及如何避免它们之间相互“打架”。这篇指南就是基于我这些年踩过的坑和项目实战经验为你梳理出一套清晰的、可落地的使用规范和避坑策略。无论你是正在学习Unity的初学者还是已经有一定经验但想优化项目稳定性的开发者这篇文章都能帮你把这块基础打得更牢。2. 核心原理深度拆解帧、时间与执行顺序要避坑先得明白坑是怎么来的。Unity的运行核心是一个循环我们称之为游戏循环Game Loop。每一帧引擎都会按特定顺序执行一系列操作处理输入、执行脚本、进行物理模拟、渲染画面等。我们的Update、FixedUpdate和LateUpdate就穿插在这个循环的特定阶段。2.1 Update与渲染帧率共舞Update方法是开发者最熟悉也最容易用错的一个。它的调用频率与游戏的帧率FPS直接相关。简单说你的游戏每秒渲染多少帧Update就在这一秒内被调用多少次。执行时机在每一帧渲染之前调用。顺序上所有游戏对象GameObject的Update方法会按某种顺序非确定顺序不应依赖在这一阶段被执行。关键变量Time.deltaTime。这是理解Update的关键。它表示上一帧到当前帧所经过的时间以秒为单位。如果你的游戏运行在60FPS那么deltaTime大约为0.0167秒1/60如果帧率降到30deltaTime就变成约0.0333秒。核心用途处理与帧率相关的逻辑。例如玩家输入Input.GetKey、Input.GetMouseButton等。你需要即时响应玩家的操作。非物理移动与动画比如让一个物体匀速旋转或者基于插值Lerp的平滑移动。游戏状态机与计时器管理角色的状态Idle, Run, Attack或技能冷却。避坑提示1永远用Time.deltaTime来平滑帧率变化在Update中做位移或旋转时切忌直接使用固定值。错误示例transform.position Vector3.forward * 10;这会导致在高帧率下物体移动飞快低帧率下移动缓慢。正确做法transform.position Vector3.forward * 10 * Time.deltaTime;这样就能保证物体每秒移动10个单位与帧率无关。2.2 FixedUpdate物理世界的恒定心跳如果说Update是随性的艺术家那FixedUpdate就是严谨的科学家。它的调用间隔是固定的默认情况下为0.02秒即每秒50次。你可以在Edit - Project Settings - Time中修改Fixed Timestep的值。执行时机在一个独立的物理更新循环中调用。这个循环的更新频率是固定的与渲染帧率脱钩。引擎会确保在每次渲染前完成足够次数的FixedUpdate调用以“追上”真实的游戏时间。关键变量Time.fixedDeltaTime。这就是FixedUpdate的固定时间步长默认等于你在Project Settings中设置的Fixed Timestep值。核心用途所有与物理引擎PhysX相关的操作必须放在这里。施加力Force或修改速度VelocityRigidbody.AddForce(),Rigidbody.velocity ...。直接通过物理方式移动刚体Rigidbody.MovePosition()。任何读取或修改Rigidbody属性的操作如果希望结果被物理引擎正确处理。避坑提示2FixedUpdate内不要使用Time.deltaTime在FixedUpdate中你应该使用Time.fixedDeltaTime。虽然在某些简单情况下混用可能看不出问题但这破坏了逻辑的一致性。物理计算依赖固定的时间步长以保证稳定性和可预测性使用变动的deltaTime会引入不确定性尤其在网络同步或复杂物理场景中可能引发问题。2.3 LateUpdate一帧的收官之笔LateUpdate在Update之后执行但在渲染之前。它的设计初衷是为了解决一些依赖顺序的问题。执行时机当所有游戏对象的Update方法都执行完毕后才会开始执行LateUpdate。核心用途处理需要基于当前帧最终状态的操作。摄像机跟随这是最经典的用例。你需要确保角色在Update中完成移动包括物理和非物理移动后摄像机再根据角色的新位置进行计算和移动。如果放在Update里可能会出现摄像机移动和角色移动在同一帧内顺序不确定导致的轻微抖动。UI更新当UI需要根据多个在Update中更新的游戏对象状态来刷新时比如血量条跟随多个动态变化的角色放在LateUpdate可以确保获取到的是最终值。其他依赖顺序的逻辑比如A对象跟随B对象而B对象在Update中有复杂的移动逻辑。避坑提示3不是所有跟随逻辑都需要LateUpdate如果跟随目标比如角色的移动完全是在FixedUpdate中通过物理引擎完成的那么跟随者比如摄像机放在FixedUpdate里可能更合适因为这样能和物理更新保持同步。但如果目标移动混杂了Update中的非物理移动则LateUpdate是更安全的选择。需要根据具体情况分析。3. 实战场景与选型决策树理论懂了到底怎么用下面我通过几个最常见的场景帮你建立直观的决策思路。3.1 场景一角色移动控制这是最基础也最容易混淆的场景。使用键盘/WASD控制一个带有CharacterController组件的角色移动选Update。CharacterController虽然处理碰撞但其.Move()方法本身不是物理力驱动它更像一个高级的碰撞体移动封装。你需要在Update中获取输入然后使用Time.deltaTime来保证移动速度与帧率无关。void Update() { float horizontal Input.GetAxis(“Horizontal”); float vertical Input.GetAxis(“Vertical”); Vector3 move new Vector3(horizontal, 0, vertical) * speed * Time.deltaTime; characterController.Move(move); }使用力或速度来控制一个带有Rigidbody的角色移动比如赛车游戏、球类游戏选FixedUpdate。这是物理驱动的移动必须放在物理更新循环中。void FixedUpdate() { float acceleration Input.GetAxis(“Vertical”) * motorForce; rb.AddForce(transform.forward * acceleration); // 转向处理也可能放在这里 }使用Transform.Translate或直接修改Transform.position来移动一个普通物体无物理需求选Update并务必乘以Time.deltaTime。3.2 场景二摄像机跟随跟随一个在Update中移动的非物理对象选LateUpdate。这是标准做法确保摄像机拿到的是角色本帧移动后的最终位置。public Transform target; public Vector3 offset; void LateUpdate() { transform.position target.position offset; // 或者使用平滑阻尼SmoothDamp }跟随一个在FixedUpdate中移动的物理对象如一个被力推动的球可以考虑FixedUpdate。这样摄像机的更新节奏和物理对象的更新节奏完全同步理论上最平滑。但要注意如果FixedUpdate频率如50Hz低于渲染帧率如60Hz摄像机更新会少于屏幕渲染可能感觉不够丝滑。此时可以在LateUpdate中通过插值来平滑。更高级的做法在FixedUpdate中计算摄像机的目标位置在LateUpdate中对摄像机位置进行插值平滑。这结合了物理同步和渲染平滑的优点。3.3 场景三攻击检测与伤害计算基于射线Raycast或碰撞体Trigger的瞬时攻击检测如挥剑选Update。因为输入是瞬时事件需要在收到输入的那一帧立刻检测。例如在Update中检测鼠标点击然后发射射线。持续性的区域伤害如角色站在火堆里持续掉血选Update但需要结合计时器。你可以用Time.deltaTime累加时间每隔固定时间如1秒计算一次伤害。这比每帧计算一次更合理。如果伤害区域与物理交互紧密比如一个带有物理力的狂风区域相关力的施加部分可能需要放在FixedUpdate中。3.4 决策流程图面对一个功能时你可以快速问自己以下几个问题来决策这个逻辑是否直接操作RigidbodyAddForce, velocity, MovePosition是- 毫不犹豫用FixedUpdate。否- 进入下一题。这个逻辑是否严格依赖当前帧所有其他Update逻辑都执行完毕后的最终状态典型例子摄像机跟随主角是- 使用LateUpdate。否- 进入下一题。这个逻辑需要每帧都检查吗比如处理输入、播放非物理动画、游戏状态更新是- 使用Update并处理好Time.deltaTime。否- 思考是否可以用协程Coroutine、事件Event或定时器Invoke来更高效地处理。4. 高级议题与性能优化当项目变得复杂仅仅知道怎么用还不够还得知道怎么用得高效、不出错。4.1 执行顺序的陷阱与掌控默认情况下不同游戏对象间Update的执行顺序是不确定的。这可能导致一些隐蔽的Bug。例如对象A在Update中依赖对象B在本帧更新后的数据但如果A的Update比B先执行它就拿到了旧数据。解决方案1脚本执行顺序Script Execution Order你可以在Edit - Project Settings - Script Execution Order中手动调整脚本的执行顺序。通过点击“”添加你的脚本并给它赋予一个优先级数字越小越先执行。这可以强制让B脚本的Update在A之前运行。但请谨慎使用过度依赖会导致项目难以维护。解决方案2使用LateUpdate进行协调如果A只需要在B的Update之后运行一个更清晰的做法是将A的逻辑移到LateUpdate中。因为LateUpdate在所有Update之后天然保证了顺序。解决方案3基于事件驱动这是更解耦、更优雅的方式。B在Update中完成工作后触发一个事件C#的event或UnityEvent。A订阅这个事件在事件回调中执行自己的逻辑。这样完全解除了执行顺序的耦合。4.2 FixedUpdate的频率与“螺旋式下降”Fixed Timestep设置得太高如0.005秒即200Hz会显著增加CPU负担因为物理引擎每帧要计算更多次。在复杂物理场景下可能造成卡顿。更危险的是“螺旋式下降”Spiral of Death当某一帧因为内容复杂导致渲染时间deltaTime很长时为了追上游戏时间引擎需要在本帧渲染前执行非常多次FixedUpdate。如果单次FixedUpdate也很耗时就会导致这一帧的总时间更长下一帧需要追赶的FixedUpdate次数更多恶性循环最终游戏卡死。优化策略合理设置Fixed Timestep对于大多数游戏0.02秒50Hz足够。对于需要高精度物理模拟的游戏如赛车可以尝试0.01秒100Hz但要密切监控性能。保持FixedUpdate内逻辑轻量只做必须的物理相关操作。复杂的计算、寻路、AI决策等应移到Update中。监控Time.maximumAllowedTimestep这个设置也在Time设置里限制了引擎在一帧内用于处理FixedUpdate的最大时间。它可以防止“螺旋式下降”完全拖垮游戏但代价是游戏会“丢时间”表现为慢动作。将其设置为一个合理的值如0.1秒或0.2秒作为安全网。4.3 Update、FixedUpdate与Time.timeScaleTime.timeScale用于控制游戏时间的缩放1为正常0为暂停0.5为慢放。它会影响Time.deltaTime和Time.fixedDeltaTime。Update中的Time.deltaTime会自动乘以timeScale。所以当你设置timeScale 0时所有乘以Time.deltaTime的移动都会停止游戏完美暂停。FixedUpdate的调用频率也会受到timeScale影响。timeScale变小FixedUpdate的调用频率也会等比例降低。这意味着物理世界也变慢了。需要注意如果你希望在游戏暂停时timeScale0仍然执行某些逻辑比如UI动画、暂停菜单逻辑你需要使用Time.unscaledDeltaTime来代替Time.deltaTime并且这些逻辑不能依赖于FixedUpdate因为FixedUpdate在timeScale0时不再被调用。5. 常见问题排查与调试技巧在实际开发中问题总会不期而至。下面是一些典型症状和排查思路。5.1 问题现象与排查表问题现象可能原因排查与解决方案角色移动卡顿、不流畅1. 在Update中移动物理对象Rigidbody而未使用FixedUpdate。2.FixedUpdate中逻辑过于复杂导致物理更新帧率不稳定。3. 移动代码未使用Time.deltaTime或Time.fixedDeltaTime进行平滑。1. 检查移动代码所在方法确保物理移动在FixedUpdate中。2. 使用Unity Profiler查看FixedUpdate耗时优化其中代码。3. 检查所有位移、旋转代码确保乘以了正确的时间增量。物理对象穿透或碰撞检测不稳定1. 在Update中修改Rigidbody的位置/旋转与物理引擎内部计算冲突。2.Fixed Timestep设置过高物理更新跟不上对象实际移动速度。3. 移动速度过快每帧移动距离超过碰撞体尺寸。1.绝对禁止在Update中使用transform.position修改物理对象位置。改用Rigidbody.MovePosition并置于FixedUpdate。2. 适当降低Fixed Timestep增加数值或检查对象速度是否合理。3. 对于高速移动物体考虑启用Rigidbody的Collision Detection为“Continuous”或“Continuous Dynamic”。摄像机跟随有轻微抖动或延迟1. 摄像机跟随代码放在了Update中而目标移动在LateUpdate或顺序不确定。2. 跟随逻辑没有进行平滑处理如使用Vector3.Lerp或SmoothDamp。1. 将摄像机跟随逻辑移至LateUpdate。2. 引入平滑插值。如果目标在FixedUpdate中移动可以考虑在LateUpdate中对摄像机位置进行插值以匹配渲染帧率。游戏在复杂场景时突然严重卡顿甚至冻结可能触发了“螺旋式下降”。某一帧渲染时间过长导致需要执行海量FixedUpdate来追赶时间。1. 在Profiler中确认是否是FixedUpdate耗时激增。2. 优化FixedUpdate和整体游戏性能。3. 设置Time.maximumAllowedTimestep如0.1秒作为保护。Time.timeScale 0 时某些动画或UI还在动这些动画使用了Time.unscaledDeltaTime或是在Update中使用了未缩放的时间。这是预期行为。如果希望它们也暂停需要手动控制其更新开关或改用受timeScale影响的Time.deltaTime。5.2 实用调试技巧可视化调试在代码中临时使用Debug.Log输出Time.deltaTime、Time.fixedDeltaTime以及Time.frameCount。观察在卡顿时这些值的变化能快速定位是帧率问题还是物理更新问题。使用ProfilerUnity Profiler是你的最佳伙伴。重点关注CPU Usage模块查看Update、FixedUpdate、LateUpdate各自的耗时占比。Physics模块查看物理模拟的耗时。如果FixedUpdate调用次数Calls异常高就是“螺旋式下降”的迹象。帧率显示在游戏视窗中打开Stats面板或自己编写一个简单的帧率显示UI。持续观察帧率变化与游戏卡顿现象关联分析。隔离测试当怀疑是Update/FixedUpdate使用不当时创建一个最简单的测试场景。只放一个Cube和你的问题脚本排除其他干扰因素往往能更快找到根源。6. 2023年新动向与最佳实践补充随着Unity版本更新和开发社区经验的沉淀一些新的工具和思路也值得关注。Unity新的输入系统Input System Package新的输入系统提供了Update和FixedUpdate两种输入更新模式。如果你的游戏逻辑主要在FixedUpdate中如物理驱动的游戏可以将输入动作的触发类型设置为FixedUpdate这样可以避免输入事件在Update中触发但逻辑在FixedUpdate中响应所带来的额外一帧延迟使操作更加跟手。对于性能极度敏感的场景可以考虑手动管理更新。例如对于大量不需要每帧更新的物体如远处的NPC、环境粒子效果可以禁用其MonoBehaviour然后通过一个管理器脚本以低于帧率的频率如每秒10次去批量更新它们的状态。这比让每个对象都空跑Update要高效得多。Time类的灵活运用除了deltaTime和fixedDeltaTime理解Time.unscaledDeltaTime不受timeScale影响、Time.time游戏启动后总时间、Time.fixedTimeFixedUpdate累计时间等能在处理暂停、慢放、计时等逻辑时更加得心应手。说到底Update、FixedUpdate和LateUpdate是Unity提供给我们的工具规则清晰但运用之妙存乎一心。真正的“避坑”不在于死记硬背规则而在于深刻理解每一行代码在游戏循环中所处的阶段和意图。下次当你写下任何一个Update方法时不妨先停一秒问自己我写的这个逻辑真的属于这里吗这个简单的习惯能帮你省去大量后期调试的麻烦。