Unity物理系统深度解析:从核心循环到高级碰撞检测与性能优化 1. 项目概述为什么Unity物理系统值得深挖如果你在Unity里做过一个让盒子掉下来、让球滚起来或者让角色被墙挡住的功能那你已经和Physics类打过交道了。但很多时候我们只是停留在“加个Rigidbody和Collider就能动”的层面一旦遇到角色卡墙、物体穿透、性能骤降或者想实现一个“真实”的爆炸效果时就开始头疼了。这个系列尤其是这篇“上篇”就是要带你从“能用”走到“精通”把Unity物理引擎里那些藏在API文档和默认行为背后的门道一次讲清楚。Unity的物理系统尤其是面向对象的3D物理基于NVIDIA PhysX它不是一个黑盒魔法。它是一套精密的、可预测的、可深度定制的模拟系统。理解它意味着你能做出手感更扎实的移动、更可信的交互反馈以及更高效的游戏性能。无论是想做一款物理解谜游戏还是在一个大型开放世界里管理成千上万的动态物体亦或是仅仅想让你的角色跳跃感觉不那么“飘”深入Physics类都是必经之路。这篇文章适合已经了解Unity基础操作、写过一些简单物理交互脚本但渴望掌控全局、解决实际复杂问题的开发者。我们会从最核心的物理模拟循环讲起深入到刚体运动的每一个细节控制最后探讨高级查询与性能优化确保你读完不仅能复现效果更能理解背后的“为什么”。2. 物理模拟的核心循环与架构解析很多开发者对物理的理解始于Rigidbody.AddForce但这就像只看到了汽车的油门踏板却不了解发动机和传动系统。要真正驾驭物理必须从它的“心跳”——模拟循环开始。2.1 FixedUpdate物理世界的独立时钟Unity的主循环有两个核心更新方法Update和FixedUpdate。Update的调用频率与游戏帧率同步是不稳定的。而FixedUpdate的调用间隔是固定的默认0.02秒即50Hz物理引擎的所有计算都发生在这里。这是为了保证物理模拟的确定性和稳定性。一个常见的误区是在Update里直接修改Rigidbody的位置transform.position或施加力这会导致不可预测的物理行为比如抖动或穿透。注意所有对物理状态位置、旋转、速度、力的修改都应该在FixedUpdate中进行。在Update中读取物理状态如刚体的当前位置用于渲染是安全的但写入操作必须留给FixedUpdate。物理模拟在FixedUpdate中大致遵循以下顺序积分前阶段Unity调用所有脚本的FixedUpdate方法。这是你施加力AddForce、设置速度velocity或执行其他物理相关逻辑的最佳时机。物理引擎解算Unity物理引擎PhysX接管执行以下核心工作碰撞检测检查所有碰撞体Collider之间的接触和重叠。求解约束计算碰撞响应、关节约束等生成冲量Impulse。积分根据受到的合力包括重力、你施加的力、碰撞冲量等和物体的质量积分计算新的速度和位置。积分后阶段物理引擎将计算出的新位置和旋转应用回Rigidbody和Transform组件。理解这个循环你就明白了为什么在FixedUpdate里AddForce是即时的而在Update里可能会延迟或丢失。2.2 物理层Layer与碰撞矩阵性能与逻辑的第一道闸当场景中有成百上千个物体时让每个物体都互相检测碰撞在计算上是灾难性的。物理层和碰撞矩阵Collision Matrix就是用来精细化控制“谁和谁碰撞”的规则系统。物理层Physics Layer你可以给任何GameObject分配一个层如Player, Enemy, Ground, Projectile, Debris。碰撞矩阵Edit Project Settings Physics这是一个NxN的表格定义了任意两个层之间是否进行碰撞检测和物理交互。实操心得默认全开是性能杀手新建项目时碰撞矩阵默认是所有层之间都碰撞。你的第一个优化动作就应该是去这里取消所有不必要的交叉勾选。例如“UI”层通常不需要与任何物理层碰撞“Debris”碎片层内部的小碎片之间可能也不需要互相碰撞以避免“计算风暴”。逻辑分离利用层可以轻松实现逻辑过滤。比如你希望子弹能击中玩家和敌人但不能击中其他子弹或触发器。通过精心设计碰撞矩阵可以免去在代码里写一堆if (other.gameObject.tag ...)的判断提升性能和代码清晰度。一个常被忽略的细节Physics.IgnoreCollision方法可以在运行时动态忽略两个特定碰撞体之间的碰撞这比修改层更灵活但管理成本较高适用于特定场景下的临时忽略如角色穿过刚刚生成的爆炸冲击波。2.3 物理材质摩擦与弹性的微观世界物理材质Physic Material是赋予碰撞体表面特性的资产。它主要控制两个属性动态/静态摩擦力Dynamic/Static Friction物体在表面上滑动或开始滑动所需的力的大小。值在0无摩擦如冰面到1极大摩擦之间。通常静态摩擦力略大于动态摩擦力模拟“启动更费力”的效果。弹力Bounciness碰撞后速度恢复的系数。0表示完全非弹性碰撞如泥巴1表示完全弹性碰撞理想情况会有能量损失。组合弹力取两个碰撞体弹力的最大值。高级应用与避坑“冰面”与“胶水”想做一个溜冰关卡创建一个摩擦力接近0的物理材质赋给地面。想做一个沼泽地让角色移动迟缓可以尝试增加摩擦力但更物理的方法是使用Rigidbody.drag线性阻尼来模拟介质阻力。弹力叠加的陷阱两个弹力为0.8的球相撞组合弹力是0.8这可能导致它们弹跳很多次才停下看起来能量不衰减。这是符合物理的理想弹性碰撞能量守恒但可能不符合游戏体验。通常我们会通过代码或在物理材质中设置一个小于1的弹力并可能结合Rigidbody.angularDrag角速度阻尼来让旋转也停下来。摩擦力方向PhysX默认使用各向同性的摩擦模型。对于像汽车轮胎这样前后、左右摩擦力不同的情况需要更复杂的车轮碰撞体WheelCollider或自定义实现。3. 刚体运动控制的进阶技巧掌握了宏观循环我们来微观操控每一个刚体。Rigidbody组件是物理对象的核心它的控制方式决定了物体的“手感”。3.1 力的艺术AddForce的多种模式Rigidbody.AddForce是最常用的方法但它的ForceMode参数选择至关重要ForceMode.Force添加一个持续的力单位牛顿。这是最符合物理直觉的力会随着时间持续改变物体的动量mass * velocity。适合模拟火箭推进器、风力等持续作用的效果。// 模拟一个持续向前的推进力 void FixedUpdate() { rb.AddForce(transform.forward * enginePower, ForceMode.Force); }ForceMode.Impulse添加一个瞬时冲量单位牛顿·秒或千克·米/秒。它会立即改变物体的动量。适合模拟爆炸、射击、跳跃等瞬间效果。// 模拟跳跃瞬间施加一个向上的冲量 if (Input.GetKeyDown(KeyCode.Space)) { rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); }ForceMode.Acceleration添加一个持续的加速度单位米/秒²忽略质量。这意味着无论物体多重它获得的加速度都是一样的。在某些需要忽略质量差异的游戏场景中很有用比如所有玩家受同一重力加速度影响。ForceMode.VelocityChange直接改变速度单位米/秒忽略质量。这是最直接、最“不物理”但有时最有效的控制方式可以瞬间将速度设为一个值。选择策略对于需要真实物理反馈的物体如被炸飞的箱子使用Force或Impulse。对于需要精确控制移动的角色控制器有时直接修改velocity或使用VelocityChange反而能获得更佳的手感但会牺牲一些物理真实性。3.2 直接速度控制与插值直接修改Rigidbody.velocity是控制运动最强大的方式之一尤其适用于角色移动、平台追逐等场景。// 让物体以恒定速度向目标移动忽略质量、摩擦 Vector3 direction (target.position - transform.position).normalized; rb.velocity direction * moveSpeed;但直接设置velocity会覆盖物理引擎计算出的所有速度包括重力。所以常见的模式是只修改水平速度而保留垂直速度如重力影响下的Y轴速度。// 典型的第一人称/第三人称角色水平移动保留重力影响 Vector3 horizontalVelocity rb.velocity; horizontalVelocity.y 0; // 保存原始的垂直速度重力、跳跃等 horizontalVelocity transform.forward * verticalInput transform.right * horizontalInput; horizontalVelocity horizontalVelocity.normalized * moveSpeed; // 将计算好的水平速度与原始的垂直速度合并 rb.velocity new Vector3(horizontalVelocity.x, rb.velocity.y, horizontalVelocity.z);运动平滑与插值直接设置速度或位置可能导致运动在视觉上卡顿因为FixedUpdate的频率可能低于帧率。Rigidbody组件上的Interpolation插值选项就是为了解决这个问题。None无插值。物体的渲染位置严格等于物理更新后的位置在帧率高于物理更新频率时会出现抖动。Interpolate插值。物体根据当前帧和上一帧的物理位置进行平滑插值渲染。这是最常用的选项能让运动看起来非常平滑即使物理更新频率不高。Extrapolate外推。物体根据当前速度和上一帧的位置预测并渲染。在物体运动稳定时效果很好但突然转向或停止时可能出现预测错误导致的“鬼影”。实操心得对于玩家控制或主要运动的物体务必开启Interpolate。对于大量静止或缓慢运动的背景物体可以设为None以节省性能。Extrapolate使用需谨慎在网络预测同步中可能有其用途但单机环境下容易出问题。3.3 刚体休眠与唤醒机制为了性能PhysX引入了刚体休眠Sleeping机制。当一个刚体的速度低于某个阈值Sleep Threshold并持续一段时间后它会进入休眠状态。休眠的刚体不再参与物理计算直到受到外力碰撞、力等被唤醒。常见问题排查“我的物体怎么推不动了”检查刚体是否意外进入了休眠状态。你可以通过Rigidbody.IsSleeping()查询并用Rigidbody.WakeUp()强制唤醒它。有时一个微小的力不足以唤醒它需要确保施加的力或碰撞足够强。性能优化确保非动态物体如静态地形使用Static碰撞体而非带刚体的Dynamic碰撞体。对于需要偶尔移动的物体如可开关的门可以设置刚体为Kinematic运动学由代码完全控制其运动它不会休眠但也不会受物理力影响直到你将其改回Dynamic。手动控制休眠通过Rigidbody.Sleep()可以手动让刚体休眠Rigidbody.WakeUp()手动唤醒。在你知道一堆物体将长时间静止时如关卡初始化后手动让它们休眠可以提升初始帧的性能。4. 碰撞检测与信息获取的深度应用碰撞不仅仅是“碰到”更是游戏逻辑交互的触发器。Unity提供了从简单到复杂的多种碰撞信息获取方式。4.1 碰撞回调函数详解MonoBehaviour中与碰撞相关的回调主要有三组区别必须厘清OnCollisionEnter/Stay/Exit触发条件两个GameObject都有非触发器的Collider且至少有一个带有非运动学的Rigidbody。信息通过Collision参数提供丰富的碰撞点contacts、法线normal、相对速度relativeVelocity和碰撞体collider信息。用途处理真实的物理碰撞效果如播放撞击音效根据relativeVelocity.magnitude决定音量、产生撞击粒子、计算伤害等。OnTriggerEnter/Stay/Exit触发条件至少有一个Collider是触发器Is Trigger勾选。触发器不会产生物理碰撞响应物体会直接穿过。信息通过Collider参数提供另一个碰撞体的引用。用途处理逻辑区域如拾取物品、进入危险区域、触发剧情、检测玩家进入房间等。OnControllerColliderHit触发条件与CharacterController组件相关。当CharacterController与任何非触发器Collider碰撞时调用。信息提供ControllerColliderHit参数包含碰撞点、法线、被撞物体等。用途专门用于处理角色控制器的碰撞逻辑比如沿斜坡行走、推开轻量物体等。避坑指南性能注意OnCollisionStay和OnTriggerStay在接触的每一帧都会被调用避免在其中执行昂贵的操作如查找对象、复杂计算。可以考虑使用计时器或只在特定帧处理。刚体类型两个都是运动学刚体Kinematic Rigidbody之间不会触发OnCollision系列回调。运动学刚体与静态碰撞体之间也不会。获取对端对象在回调中使用collision.gameObject或other.gameObject来获取碰撞的对方GameObject进而获取其身上的组件如PlayerHealth来交互。4.2 射线检测从屏幕到世界的桥梁射线检测Raycasting是物理查询中最常用、最灵活的工具。它不依赖于碰撞回调而是主动向场景中投射一条无形的射线检测沿途的碰撞体。基础用法// 从摄像机屏幕中心发射一条射线 Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; // 用于存储碰撞信息 float maxDistance 100f; int layerMask LayerMask.GetMask(Ground, Enemy); // 只检测特定层 if (Physics.Raycast(ray, out hit, maxDistance, layerMask)) { // 命中hit.point是世界坐标中的命中点 // hit.normal是命中点的法线方向 // hit.collider是命中的碰撞体 Debug.Log(Hit: hit.collider.gameObject.name); }高级应用与参数解析LayerMask极其重要永远不要使用默认的Physics.Raycast(ray, out hit)这会对所有层进行检测包括UI、忽略层等浪费性能且可能产生错误结果。始终指定你需要检测的层。QueryTriggerInteraction枚举参数控制射线是否检测触发器。QueryTriggerInteraction.Ignore忽略触发器默认。QueryTriggerInteraction.Collide检测触发器。在需要检测触发器区域如拾取范围时使用。RaycastAll 与 RaycastNonAllocPhysics.RaycastAll返回所有命中的RaycastHit数组。简单但每次调用都会分配新数组在频繁调用如每帧时会产生GC垃圾回收压力。Physics.RaycastNonAlloc将命中结果填充到一个预先分配的RaycastHit[]数组中无GC分配。这是性能关键优化点。private RaycastHit[] hits new RaycastHit[10]; // 预分配数组 void Update() { Ray ray new Ray(transform.position, transform.forward); int hitCount Physics.RaycastNonAlloc(ray, hits, 10f, groundLayer); for (int i 0; i hitCount; i) { // 处理 hits[i] } }SphereCast, CapsuleCast, BoxCast这些是形状投射Shape Cast相当于发射一个有体积的形状球体、胶囊体、盒子进行检测。对于角色移动前的障碍物检测防止卡进墙里或车辆驾驶检测极其有用。// 检测角色前方一个胶囊体范围内是否有障碍物 float radius 0.5f; float height 2f; Vector3 point1 transform.position Vector3.up * radius; Vector3 point2 transform.position Vector3.up * (height - radius); if (Physics.CapsuleCast(point1, point2, radius, transform.forward, out hit, 1f)) { // 前方1米处有障碍物可以提前减速或转向 }4.3 重叠检测与物理查询除了射线有时你需要检测一个区域内的所有物体比如爆炸范围、技能作用区域。Physics.OverlapSphere/Box/Capsule返回指定形状区域内所有碰撞体的数组。同样有OverlapSphereNonAlloc等无GC版本。Collider[] colliders Physics.OverlapSphere(explosionCenter, explosionRadius, enemyLayer); foreach (var collider in colliders) { // 对范围内的每个敌人造成伤害 Enemy enemy collider.GetComponentEnemy(); if (enemy ! null) { enemy.TakeDamage(damage); } }Physics.SphereCast/BoxCast/CapsuleCast与Overlap不同Cast是检测在移动路径上是否会与某物相交常用于移动前的可行性检测。性能警告所有物理查询Raycast, Overlap, Cast都是CPU密集型操作尤其是在每帧对大量物体或复杂网格进行检测时。务必使用合适的层掩码LayerMask缩小检测范围。在不需要每帧检测时如技能冷却期间降低检测频率。优先使用NonAlloc版本。对于固定区域的持续检测可以考虑使用触发器Trigger配合OnTriggerStay但要注意其调用频率也很高。5. 关节与约束构建物理机关Unity提供了多种关节Joint组件用于在刚体之间创建约束模拟现实世界的连接关系如铰链、弹簧、固定等。5.1 常用关节类型与应用场景Fixed Joint固定关节将两个刚体牢固地连接在一起像一个整体。可用于将小部件如剑柄上的宝石固定在主体上或者临时将两个物体焊接在一起。Hinge Joint铰链关节模拟门、杠杆、钟摆的运动。围绕一个轴旋转。你需要配置锚点Anchor局部空间中的旋转中心和轴Axis旋转围绕的局部方向。技巧使用HingeJoint.useMotor和HingeJoint.motor可以给铰链添加一个驱动马达让它自动旋转到目标速度或角度常用于制作自动门、旋转平台。限制通过HingeJoint.limits可以设置旋转的最小和最大角度制作只能打开一定角度的门。Spring Joint弹簧关节用弹簧力连接两个刚体。可以设置弹簧的刚度Stiffness、阻尼Damper和最小/最大距离。非常适合做绳索、蹦极、悬挂系统汽车悬挂的简化模拟。避坑弹簧的刚度和阻尼需要仔细调校数值过大容易导致系统不稳定剧烈抖动数值过小则弹簧过于松软。通常需要反复测试。Configurable Joint可配置关节这是最强大也是最复杂的关节。它几乎可以模拟任何类型的约束通过自由配置每个平移和旋转自由度的锁定Locked、限制Limited或自由Free状态。可以用来制作自定义的车辆悬挂、玩偶关节、复杂的机械臂等。警告配置不当的Configurable Joint极易导致奇怪的抖动或崩溃。建议从一个简单的约束开始逐步增加复杂度。5.2 关节的配置要点与常见问题连接体Connected Body关节需要绑定两个刚体一个是在关节组件所在物体上的刚体另一个是Connected Body。如果Connected Body为空则意味着关节连接到世界空间的一个固定点。锚点与轴Anchor Axis这些都是在关节所在物体的局部空间中定义的。锚点是连接点轴是旋转或移动的方向。错误理解局部空间是关节配置出错的主要原因。在Scene视图中启用关节的Gizmo显示可以直观地看到锚点和轴。断裂力Break Force与断裂扭矩Break Torque当关节受到的力或扭矩超过这个阈值时关节会自动断开销毁。这可以用来模拟绳子被拉断、木板被撬开的效果。断开后可以通过监听关节的OnJointBreak回调函数来触发后续逻辑如播放断裂音效。性能考虑每个关节都会增加物理解算的复杂度。关节链如多个铰链连接成的链条越长稳定性越差对求解器迭代次数的要求也越高。在Project Settings Physics中可以调整Default Solver Iterations和Default Solver Velocity Iterations来改善复杂约束的稳定性但会增加CPU开销。一个铰链门的快速实现示例创建一个Cube作为门板添加Rigidbody质量适中如20。添加Hinge Joint组件。设置Anchor为(0, 0.5, 0)假设门高1米铰链在顶部。设置Axis为(0, 1, 0)绕Y轴旋转。勾选Use Limits设置Limits的Min为-90Max为90门可以向内或向外打开90度。可选勾选Use Spring给门添加一个自动关闭的弹簧力。这样一个基本的物理门就完成了玩家或NPC可以通过碰撞推开它松手后它会因摩擦或弹簧力慢慢停下或关闭。6. 物理材质进阶与自定义碰撞响应虽然Unity提供了标准的碰撞响应基于法线和弹力/摩擦力但有时我们需要更定制化的效果比如根据碰撞部位播放不同动画或者实现“踩头跳跃”这种平台游戏经典机制。6.1 通过碰撞信息判断碰撞点Collision对象包含了contacts数组这是一个ContactPoint结构体数组存储了本次碰撞的所有接触点信息。即使是一次碰撞也可能有多个接触点比如一个盒子以一个角着地。void OnCollisionEnter(Collision collision) { ContactPoint contact collision.contacts[0]; // 通常取第一个接触点 Vector3 hitPoint contact.point; // 世界空间碰撞点 Vector3 hitNormal contact.normal; // 碰撞点法线从自身指向对方 // 判断是否是“头顶”碰撞 float dot Vector3.Dot(hitNormal, Vector3.down); if (dot 0.7f) { // 法线大致朝下说明撞到的是头顶上方的物体 // 处理头顶碰撞逻辑比如顶碎砖块 } // 判断是否是“脚底”碰撞 if (Vector3.Dot(hitNormal, Vector3.up) 0.7f) { // 处理落地逻辑比如重置跳跃次数 canJump true; } }通过分析接触点法线与角色自身方向上、下、前、后等的点积可以粗略判断碰撞发生在哪个部位。6.2 修改碰撞响应PhysicsMaterial与脚本结合标准物理材质提供的摩擦和弹力是全局的、被动的。我们可以在碰撞回调中主动修改碰撞响应。忽略后续碰撞在OnCollisionEnter中如果你希望这次碰撞后两个物体暂时不再碰撞比如子弹击中敌人后消失不应再产生碰撞可以调用Physics.IgnoreCollision(collision.collider, thisCollider)。注意管理好忽略的时机和恢复。自定义力施加你可以不依赖物理引擎的自动计算而是根据碰撞信息自己计算并施加力。void OnCollisionEnter(Collision collision) { // 计算一个自定义的反弹力比如总是以固定速度反弹 Vector3 reflectDirection Vector3.Reflect(rb.velocity.normalized, collision.contacts[0].normal); rb.velocity reflectDirection * bounceSpeed; // 或者根据碰撞的相对速度计算一个冲击力 float impactStrength collision.relativeVelocity.magnitude; rb.AddForce(-collision.contacts[0].normal * impactStrength * someFactor, ForceMode.Impulse); }这种方式让你完全掌控碰撞后的行为但需要一定的物理和数学知识来保证效果自然。6.3 实现平台游戏角色控制器一个经典的2D/3D平台游戏角色控制器其核心物理需求是可靠的地面检测使用Physics.SphereCast或Physics.CapsuleCast从角色底部向下投射检测一定距离内是否有“地面”层物体。比单纯用OnCollisionStay更可靠因为可以控制检测距离和形状。可控的跳跃跳跃不是简单施加一个向上的力。为了手感更好通常采用“可变高度跳跃”——按跳跃键时间越长跳得越高。这可以通过在起跳时给一个初始冲量然后在空中如果松开按键就减小垂直速度来实现。void HandleJump() { if (isGrounded Input.GetButtonDown(Jump)) { rb.velocity new Vector3(rb.velocity.x, jumpInitialSpeed, rb.velocity.z); isJumping true; } // 如果正在跳跃且松开了按键减少上升速度实现小跳 if (isJumping !Input.GetButton(Jump) rb.velocity.y 0) { rb.velocity new Vector3(rb.velocity.x, rb.velocity.y * jumpCutMultiplier, rb.velocity.z); } }斜坡处理当角色在斜坡上时重力会将其拉向坡底。为了能在斜坡上站立和行走需要将移动方向投影到斜坡的切平面上。这可以通过射线检测获取地面法线然后使用Vector3.ProjectOnPlane来实现。Vector3 groundNormal ... // 从地面检测中获得 Vector3 moveDirection ... // 玩家输入的方向 Vector3 slopeMoveDirection Vector3.ProjectOnPlane(moveDirection, groundNormal).normalized; rb.velocity new Vector3(slopeMoveDirection.x * speed, rb.velocity.y, slopeMoveDirection.z * speed);墙壁跳跃与蹬墙跳这需要检测侧面的碰撞。可以通过在角色左右两侧放置触发器或进行射线检测来实现。当检测到侧面碰撞且玩家按下跳跃键时施加一个远离墙壁的力和一个向上的力。将这些技巧组合起来你就能构建一个手感扎实、反应灵敏的物理角色控制器它远比使用纯CharacterController或简单的AddForce要强大和灵活。关键在于理解每一帧物理状态的变化并精确地施加干预让物理引擎为你工作而不是与你对抗。