拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Unity物理引擎核心机制解析:从FixedUpdate到刚体碰撞与性能优化

刚体组件拖上去、碰撞体加上、场景一跑——东西穿模掉下去了或者物体抖动得跟筛子似的。这种问题在Unity物理开发里太常见了根源多半不是“忘了加组件”而是没搞懂物理引擎背后那套时间步长、碰撞检测和约束解算的运作逻辑。这篇把Unity物理引擎里刚体、碰撞和力的底层机制串一遍从FixedUpdate的真实用途讲到ForceMode四种模式怎么选再落到几百个刚体场景的性能优化适合刚接触物理系统的新手也适合做了几个项目但老在“为什么卡顿/穿透/乱弹”上翻车的中级开发者。1. 物理时钟FixedUpdate和Time.fixedDeltaTime才是物理世界的秒针1.1 为什么物理计算不放在Update里很多刚接触Unity的人天然会用Update推动物体结果发现物体要么忽快忽慢要么抖动要么穿透。原因在于Update的调用频率不是固定的它跟当前帧率挂钩——屏幕刷新快Update就多跑几次刷新慢就少跑几次。物理引擎需要的是稳定、确定性的时间推进每次物理步进跨过的时间窗必须一致否则碰撞检测、速度积分、约束解算全都会产生误差。物理系统的那根“秒针”是FixedUpdate。Unity默认的Time.fixedDeltaTime是0.02秒也就是固定时间步长20毫秒意味着物理系统每秒最多模拟50次。你在Update里写transform.position direction * speed * Time.deltaTime只是单纯在渲染层移动物体但刚体的速度、受力、碰撞都是靠物理引擎在FixedUpdate阶段推演的。有一个特别值得注意的细节当游戏帧率低于物理频率时比如帧率掉到30FPSUnity会在单帧内多次调用FixedUpdate来补足物理模拟次数保证物理世界的时间不会“拖欠”。这也是为什么物理相关的逻辑必须写进FixedUpdate而不是Update——如果你在Update里给刚体加力实际加力的密度会随帧率波动直接导致物理表现不稳定。1.2 Time.timeScale对物理世界的影响Time.timeScale会同时影响Update和FixedUpdate的执行频率。timeScale调成0之后Update和FixedUpdate都会停止整个游戏逻辑和物理模拟全部冻结。这一点在实现“暂停菜单”时特别关键——如果你只想暂停游戏逻辑而不想冻结物理效果比如暂停时让粒子继续飘不能简单把timeScale设成0需要单独控制物理模拟的开关或用自定义的暂停系统。对于需要精确复现物理过程比如赛车游戏回放、台球比赛回放的项目还可以考虑手动控制Physics.autoSimulation把它设为false后物理引擎不再自动步进改由你在合适时机调用Physics.Simulate(Time.fixedDeltaTime)手动推进。这样能做到物理模拟完全可控回放过程中每一帧都在精确复现当时的物理状态。1.3 物理插值解决“物理更新慢、渲染快”造成的抖动固定时间步长20毫秒对应的物理帧率是50FPS但显示器往往是60Hz、120Hz甚至144Hz。物理引擎每更新一次渲染层可能已经跑了好几帧如果直接用物理引擎给出的最新位置去渲染就会出现物体一顿一顿的现象。解决这个问题靠刚体组件上的Interpolate和Extrapolate。Interpolate插值把上一帧物理位置和当前物理位置做平滑过渡推荐绝大多数情况使用。Extrapolate外推按当前速度推测未来位置适合极高速运动的物体但预测错误时会表现出轻微漂移。实际使用中我基本只会选Interpolate它在低速和高速场景下表现都足够平滑。Extrapolate看起来“反应更快”但速度突变时位置容易跳变除非有特殊需求否则别随便开。2. 刚体的核心参数不只是“重量”这么简单2.1 Mass、Drag、Angular Drag的真实含义刚体的Mass不是“多重”的概念而是决定受力后加速度大小的系数。根据F ma同样大小的力作用在质量更大的物体上获得的加速度反而更小。质量会参与碰撞时的冲量交换计算两个质量悬殊的物体碰撞时质量小的那个会获得更明显的速度变化。Drag阻力和Angular Drag角阻力模拟的是空气或其他介质对线性运动和旋转运动的衰减单位不好用现实物理去套Unity内部做了数值调整。默认值0代表没有阻力。给高速运动物体加阻力可以快速限速但阻力过大时物体可能还没到达目标位置就停下来了这个参数通常用来做“速度阻尼”而不是模拟真实空气阻力。有一个实用技巧控制角色移动时如果直接用AddForce角色的手感会有“慢慢加速再慢慢减速”的粘滞感。更常用的做法是直接把rb.velocity赋值给目标速度然后靠rb.drag来制造自然的减速效果既能保持物理引擎的碰撞响应又能让角色移动跟手。2.2 Constraints和Sleeping ModeConstraints用来冻结刚体某个轴上的位置或旋转变化。做布娃娃、物理门、掉落物时冻结不需要的轴能节省解算开销同时避免物体在某个方向上产生奇怪的漂移。Sleeping Mode更关键。刚体在同一位置不动且速度低于一定阈值一段时间后物理引擎会自动把它标记为“睡眠”状态不再参与碰撞测试和约束解算极大地节省CPU开销。默认的Auto模式在大多数情况下是正确的但有一个坑如果物体被飞来的其他刚体撞到睡眠状态会被自动唤醒而如果两个睡眠中的刚体互相重叠放置它们可能永远检测不到对方的存在表现成“穿模”。遇到这类问题时可以先检查两个物体是否都被睡眠了。2.3 Collision Detection三种模式的选择刚体上的Collision Detection参数决定碰撞检测的精确程度模式适用场景说明Discrete低速、体积较大的物体默认模式性能最优但快速移动时容易穿透Continuous高速物体碰撞静态碰撞器对静态环境做扫掠检测能避免大部分穿透Continuous Dynamic高速物体碰撞其他刚体连续检测两个运动刚体之间的碰撞开销最高Speculative需要更高性能的连续检测基于推测的连续碰撞检测性能优于Continuous Dynamic会有一定误差如果你在射击游戏里做子弹子弹速度达到每秒几十米甚至上百米直接上Continuous都有点不够。更可靠的做法是加上Physics.Raycast做手动射线检测子弹每帧先朝运动方向发射一条射线看看射线是否命中了什么再决定是否触发命中逻辑。不要把子弹当成纯刚体来推物理引擎的连续检测在极端速度下也不是绝对可靠。3. 碰撞体系的底层Collider、碰撞事件与触发器3.1 碰撞器类型怎么选Unity提供了多种Collider其中Box、Sphere、Capsule带来的性能开销远低于Mesh Collider。Mesh Collider的碰撞检测是按三角形网格逐个做的开销巨大而且对非凸形状比如一个带凹坑的石头会产生错误的碰撞法线。实际项目里做地形、建筑这种静态环境才考虑用Mesh Collider而且尽量把它标记为Convex凸包——一旦勾选Convex引擎会用一个简化的凸包包住网格碰撞体性能会大幅改善代价是形状精确度下降。日常开发最常用的组合是“多个基本碰撞体嵌套”。比如一个复杂道具用网格模型做显示但碰撞体挂好几个Box Collider/Capsule Collider组合拼接。控制角色、子弹、拾取物这些需要高频交互的物体这个方法远比一个逼真的Mesh Collider高效。3.2 碰撞事件的生命周期与触发条件两个物体要触发OnCollisionEnter至少要满足两条双方至少一个带有刚体如果两个都是静态碰撞器根本不会产生碰撞事件。双方的碰撞器都设置为非触发器Is Trigger不勾选。事件顺序上OnCollisionEnter只会触发一次OnCollisionStay在持续接触时每物理帧触发一次OnCollisionExit在分离时触发。值得注意的一个开发陷阱OnCollisionStay的调用频率等于物理频率默认50次/秒如果你在Stay里做伤害累计或者音效播放须用累计时间或事件去重否则同一碰撞会连续触发几十次。用触发器Is Trigger勾选则不会受到物理碰撞的反作用力影响只用来做“区域检测”或“门禁”。但触发器事件是OnTriggerEnter、OnTriggerStay、OnTriggerExit别跟OnCollision搞混。如果你在调试时发现自己的碰撞事件死活不触发优先检查是不是用了Trigger但监听的是OnCollision事件或者两个物体都勾选了Is Trigger导致没有任何碰撞响应。3.3 别在生产环境里读取Transform.position刚体物理系统的权威数据源是Rigidbody.position和Rigidbody.rotation而不是Transform.position和Transform.rotation。物理引擎在内部对刚体做积分、约束求解后会把结果写回Transform这个回写时机和渲染同步没有严格对齐。如果你在Update里读Transform.position拿到的很可能是物理引擎上一次写回的旧值甚至是被插值后的中间值最终会跟物理世界的真实位置产生偏差。正确做法是把依赖刚体位置的操作比如子弹命中检测、AI追目标放在FixedUpdate里读取rb.position。需要修改刚体位置时使用rb.MovePosition和rb.MoveRotation而不是直接改Transform因为MovePosition会让物理引擎把这次位置变化当作运动学行为来处理能保证碰撞检测仍然有效。如果直接改transform.position相当于把物体“瞬移”过去物理引擎的碰撞检测会错过中间的碰撞路径极端情况下物体会瞬间穿过墙壁。4. 物理材质也重要摩擦、弹力和接触面的微妙关系4.1 Physic Material参数回顾一个Physic Material文件由以下核心参数组成Dynamic Friction动摩擦物体滑动时产生的摩擦系数。Static Friction静摩擦物体从静止到开始运动需要克服的摩擦系数通常比动摩擦大所以推一个重箱子比推一个动起来的箱子更费力。Bounciness弹性碰撞后相对速度的保持程度0表示完全没有反弹1表示完全弹性碰撞。比较容易被忽略的是“Combine”组合模式。两块物体接触时各自的摩擦/弹性如何合并计算由Friction Combine和Bounce Combine决定可选Average、Minimum、Maximum、Multiply。实际项目里把地面摩擦设为Maximum、把角色摩擦设为Minimum会遇到很多难缠的抖动问题。我建议的做法是大部分物体都用Average特殊需求比如冰面、跳床单独做材质再手动调Combine。4.2 为什么摩擦会让物体“卡在”斜坡上物理引擎的摩擦约束本质上是基于接触点的冲量约束。当一个刚体放在斜坡上重力沿坡面方向的分量小于静摩擦产生的临界力时物体会保持静止。听起来很合理但实际工程里有个常见问题斜坡加载时刚体已经在坡面内部或者接触面刚好贴合在边缘上法线计算产生微小偏差导致物体莫名其妙开始滑动或抖动。要规避这类问题尽量让斜坡的底部多预留一小段水平面避免物体刚好停在坡道的临界点上。同时给斜坡加摩擦系数较高的材质降低滑动的可能性。4.3 弹力反弹的“阈值”机制Bounciness不是反弹高度为1就真的完全弹性。物理引擎对反弹速度有一个bounce threshold默认值为2单位m/s的控制——当碰撞法线方向的相对速度低于这个阈值时引擎不执行反弹计算表现为没有弹起。这个设计是为了避免物体在微小碰撞中无限弹跳。如果你做一个弹球游戏发现球碰到地面后不弹了方向应该是检查碰撞速度够不够大而不是先怀疑Bounciness设置有问题。5. 力的应用AddForce和ForceMode的四种模式到底怎么选5.1 Force、Acceleration、Impulse、VelocityChangeRigidbody.AddForce是物理系统最常用的接口但它有个容易理解错的地方ForceMode参数决定了这个“力”是以什么单位结算的。ForceMode含义单位适用场景Force连续力牛顿持续推物体、火箭推力Acceleration加速度m/s²持续改变速度忽略质量Impulse冲量牛顿·秒瞬间爆发力比如射击后坐力VelocityChange速度变化m/s直接修改速度忽略质量最直接在很多游戏代码里AddForce(..., ForceMode.Impulse)被用来做“点一下蹦一下”的操作但这个叫法容易让人误以为“和给个初速度等效”。实际上Impulse还会参与质量计算同样大小的冲量作用在重物体上速度变化会小而VelocityChange则完全无视质量——两个对象质量差距悬殊时用Impulse能做出更符合物理直觉的表现用VelocityChange则会让重物和轻物跑得一样快。5.2 相对力、力矩与受力点在开发时经常遇到的另一个需求是沿刚体自身坐标方向施力。AddRelativeForce正是为此设计的它把力方向从物体的局部坐标系转换到世界坐标系后再施加。而如果想制造旋转效果得用AddTorque或AddForceAtPosition。AddForceAtPosition会在刚体某个局部位置上施加一个产生力矩的力这个力会把物体“推歪”而不是单纯平移。这里有个实战经验如果你要给“爆炸”效果做物理表现AddForceAtPosition加ForceMode.Impulse是个经典组合——把冲击点设置在爆炸中心到物体质心之间物体会同时获得向外移动和翻转的动感。如果只用AddForce所有物体都只会向外平移效果会显得很“程序化”不够真实。5.3 重心重定义物理表现的魔术开关刚体的centerOfMass参数默认由碰撞体自动计算但你可以手动覆盖。把重心调整到模型几何中心下方会让物体更难翻倒。做载具类项目时这是一种常见的手段把重心压得很低让车辆在转弯时能自然地先侧倾再回正而不需要写大量旋转修正代码。更进阶的用法是改动重心位置实现“漂移”——把重心放到前轴附近车辆就会更倾向于甩尾。这个参数在运行时动态调整也不会引发大量性能开销非常适合做载具手感调校。5.4 关于力的叠加和帧率依赖在使用AddForce做持续力Force模式时引擎会根据fixedDeltaTime自动把力的效果累积到速度上所以每次调用的力度和时间步长无关不需要你自己乘Time.fixedDeltaTime。但如果你在Update里调用AddForce由于Update的调用频率和FixedUpdate不一致实际施力频率是随帧率波动的——这也是为什么始终建议把物理相关调用放在FixedUpdate里。6. 场景性能优化几百个刚体怎么保持稳定6.1 睡眠机制是第一生产力物理引擎最大的性能杀手不是刚体数量而是“活跃”刚体数量。一个不动的刚体会在短暂时间后进入睡眠状态完全停止参与物理运算。要让系统自动触发睡眠物体必须在连续一段时间内保持速度极低且不受任何外力干扰。在实现掉落物、碎石、弹壳这类临时物理物体时可以在它们碰撞稳定后调用rb.Sleep()主动进入睡眠再逐步把碰撞体置为不可交互但是注意主动Sleep后再被其他刚体撞击时引擎不会自动唤醒。更好的方式是设置rb.sleepThreshold让引擎根据你的项目节奏自动判断何时该睡。6.2 善用Layer和碰撞矩阵Unity的物理碰撞矩阵位于Project Settings Physics Layer Collision Matrix通过它你可以精确控制哪些Layer之间发生碰撞、哪些Layer之间忽略碰撞。比如玩家子弹和玩家角色之间禁用碰撞AI敌人之间禁用碰撞能省掉大量无意义的碰撞检测计算。用Layer做碰撞过滤还有一个好处它影响的是整个物理过程不只是渲染层敌我子弹、队友、掉落物之间的相互作用可以被整体隔离避免许多隐性问题。6.3 避免频繁实例化和销毁刚体需要大量生成和回收刚体的场景比如射击游戏弹壳不要频繁Instantiate和Destroy最好用对象池。原因在于实例化刚体时物理引擎要创建新的碰撞体数据销毁时又要移除它这个过程的开销比渲染层大得多。一个成熟的方案是池子里预先创建大量刚体并设为睡眠状态需要时唤醒、设置初始位置和速度用完再睡回池子里。6.4 把静态碰撞体做进Bake很多开发者把没有任何逻辑的装饰物挂上静态碰撞器后就不管了但物理引擎会为每个静态碰撞器做加速结构几千个Box Collider会自动被物理引擎构建成BVH包围体层次结构查询效率尚可但仍有开销。如果你的场景里有一整面墙是几十个箱子拼出来的把它们的Mesh合并成一个Mesh Collider能直接少几十个碰撞体。这个优化手段在移动端特别有效能显著减少物理查询耗时。7. 实战排错穿透、抖动、乱弹的排查顺序7.1 物体快速移动穿过墙体遇到高速穿墙时先检查Collision Detection是不是Discrete如果是改成Continuous或Continuous Dynamic。其次要检查物体本身的运动方式——如果是在Update里直接改transform.position物理引擎根本不知道它的运动路径改多少都白搭。正确做法是用rb.MovePosition、rb.velocity或AddForce驱动运动。如果速度实在太快连续检测也力不从心最稳妥的方案是手动做射线检测每帧从物体当前位置向运动方向发一条射线如果射线在移动距离内撞到东西就把物体拉到碰撞点附近避免穿透。7.2 多个刚体互相推挤导致的永久抖动一堆刚体堆叠在一起时约束求解器可能在每帧反复调整位置导致物体看起来像“活物”一样抖动。首先检查是不是用了过大的Physics.defaultSolverIterations迭代次数越多越稳定也越耗性能。其次检查物体的质量比——如果所有物体的质量都一样堆叠时会产生较多不必要的解算复杂度让最底层的物体质量更大、顶部的物体质量更小能明显减少堆叠抖动。还有一个经常被忽略的原因物体接触时如果你的Update逻辑里持续用AddForce给某个物体施力但它又和另一个刚体直接接触这个外力会让求解器无法收敛到稳定状态从而产生持续抖动。排查时先把所有外部持续力关掉观察是否恢复稳定。7.3 碰撞器之间的“缝隙”陷阱非均匀缩放的碰撞器比如x轴拉长3倍、y轴不变、z轴缩小到0.2倍在技术文档中被明确警告过。物理引擎在处理非均匀缩放的碰撞器时会出现法线偏差导致物体卡在缝隙里或表面上打滑。如果一个刚体模型需要非均匀缩放最简单的做法是把碰撞器重新搭成基本碰撞体组合而不是依赖Mesh Collider自动生成。如果物体还是卡在缝隙里可以尝试调小碰撞器的尺寸或调大Physics.defaultContactOffset默认值0.01。这个参数决定物体接触时保留的最小间隙把这个值稍微调大一定程度上能缓解缝隙卡顿但调太大了会让物体看起来“悬浮”在表面上。7.4 用Physics Debugger可视化排查Unity自带的Window Analysis Physics Debugger是排查物理问题的利器。它能实时显示刚体的速度、受力、睡眠状态还能高亮当前活跃的碰撞器和触发区域。遇到莫名其妙的物理行为时先打开这个面板看看物体是不是没睡、是不是碰撞器位置跟模型错位、是不是某个隐形的碰撞器在捣乱——很多时候问题一眼就能看出来。8. 一些实战中的取舍与个人经验先说一个原则别把物理引擎当成万能的。Unity物理适合做“看起来对”的实时模拟而不是精确的物理仿真。如果你的需求是机械装置、关节受力分析、多体动力学的精确模拟那应该考虑专业的物理引擎或计算库比如Mujoco或者直接上仿真工具。项目中常见的是把Unity物理用于游戏表现而不是工程计算。另一个经验是“尽量少依赖物理系统的精细调参多用逻辑去规避”。比如做平台跳跃直接给角色一个固定跳跃速度比依赖弹力系数和一串摩擦力参数稳定得多。物理引擎的摩擦和弹力受很多因素影响材质组合、接触面法线、求解迭代次数调参容易出现“调一个参数带崩另一个效果”的情况。反过来把决定性手感用逻辑控制好物理只负责碰撞响应和表面表现会让项目稳定很多。关于刚体移动方式的选择我的习惯是需要物理碰撞的物体用rb.MovePosition或rb.velocity需要完全逻辑控制的物体比如UI动画、非物理粒子直接操作Transform。两种方式混用的时候一定不要让物理驱动和Transform驱动同时作用在同一个刚体上否则会出现“引擎说往左你手动往右”的冲突最终表现为完全不可控的抖动和随机弹跳。最后想特意提一下移动端性能。移动设备CPU性能有限物理引擎的模拟能力大打折扣。手机上尽量减少同时活跃的刚体数量减少Mesh Collider的使用非必要的刚体全部设为Sleep用Layer Collision Matrix把碰撞范围收窄。我自己做过一个移动端休闲游戏场景里有约200个可交互物体但同一时刻活跃的刚体数量控制在30以内依然能保持60帧不卡顿——核心就是睡眠、碰撞过滤和对象池这老三样的组合。如果你在工作中遇到物理表现奇怪的问题别急着怀疑Unity的物理引擎有bug先从时间步长、运动驱动方式、碰撞检测模式、碰撞矩阵这几层去逐层排查。大部分所谓“物理bug”其实都是物理系统被错误使用的方式逼出来的。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门