Unity物理系统核心:Update与FixedUpdate的5个关键区别与实战应用

📅 2026/8/2 11:23:17
Unity物理系统核心:Update与FixedUpdate的5个关键区别与实战应用
1. 项目概述为什么Unity物理系统需要两个“更新”刚接触Unity开发的朋友尤其是从其他游戏引擎或者纯应用开发转过来的很容易被Update和FixedUpdate这两个函数搞懵。我刚开始用Unity做一个小球滚动的Demo时就踩过一个大坑在Update里直接修改刚体的速度结果在性能好的电脑上小球跑得飞快在旧电脑上却慢悠悠的物理碰撞也变得时灵时不灵。这其实就是没搞懂这两个核心函数在物理系统中的分工。简单来说Update是“画面更新”的节奏大师而FixedUpdate是“物理模拟”的节拍器。Unity的物理引擎默认是NVIDIA PhysX是一个独立的、基于固定时间步长的模拟系统。它要求在一个稳定、可预测的时间间隔内进行运算才能保证物理现象比如碰撞、重力、关节约束的确定性和准确性。如果物理计算跟着画面帧率走帧率高时算得多帧率低时算得少那一个物体从高处落下的速度、碰撞反弹的力度都会变得飘忽不定游戏就毫无公平性和可玩性可言了。因此FixedUpdate就是为这个固定时间步长服务的专用回调。而Update则与渲染帧率绑定主要负责处理输入、播放动画、更新非物理逻辑等与视觉表现紧密相关的内容。理解它们5个维度的关键区别是写出稳定、高效物理交互代码的基石。无论你是想做一款手感扎实的2D平台跳跃游戏还是一个物体交互复杂的模拟器这篇文章都能帮你避开我当年踩过的那些坑。2. 核心区别五维深度解析2.1 调用频率与稳定性帧率依赖 vs 时间步长锁定这是最根本、也最易理解的区别但背后的影响远比表面深刻。Update与渲染共舞Update的调用频率直接取决于游戏的当前帧率FPS。在你的MonoBehaviour脚本中每一渲染帧执行一次。如果游戏以60FPS运行它每秒就被调用60次如果帧率降到30FPS它每秒就只被调用30次。它的时间间隔Time.deltaTime是变量表示上一帧到当前帧的实际时间差。void Update() { // 帧率依赖的移动距离与时间成正比但调用次数不稳定 float moveDistance speed * Time.deltaTime; transform.Translate(Vector3.forward * moveDistance); }这种模式适合视觉连续性的操作比如相机平滑跟随、基于时间的动画混合。但它致命的弱点在于不确定性。对于物理计算我们需要“力”在固定时间内产生固定的“冲量”从而得到确定的速度变化。如果计算力的时间间隔忽长忽短结果就会失控。FixedUpdate物理世界的节拍器FixedUpdate的调用频率是固定的默认情况下每秒调用50次即时间间隔Time.fixedDeltaTime默认为0.02秒。你可以在Edit - Project Settings - Time中修改Fixed Timestep的值。无论游戏帧率是120还是20物理引擎都会尽力按照这个固定的节奏来更新状态。void FixedUpdate() { // 固定时间步长的物理操作 Vector3 force Vector3.up * jumpForce; rb.AddForce(force, ForceMode.Impulse); // 对刚体施加力 }这里的关键在于即使某一帧图形渲染卡顿了导致Update暂停物理世界仍在后台按照自己的节奏“嘀嗒嘀嗒”地向前模拟。Unity可能会在一帧渲染周期内调用多次FixedUpdate来“追赶”物理模拟进度确保物理时间的流逝与真实时间基本同步。这种稳定性是物理可预测性的基石。实操心得不要试图在Update中通过Time.deltaTime来“模拟”固定时间步长去计算物理量如力、速度。对于刚体操作永远首选FixedUpdate。我曾为了“代码整洁”把一些简单的速度赋值放在Update里结果在低端设备上就出现了物体穿透碰撞体的灵异事件。2.2 时序与执行顺序一帧内的微观世界理解一帧之内Update、FixedUpdate以及物理引擎内部更新的先后顺序对于调试复杂交互至关重要。Unity一帧的主要循环顺序简化如下FixedUpdate调用所有脚本的FixedUpdate方法被调用。物理引擎更新在FixedUpdate调用之后Unity的物理引擎PhysX开始工作。它处理所有在FixedUpdate中施加的力、速度更改然后进行碰撞检测、求解约束并更新所有刚体的位置和旋转。Update调用物理引擎更新完毕后才轮到所有脚本的Update方法执行。动画系统更新可选。渲染最终将场景中的物体状态绘制到屏幕上。这个顺序揭示了几个关键点在FixedUpdate中施加力在本帧的物理更新中就能生效。你可以在FixedUpdate里AddForce然后紧接着在同一帧的物理更新阶段就能看到刚体因此开始运动。在Update中读取的刚体位置/速度是经过当前帧物理更新后的最新状态。所以如果你想在每帧渲染前根据最新的物理位置更新其他逻辑比如更新血条位置放在Update里是合适的。在Update中修改刚体属性要等到下一帧的FixedUpdate周期后的物理更新才会被处理。这引入了一帧的延迟对于需要即时反馈的操作如角色跳跃可能产生手感上的迟滞。private Rigidbody rb; private bool jumpRequested false; void Update() { // 在Update中检测输入响应迅速 if (Input.GetKeyDown(KeyCode.Space)) { jumpRequested true; } } void FixedUpdate() { // 在FixedUpdate中执行物理操作保证稳定性 if (jumpRequested) { rb.AddForce(Vector3.up * 10f, ForceMode.VelocityChange); jumpRequested false; // 重置请求 } }这是一种常见的“请求-执行”模式兼顾了输入的即时性和物理的稳定性。2.3 适用场景与典型误用根据它们的特性其适用场景泾渭分明。FixedUpdate 的绝对领域任何对 Rigidbody 或 Rigidbody2D 的操作AddForce,AddTorque,velocity,angularVelocity的赋值MovePosition,MoveRotation。与物理状态直接相关的逻辑判断例如检测刚体是否处于地面通常通过射线检测或碰撞检测这类检测需要与物理步调一致否则会出现“脚已离地却还能跳”的Bug。需要确定性模拟的游戏比如台球、赛车游戏、物理谜题。这些游戏要求每次运行的物理结果尽可能一致不受帧率波动影响。Update 的主场处理用户输入Input.GetKeyDown,Input.GetMouseButton等。这些输入是离散事件需要即时响应放在Update中能获得最佳手感。非物理对象的变换操作transform.Translate,transform.Rotate操作非刚体物体。计时器、状态机更新使用Time.deltaTime进行与帧率无关的时间累计。相机控制除非是物理跟随相机。UI更新根据游戏状态更新血条、分数显示等。典型误用与后果在Update中直接修改刚体速度如前所述会导致物理行为因帧率而异。高帧率下物体可能获得更高的速度因为Update调用更频繁。在FixedUpdate中处理单次输入事件Input.GetKeyDown可能在两个FixedUpdate调用之间被触发从而被遗漏。因为FixedUpdate调用频率可能低于帧率。在Update中进行需要与物理状态同步的射线检测例如判断角色是否着地。如果检测在Update中进行而刚体位置在FixedUpdate中更新就可能出现检测时角色还未落地或已经弹起的状态误判。2.4 时间参数差异DeltaTime vs FixedDeltaTime时间参数是连接逻辑与真实时间的关键桥梁用错地方会导致速度失控。Time.deltaTime表示上一帧Update到当前帧Update之间的实际时间秒。用于在Update中使运动与帧率解耦。void Update() { // 使旋转每秒90度与帧率无关 float rotation 90f * Time.deltaTime; transform.Rotate(Vector3.up, rotation); }Time.fixedDeltaTime表示固定的物理时间步长秒默认0.02s。在FixedUpdate内部Time.deltaTime的值等于Time.fixedDeltaTime。这是为了在FixedUpdate中也能方便地写出与“物理帧率”解耦的代码尽管物理帧率本身是固定的。void FixedUpdate() { // 在FixedUpdate中以下两种写法在效果上是等价的 // 写法一 rb.AddForce(Vector3.forward * forcePower * Time.fixedDeltaTime); // 写法二 rb.AddForce(Vector3.forward * forcePower * Time.deltaTime); }通常在FixedUpdate中直接使用Time.deltaTime即可因为它已经被Unity自动替换为固定值。但明确使用Time.fixedDeltaTime可以使代码意图更清晰。Time.fixedTime表示从游戏开始到现在经过的固定物理时间秒。类似于Time.time但它是基于FixedUpdate的累计。注意事项永远不要在FixedUpdate中使用Time.deltaTime来试图连接渲染帧的时间除非你非常清楚自己在做什么。反之亦然。混淆它们是新手最常犯的错误之一会导致物理力的大小或运动速度完全错误。2.5 性能影响与帧率补偿机制这是进阶开发者必须关注的层面关系到游戏的流畅度和稳定性。FixedUpdate的潜在性能陷阱如果Fixed Timestep设置得过小比如0.005秒即每秒200次而游戏逻辑又非常复杂可能导致CPU在每一渲染帧需要执行多次FixedUpdate来追赶时间。在极端情况下如果一帧的真实时间超过了多个固定时间步长的总和就会发生“死亡螺旋”物理更新占用了大量时间导致渲染帧更慢进而需要在一帧内处理更多的物理更新来追赶恶性循环游戏卡死。Unity的补偿机制Unity通过Maximum Allowed Timestep最大允许时间步长也在Time设置中来防止这种情况。它规定了一帧中用于处理FixedUpdate和物理更新的最长时间。如果物理模拟落后太多超过了这个最大时间Unity会暂时“放慢”物理世界的时间或者丢弃一些物理更新优先保证游戏的渲染帧能够推进避免完全卡死。但这会导致物理世界的“慢动作”效果。优化建议合理设置Fixed Timestep对于大多数游戏默认的0.02s50Hz已经足够。对于需要极高物理精度如竞技赛车的游戏可以尝试0.01s100Hz但务必充分测试性能。对于节奏较慢的游戏0.033s~30Hz也能接受。精简FixedUpdate中的逻辑只在这里做必须做的事。复杂的AI决策、路径寻找、资源加载等应该移到Update或协程中。监控Time.timeScale当通过Time.timeScale降低游戏整体时间流速时FixedUpdate的调用频率也会等比例降低。这可以用来实现全局慢动作特效。但注意这也会等比例拉长物理模拟的间隔。3. 实战代码剖析从混淆到清晰理论说再多不如一行代码。我们通过一个典型的“玩家控制器”案例来看看如何正确运用这两个函数。3.1 案例一个兼具响应性与稳定性的角色控制器假设我们要制作一个3D角色可以用WASD移动按空格键跳跃。错误示范混合在Update中public class FlawedPlayerController : MonoBehaviour { public float moveSpeed 5f; public float jumpForce 7f; private Rigidbody rb; private bool isGrounded; void Start() { rb GetComponentRigidbody(); } void Update() { // 错误1在Update中检测地面可能与物理状态不同步 isGrounded Physics.Raycast(transform.position, Vector3.down, 1.1f); // 错误2在Update中直接修改刚体速度 float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); Vector3 move new Vector3(h, 0, v) * moveSpeed; rb.velocity new Vector3(move.x, rb.velocity.y, move.z); // 直接覆盖水平速度 // 错误3在Update中检测跳跃输入并立即施加力 if (isGrounded Input.GetKeyDown(KeyCode.Space)) { rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); } } }这个控制器在高端PC上可能运行良好但在帧波动的移动设备或低配电脑上地面检测会出错移动手感飘忽跳跃响应可能延迟或连跳。正确示范分离关注点public class RobustPlayerController : MonoBehaviour { public float moveSpeed 5f; public float jumpForce 7f; public float groundCheckDistance 1.1f; public LayerMask groundLayer; private Rigidbody rb; private Vector3 moveInput; private bool jumpRequested; private bool isGrounded; void Start() { rb GetComponentRigidbody(); } void Update() { // 1. 在Update中收集输入即时响应 float h Input.GetAxisRaw(Horizontal); // 使用Raw获得无平滑的输入响应更快 float v Input.GetAxisRaw(Vertical); moveInput new Vector3(h, 0, v).normalized; // 标准化防止斜向移动更快 // 2. 在Update中检测跳跃按键离散事件 if (Input.GetKeyDown(KeyCode.Space)) { jumpRequested true; } // 3. 可选在Update中基于最新物理位置更新视觉相关逻辑如粒子效果 } void FixedUpdate() { // 1. 在FixedUpdate中进行可靠的地面检测与物理同步 RaycastHit hit; isGrounded Physics.Raycast(rb.position, Vector3.down, out hit, groundCheckDistance, groundLayer); // 2. 在FixedUpdate中计算并应用移动力保持物理稳定性 Vector3 targetVelocity moveInput * moveSpeed; // 计算当前水平速度与目标速度的差值并施加力来改变它 Vector3 velocityChange targetVelocity - new Vector3(rb.velocity.x, 0, rb.velocity.z); // 使用VelocityChange模式直接改变速度忽略质量手感更直接。也可用ForceMode.Force。 rb.AddForce(velocityChange, ForceMode.VelocityChange); // 3. 在FixedUpdate中处理跳跃请求 if (jumpRequested isGrounded) { // 首先将垂直速度归零确保每次跳跃高度一致 rb.velocity new Vector3(rb.velocity.x, 0f, rb.velocity.z); // 然后施加一个瞬间的冲量 rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); jumpRequested false; // 清除请求 } else if (jumpRequested) { // 如果请求跳跃但未着地可能是空中按了键可以选择忽略或实现二段跳逻辑 jumpRequested false; } } // 可选在OnDrawGizmos中绘制地面检测射线便于调试 void OnDrawGizmosSelected() { Gizmos.color Color.red; Gizmos.DrawLine(transform.position, transform.position Vector3.down * groundCheckDistance); } }3.2 代码关键点解读输入分离将Input.GetKeyDown这类瞬时输入检测放在Update确保不遗漏任何一次按键。将连续的Input.GetAxis值缓存到变量如moveInput中供FixedUpdate使用。物理状态同步检测地面检测Physics.Raycast放在FixedUpdate中确保检测时使用的刚体位置是经过物理引擎最新更新的结果最可靠。力的应用所有AddForce操作均在FixedUpdate中完成。这里使用了ForceMode.VelocityChange它直接改变速度忽略质量对于角色控制器来说通常比ForceMode.Force受质量影响手感更跟手、更可控。跳跃处理采用了“请求-执行”模式。Update中设置jumpRequested标志FixedUpdate中检查该标志并满足条件着地后执行跳跃力施加然后清除标志。这完美解决了输入即时性与物理稳定性的矛盾。速度处理技巧在跳跃前将rb.velocity.y归零是一个重要技巧。这可以防止角色从斜坡边缘起跳时将向下的斜坡速度带入跳跃计算导致跳跃高度不一致。4. 常见问题与高级调试技巧即使理解了原理在实际开发中还是会遇到各种诡异的问题。下面是我总结的一些“坑”和解决之道。4.1 高频问题速查表问题现象可能原因解决方案物体运动“一卡一卡”或抖动在Update中修改Transform同时物理引擎也在FixedUpdate中更新刚体位置两者冲突。确保原则对于带有Rigidbody的物体其位置/旋转应完全由物理引擎驱动。如需移动使用rb.MovePosition/MoveRotation在FixedUpdate中或直接施加力。禁用Rigidbody的Interpolate插值也可能导致视觉抖动通常应设置为Interpolate刚体插值或Extrapolate刚体外推。跳跃有时失灵尤其在帧率低时跳跃检测如射线检测放在Update中与FixedUpdate的物理更新不同步。按键事件可能在两帧FixedUpdate之间被触发。采用“请求-执行”模式。在Update中设置跳跃请求标志在FixedUpdate中执行跳跃力和检测。确保地面检测也在FixedUpdate中进行。物体移动速度在高帧率下更快在Update中使用transform.Translate或直接修改rb.velocity且未乘以Time.deltaTime。或者乘了Time.deltaTime但Update调用更频繁累计位移更大。对于非物理移动在Update中使用transform.Translate时务必乘以Time.deltaTime。对于物理移动将所有速度/力的操作移至FixedUpdate。碰撞检测有时不触发物体速度过快在单次FixedUpdate的物理更新中穿过了另一个碰撞体。这称为“隧道效应”。1. 提高Fixed Timestep频率减少步长。2. 为高速物体如子弹启用刚体的Continuous连续碰撞检测模式。3. 使用Continuous Dynamic连续动态检测用于高速物体与重要静态物体的碰撞。注意这会增加性能开销。FixedUpdate逻辑导致游戏卡顿Fixed Timestep设置过小或FixedUpdate中的逻辑过于复杂导致一帧内需要执行多次CPU不堪重负。1. 适当调大Fixed Timestep如0.033s。2. 优化FixedUpdate内的代码将非物理必要的逻辑移出。3. 使用Profiler窗口的Physics模块查看物理更新耗时。4.2 高级技巧平滑与插值当你直接在FixedUpdate里更新刚体物体的运动在视觉上可能会显得有点“生硬”因为物理更新的频率默认50Hz可能低于渲染频率比如60Hz或更高。这时就需要插值Interpolation。原理插值是在渲染帧之间根据上一物理帧和当前物理帧的状态计算出一个中间状态进行渲染。这样即使物理更新频率较低视觉运动也是平滑的。如何启用在刚体组件上找到Interpolation属性。有三种模式None无插值。物体直接渲染在物理引擎计算出的位置上可能抖动。Interpolate推荐用于大多数运动物体。基于上一物理帧的状态进行平滑。Extrapolate基于当前物理速度和上一帧状态预测当前帧的位置。对于运动方向变化剧烈的物体可能产生“鬼影”。重要提示插值只影响视觉表现不影响物理模拟本身。碰撞检测、力计算等仍然严格在FixedUpdate的节奏中进行。启用插值后你在Update中通过transform.position读取的位置是插值后的视觉位置而非精确的物理位置。如果需要获取精确的物理位置应使用rigidbody.position。4.3 性能监控与Profiler使用怀疑物理系统拖慢游戏必须学会使用Unity Profiler。打开Window - Analysis - Profiler。运行游戏在Profiler窗口选择CPU Usage模块。观察图表找到标为Physics.Processing或FixedUpdate的耗时条。如果它们占用时间过长比如超过每帧时间的10%-20%就需要警惕。可以进一步使用Physics Profiler模块可能需要通过Add Profiler Module添加查看具体的物理操作耗时如碰撞检测、求解器等。优化物理性能的常见手段包括减少动态刚体数量、使用简化碰撞体用Box/Sphere代替Mesh Collider、合理设置碰撞层Layer以减少不必要的碰撞检测、将静止的刚体设为Kinematic或Static。理解Update和FixedUpdate本质上是理解Unity如何协调“视觉表现”与“物理模拟”这两个不同节奏的世界。掌握它们的分工与协作你的游戏在物理交互上就迈出了从“能用”到“扎实可靠”的关键一步。记住这个黄金法则输入与视觉反馈走Update力与物理状态走FixedUpdate。多实践多调试很快你就能凭直觉写出稳定高效的物理代码了。