
在 Unity 里做交互绕不开两个组件Rigidbody刚体和 Collider碰撞体。一个是让物体听话地动起来、受重力影响、被力推动的“身体”另一个是决定这个身体长什么形状、能不能和别人撞上的“皮肤”。很多刚上手 Unity 的朋友会把它们混为一谈觉得加了 Collider 物体就会掉落或者加了 Rigidbody 就一定能触发碰撞回调结果在项目里踩一堆坑。这篇内容适合三类人刚学 Unity 想做物理交互的新手、被穿模和抖动折磨过的中级开发者、以及要在一体机或 WebGL 这类性能吃紧的平台上做物理方案的人。我会把刚体和碰撞体拆开讲透补上参数怎么算、坑怎么排、性能怎么抠尽量给你能直接抄作业的配置和代码。1. 刚体与碰撞体到底在解决什么问题1.1 物理引擎替我们干了哪些活Unity 的物理系统底层用的是 NVIDIA PhysX它本质上是一个固定步长运行的解算器。你可以把它想成一群“看不见的裁判”每隔一个固定时间就检查一次场上所有带刚体的物体位置在哪、速度多少、有没有互相压在一起、压多深、该给多大的反作用力。这些计算都不是在 Update 里做的而是在 FixedUpdate 对应的物理帧里完成。默认情况下物理帧率由 Project Settings Time Fixed Timestep 控制出厂值是 0.02 秒也就是每秒 50 次解算。这个数字为什么重要因为它直接决定了物理的稳定性和开销。把 Fixed Timestep 调小比如 0.01100Hz解算更细腻高速物体不容易穿透但 CPU 开销翻倍调大到 0.03330Hz性能省下来了可是一些快速移动的物体就会“一步跨过”薄墙。我在做一个抓取类的 XR 项目时手柄甩动速度很快0.02 的步长下小球偶尔会穿过手掌碰撞体最后是把它改到 0.011 并配合连续碰撞检测才彻底稳住。还有一个容易被忽略的参数是 Maximum Allowed Timestep默认 0.333。它是个保险丝如果某一帧卡顿太久物理追赶的时间超过这个上限就直接放弃追赶避免“死亡螺旋”——物理积压越来越多导致越来越卡。低端设备上出现过掉帧后物理彻底失控的情况多半就是这里没处理好。1.2 Rigidbody 与 Collider 的分工边界一句话说清分工Collider 定义“形状”Rigidbody 定义“谁会动”。没有 Rigidbody 的 Collider 是静态碰撞体它在物理世界里永远不动也几乎不消耗动力学解算开销只负责被别人撞。而 Rigidbody 负责把物体注册进物理世界让它受重力、受施加的力、被碰撞反弹。这意味着两件事。第一只加 Collider 不加 Rigidbody物体悬在空中不会掉因为没人告诉物理引擎它该动。第二两个都没加 Rigidbody 的静态碰撞体互相接触不会产生任何 OnCollisionEnter 回调——很多人第一次写碰撞检测就卡在这反复检查代码其实是配置问题。注意Unity 2020 之后的版本里两个静态碰撞体接触确实不会触发任何碰撞消息这是设计如此不是 Bug。想让回调跑起来至少一方得带 Rigidbody或者使用 CharacterController 这类特殊组件。还有一个反直觉的点碰撞体的形状和渲染用的 Mesh 是两套东西。你导入一个精细的角色模型Mesh 有上万个三角面但碰撞体完全可以只是一个胶囊或者几个盒子拼起来的复合体。物理只认 Collider不认 Mesh Renderer。这也是为什么“看起来撞上了却没反应”或者“明明没接触却撞飞了”经常发生——两者的轮廓不一致。2. 刚体组件的参数逐项拆解与调参思路2.1 质量、阻尼与重力三个最容易拍脑袋填错的参数打开 Rigidbody 组件第一眼看到的 Mass 很多人随手填个 1 就完事。这里有个前提要先建立Unity 默认 1 个世界单位等于 1 米Mass 的单位是千克。所以一个 Mass 为 1 的立方体质量相当于 1 公斤体积却有 1 立方米密度接近泡沫。这个默认值会导致体感很“飘”。我做推箱子的玩法时把角色质量设成 70接近成年人箱子设成 15 到 30推动的手感立刻正常了因为冲量传递的比例符合直觉了。Drag线性阻尼和 Angular Drag角阻尼控制的是速度衰减。Drag 为 0 意味着物体永远匀速运动现实中不存在这种情况。Drag 的作用不是简单的“阻力”而是每帧按比例削减速度数值越大衰减越快。Angular Drag 默认 0.05这个值其实挺关键它让滚动的球慢慢停下来。如果你把 Angular Drag 也设成 0一个球在平地上能滚到你怀疑人生。重力这块Use Gravity 勾上才会受全局重力影响全局重力在 Project Settings Physics 里配置默认 (0, -9.81, 0)。有些项目想要卡通手感会把重力改成 -20 甚至 -30跳跃就变得干脆利落但要注意重力一改所有依赖质量的力都要重新调比如跳跃初速度需要按 h v²/(2g) 反推。举个具体的想让角色跳起 1.2 米高重力是 -20 时需要的起跳初速度 v √(2×20×1.2) ≈ 6.93 m/s。如果还按重力 -9.81 时算出来的 4.85角色就只能跳 0.6 米左右手感会差很多。参数默认值调参建议常见误区Mass1按真实量级给角色 60-80箱子 10-30全填 1 导致推挤手感像纸片Drag0空气阻力场景 0.02-0.5填太大导致物体像在泥里Angular Drag0.05球体 0.05-0.3硬表面保持默认设 0 导致无限滚动Use Gravity开启悬浮类物体关闭关闭后 AddForce 依然生效Is Kinematic关闭平台、门等脚本控制物体打开打开后不再受力和重力2.2 插值、碰撞检测与约束解决抖动和穿模的三把钥匙插值是新手最容易忽略、但对视觉体验影响最大的选项。物理在 50Hz 跑渲染可能跑 60、90 甚至 120 帧。如果两者频率不一致物体在屏幕上就会一格一格地跳这就是抖动。设成 InterpolateUnity 会拿上一物理帧和当前物理帧的位置做插值画面立刻顺滑Extrapolate 则是预测下一物理帧的位置理论上更平滑但快速转向时可能出现过度预测的“回弹”。我一般优先选 Interpolate稳定压倒一切。碰撞检测模式是解决穿模的核心。Discrete 是默认值只在当前帧位置做碰撞检测速度太快就直接跳过墙体。Continuous 针对该物体做连续检测能有效防止自身穿透但不会阻止别人穿它。Continuous Dynamic 则是既能防自己穿别人也能防高速物体穿自己代价是 CPU 开销更高。Continuous Speculative 用预测方式性能比 Continuous Dynamic 好一些在 Unity 2018.3 之后才有。实际选择我有套朴素标准玩家角色、可推动的箱子这类“主角周边”物体一律 Continuous Dynamic普通场景道具 Discrete 足够单纯想防穿透又能接受一点性能损耗的Speculative 是个不错的折中。曾经做过一个弹球游戏小球初速 30 m/sDiscrete 下每次撞墙都直接飞出去换成 Continuous Dynamic 后再没出过问题。Constraints约束用来锁死某些自由度这在做 2.5D 游戏时特别实用。想做一个只能在 X 轴和 Y 轴平面移动的角色直接 Freeze Position Z 加上 Freeze Rotation X、Y物体就再也不会歪着倒下。这比在代码里每帧强行归一化旋转要省事得多也不会破坏物理解算的连续性。3. 碰撞体家族选型从性能表到场景适配3.1 原始碰撞体与网格碰撞体的成本对比Collider 的性能排序大致是Sphere ≈ Capsule Box Convex Mesh Non-Convex Mesh。差异有多夸张Sphere 和 Capsule 的相交检测是纯数学公式几纳秒级别而一个非凸 Mesh Collider 需要构建 BVH 树逐面检测数量一多就是灾难。所以我的原则是能用原始碰撞体凑出来就别用 Mesh Collider。一个椅子可以用一个 Box 做坐垫、四条细 Capsule 做腿一个角色用 Capsule 做身体、Sphere 做头再加几个小 Box 做武器。这种复合碰撞体父物体带 Rigidbody子物体挂 Collider在物理世界里会被自动合并成一个刚体性能好还便于调整。Mesh Collider 也不是不能用但必须开 Convex 才能和非静态碰撞体交互。开 Convex 后它会被简化成凸包顶点数量有上限官方限制是 255 个顶点模型太复杂就会被砍掉细节所以往往需要单独做一份碰撞用的低模。我一般让美术把碰撞模型面数控制在 200 面以内命名成 xxx_collider导入时关掉 Mesh Renderer只留 Mesh Collider。Terrain Collider 属于特殊存在它由地形数据生成专用于地形。它的开销跟地形精度挂钩不建议在一个场景里叠很多。Wheel Collider 是载具专用带悬挂和摩擦模型做赛车必备但它的调参曲线比较陡新手建议先看官方 Car 示例。3.2 触发器、物理材质与层碰撞矩阵的配合把 Collider 的 Is Trigger 勾上它就变成触发器物理上不再产生阻挡和反弹但会触发 OnTriggerEnter / Stay / Exit。触发器常用来做拾取、检测区域、关卡触发线。这里有个经典坑触发器要生效必须有 Rigidbody 参与。只有 Collider Is Trigger另一个也是静态 Collider两者相对静止时不会触发。解决办法是给移动的一方加 Rigidbody勾上 Is Kinematic 也行或者用刚体驱动。物理材质Physics Material管的是摩擦和弹性包含 Dynamic Friction、Static Friction、Bounciness 以及两个 Combine 模式。这里最容易翻车的是 Friction Combine。假设地面材质 Average、角色平均摩擦 0.6那实际就是 0.6但如果地面设成 Minimum取两者最小值可能就变成 0.1角色立刻像踩了冰。做冰面关卡我就是靠把地面材质的 Friction Combine 设成 Minimum 加低摩擦值来实现的比改代码优雅得多。Bounciness 和 Bounce Combine 同理。想让球越弹越低直到停下Bounciness 给 0.6 到 0.8如果用 Maximum 会让弹性一直不衰减得配合 Drag 使用。实测中 Bounciness 大于 0.9 且 Bounce Combine 为 Maximum 时球可能因为能量注入而越弹越高这是数值问题不是灵异事件。层碰撞矩阵Layer Collision Matrix在 Project Settings Physics 里是性能优化的第一把刀。把不相关的层互相取消勾选物理引擎连宽相位检测都省了。一个典型场景有玩家、敌人、子弹、可破坏物、地面如果子弹只跟敌人和可破坏物碰撞把其他层全部取消几十上百发子弹的开销能降一大截。4. 从零搭建一个物理交互场景的完整实操4.1 场景搭建与脚本落地我们来搭一个能直接跑的最小场景一个地面、一个会掉落的箱子、一个负责推箱子的玩家胶囊。地面用 Plane挂 Box Collider 或者用自带 Mesh Collider 都行它不带 Rigidbody保持静态。箱子加 Box Collider 和 RigidbodyMass 设 20Drag 设 0.1Angular Drag 保持默认碰撞检测设 Continuous Dynamic。玩家胶囊加 Capsule Collider 和 RigidbodyMass 设 70锁定 X、Z 轴旋转防止倒地。这里有个细节值得说玩家如果直接靠 Rigidbody 的物理模拟去推箱子容易有“打滑”“推不动”“推着推着自己飞了”的情况。主流做法是用 Rigidbody 的 MovePosition 配合速度控制或者干脆用 CharacterController。我这里演示用 Rigidbody 的方式因为它更能说明物理参数的作用using UnityEngine; [RequireComponent(typeof(Rigidbody))] public class Pusher : MonoBehaviour { [SerializeField] private float speed 4f; [SerializeField] private float acceleration 40f; private Rigidbody rb; private Vector3 inputDir; private void Awake() { rb GetComponentRigidbody(); rb.mass 70f; rb.drag 6f; rb.angularDrag 8f; rb.constraints RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ; rb.interpolation RigidbodyInterpolation.Interpolate; rb.collisionDetectionMode CollisionDetectionMode.ContinuousDynamic; } private void Update() { float h Input.GetAxisRaw(Horizontal); float v Input.GetAxisRaw(Vertical); inputDir new Vector3(h, 0f, v).normalized; } private void FixedUpdate() { Vector3 targetVel inputDir * speed; Vector3 delta targetVel - new Vector3(rb.velocity.x, 0f, rb.velocity.z); rb.AddForce(delta * acceleration, ForceMode.Acceleration); } }这段代码有几个讲究。第一输入在 Update 里读力在 FixedUpdate 里施避免输入采样频率和物理频率不匹配。第二用 ForceMode.Acceleration 而不是 Force这样推动效果不受质量影响加速模式忽略质量方便调参。第三Drag 给了 6是因为用加速度模式时如果不给阻尼松手后角色会一直滑6 是我实测下来既跟手又不会瞬间刹死的值。接下来加一个碰撞反馈用来观察真实的接触点和法线方向private void OnCollisionEnter(Collision collision) { Debug.Log($撞到 {collision.gameObject.name}相对速度 {collision.relativeVelocity.magnitude:F2}); foreach (ContactPoint cp in collision.contacts) { Debug.DrawRay(cp.point, cp.normal * 0.5f, Color.red, 2f); } }ContactPoint 里的 normal 就是碰撞法线做踩踏检测、判断撞到的是墙还是地面全靠它。比如判断角色是否落地可以检查接触点法线的 y 分量是否大于 0.7比单纯判断 tag 更靠谱因为它能识别斜坡。4.2 参数计算与验证方法参数不能拍脑袋算一遍心里才有底。以跳跃为例如果希望角色 0.4 秒到达最高点、跳起 1 米用 v g × t/2 和 h g × t²/8 这组关系可以得到 g 8h/t² 8×1/0.16 50v 50×0.2 10。也就是重力 -50、起跳速度 10 能实现这个手感。这个重力明显大于现实但游戏手感往往就是“超现实”的马里奥式的跳跃没几个是真实物理。冲量传递也能算。质量 70 的角色以 4 m/s 撞上质量 20 的箱子如果完全弹性碰撞箱子速度约是 2×70/(7020) × 4 ≈ 6.2 m/s。但实际有摩擦和阻尼手感会低于这个值所以箱子 Mass 别设得太大否则推起来像推石头。我一般让可推物体的质量在角色的 1/4 到 1/2 之间。验证方法上我会开 Gizmos 可视化。写一个简单的脚本在 OnDrawGizmos 里画出速度向量和接触法线配合物理调试窗口Window Analysis Physics Debugger能看到碰撞体的实际形状、是否休眠、接触数量。这个调试器很多人没用过但它对排查穿模和幽灵碰撞极其有用——它会直接告诉你有多少对碰撞体在互相检测。提示Physics Debugger 可以按 Rigidbody 的 Is Sleeping 状态给物体上色休眠物体是灰色。如果你的场景里大量物体一直是醒着的说明它们在无意义地消耗 CPU需要检查是不是有小位移一直在唤醒它们。5. 排查实录新手最常撞的 7 个坑5.1 穿模、抖动、不碰撞三类高频问题穿模是出现频率最高的抱怨。除了用连续碰撞检测还有几个隐蔽原因。一是碰撞体太薄。一个厚度只有 0.01 的墙物体一帧移动 0.5 米连续检测也可能失效因为连续检测在极小厚度下精度也有限。我的经验是墙体厚度至少 0.1角色类物体每帧位移尽量别超过自身半径的一半。二是 Transform 直接改位置。用 transform.position 移动带刚体的物体相当于绕过物理引擎硬塞物理引擎根本不知道它经过了哪些位置自然穿。要移动就用 MovePosition 或者改 velocity。抖动的原因通常是“物理在 FixedUpdate视觉在 Update”的错位。常见的错误写法是在 Update 里写摄像机跟随而角色在 FixedUpdate 里移动结果摄像机采样频率和物理位置对不齐看起来就是一跳一跳。解决办法有三条给刚体开 Interpolate把跟随逻辑放进 FixedUpdate但会牺牲一点画面流畅度用 SmoothDamp 在 LateUpdate 里做平滑跟随。我一般用第三条兼顾平滑和跟手。不碰撞的问题几乎都能归结到配置。要么至少一方没有 Rigidbody要么 Is Trigger 被误勾选要么层碰撞矩阵把两个层取消了勾选要么碰撞体被缩放成了 0Scale 里有一项是 0 时碰撞体会退化成无效形状。还有一个很少人注意的如果父物体带 Rigidbody子物体的 Collider 会随父刚体一起动但如果代码里改了子物体的 localScale碰撞体的形状更新会有延迟可能好几帧都处于错误状态。5.2 问题速查表与独家避坑经验把上面的经验整理成表遇到问题按顺序排查通常两分钟能定位。现象最可能的原因排查动作物体不掉落没加 Rigidbody检查是否只有 Collider不触发 OnCollisionEnter双方都无 Rigidbody给运动方加 Rigidbody高速穿透墙体Discrete 检测改 Continuous Dynamic墙体加厚物体持续抖动渲染与物理帧错位开 Interpolate跟随放 LateUpdate触发器不响应无 Rigidbody 参与给移动方加刚体勾 Is Kinematic物体在斜面上滑动摩擦不足调物理材质 Static Friction 到 0.6 以上场景越跑越卡大量刚体未休眠Physics Debugger 看休眠状态推箱子推不动箱子质量过大或摩擦力过强箱子质量降到角色 1/4 左右几个不太容易想到的坑我单独说。第一个是“幽灵碰撞”两个看起来没接触的物体撞在一起多半是碰撞体比视觉模型大尤其是 Mesh Collider 开了 Convex 之后形状被简化可能比原模型鼓出来一块。解决方式是打开 Physics Debugger 看真实形状或者在最外层给碰撞体留出与外观一致的内缩量。第二个是 Kinematic 刚体的陷阱。Is Kinematic 勾上后物体不受力、不受重力只能靠代码移动但它依然能触发碰撞回调只是不会产生物理推挤。很多人以为 Kinematic 就等于“关闭物理”其实不是。用它做移动平台是标准做法但要注意平台上的物体需要靠摩擦或者父子关系带着走否则平台一动上面的箱子就留在原地。第三个是物理层与摄像机裁剪的关系。摄像机裁剪面设得太大远处物体虽然被裁掉了不渲染但物理还在跑白白浪费性能。反过来如果发现某些物体在远处依然参与碰撞但看不见也别惊讶物理和渲染是两套系统。第四个是平台相关的坑。在 WebGL 平台PhysX 是以单线程方式跑的物理性能比原生平台差不少刚体数量要克制能烘培成静态的就烘培。在 Pico 这类一体机上物理和渲染抢同一个移动端 GPU/CPU 预算动作类场景里刚体数量建议控制在几十个量级剩下的交互用射线检测和触发区域来做别硬堆。6. 性能优化与扩展玩法6.1 大量刚体的优化手段先说一个硬指标同一个刚体上的多个 Collider 会被合并处理但不同刚体之间的每一对碰撞都要走宽相位加窄相位。所以优化的本质是减少碰撞对的数量和减少唤醒的刚体数量。减少碰撞对靠层矩阵。把场景分成 Player、Enemy、Projectile、Environment、Debris 等层只在需要的组合上打勾。一个几百分子弹的场景子弹之间互不碰撞、子弹只碰 Enemy碰撞对数量能从几万降到几百帧率差别非常直观。减少唤醒靠休眠机制。PhysX 会在物体速度低于 sleepThreshold 时让它休眠默认阈值是 0.005。问题在于如果你代码里每帧都在调用 WakeUp 或者修改 velocity物体永远不休眠。我见过有人在 FixedUpdate 里无条件写 rb.velocity new Vector3(...)整个场景的物体全部醒着CPU 占用居高不下。正确做法是判断输入是否为零为零时不设速度让它自然停下。Mesh Collider 的替代方案也值得展开。地面、墙这类静态大物体用原始碰撞体拼合往往比一个大的 Mesh Collider 更快因为宽相位的包围盒更紧凑。一个房间地板用 Box、四面墙各一个 Box比一个整体的 Mesh Collider 便宜得多。至于地形Terrain Collider 有专门的优化路径但精度太高也会变慢可以在 Terrain 设置里调高 Pixel Error 来减少碰撞面。再有就是 Solver Iterations。Project Settings Physics 里的 Default Solver Iterations 默认是 6Velocity Iterations 是 1。堆叠物体多、容易出现穿透和抖动时把 Solver Iterations 提到 10 到 12 会有明显改善但每个刚体的解算开销也上去了。这是个典型的时间和精度的交换我只在堆叠玩法里提高它其他场景保持默认。6.2 与动画、网络、角色的结合刚体和碰撞体不是孤立的它跟动画、载具、网络同步都有交集。角色动画方面如果用 Animator 驱动带 Rigidbody 的角色会出现物理和动画打架的情况典型表现是脚陷进地面或者动画被物理拉扯变形。稳妥方案是把物理碰撞体放在根物体上控制移动视觉模型挂在子物体上自由播动画再写一个简单的 IK 或位置修正脚本让脚部贴合地面。载具方面Wheel Collider 是绕不开的组件。它自带悬挂弹簧和摩擦曲线需要设置 Suspension Distance、Spring、Damper 和 Forward/Sideways Friction。新手最常犯的错误是把 Wheel Collider 放在车轮模型的中心其实它的悬挂压缩是从挂点向下延伸的位置需要按悬挂行程往上偏移一段。我调一辆越野车时前轮悬挂行程给了 0.35弹簧 35000、阻尼 4500跑起来才既不飘也不颠。物理抓取是 XR 项目里的高频需求。简单实现是给物体加一个 FixedJoint 或者 ConfigurableJoint把手柄的位置传给关节的 connectedBody。但直接改关节位置容易产生巨大的冲量导致物体乱飞需要限制手柄每帧的位移上限或者用 MovePosition 来做平滑。这块我在 Pico 项目里折腾了挺久最后发现把抓取物体的 Drag 临时调高、松手时恢复是抑制抖动最省事的手段。最后一个方向是自定义轻量物理。当刚体数量上千或者场景是弹幕、粒子级别的交互时PhysX 就显得太重了。这时候可以自己写一个基于空间网格的圆形碰撞检测只处理位置和半径开销能降一到两个数量级。这种做法在弹幕游戏和小游戏里很常见代价是丢掉了真实的旋转、摩擦和堆叠。选择前先问自己我真的需要全套物理吗还是只需要“碰到就消灭”我个人在实际操作中的体会是刚体和碰撞体的难点从来不在 API而在“什么时候不用它”。把静态的东西保持静态把能简化的碰撞体简化掉把不需要互相检测的层关掉这三点做到了项目里的物理问题会少一大半。至于那些确实需要精细物理的场景花时间打开 Physics Debugger 和物理材质反复试参数比在代码里加一堆补丁要划算得多。