
1. 项目概述碰撞检测为何是游戏物理的基石在Unity引擎里折腾过一阵子的开发者无论是做一款横版跳跃游戏还是一个需要精确交互的VR应用迟早都会和物理引擎正面“碰撞”。这个“碰撞”不是比喻而是实实在在的、决定你游戏里角色会不会穿墙、子弹能不能击中目标、箱子能不能被推走的核心机制——碰撞检测。很多新手包括当年的我都曾天真地以为给模型挂上一个碰撞体Collider就万事大吉结果要么是角色卡在莫名其妙的地方动弹不得要么是高速运动的物体像幽灵一样穿过障碍物调试起来让人头大。实际上Unity的物理引擎默认是NVIDIA PhysX提供的碰撞检测是一套极其精密又充满“脾气”的系统。它远不止是“两个盒子碰在一起”那么简单其背后涉及到离散与连续检测的权衡、性能与精度的博弈、以及触发器与碰撞体在逻辑上的根本区别。理解这套机制是你从“能用Unity做东西”到“能做好东西”的关键一步。无论是解决诡异的物理Bug还是为了实现特定的游戏手感比如《蔚蓝》里那种像素级的精准平台跳跃深入碰撞检测的细节都必不可少。这篇文章我就结合自己踩过的无数个坑把Unity的碰撞检测机制掰开揉碎了讲清楚让你不仅知道怎么用更明白为什么要这么用。2. 碰撞检测的核心组件与工作原理拆解在深入机制之前我们必须先理清参与碰撞检测的几个核心演员刚体Rigidbody、碰撞体Collider和物理材质Physic Material。它们各司其职共同演绎了物理交互这场大戏。2.1 刚体物理世界的入场券刚体组件是物体参与物理模拟的“身份证”。没有刚体一个物体在物理引擎眼里就是一片虚无的背景即使它有碰撞体也不会被计算受力、速度和碰撞响应。你可以把它理解为一个物体的“物理质量中心”。关键属性解析质量Mass这是最容易被误解的参数之一。质量不是模型的视觉大小而是物理惯性。一个视觉上巨大的气球质量可以很小而一个视觉上的小铁球质量可以很大。质量比直接影响碰撞后的运动状态。例如质量比为10:1的两个球碰撞质量小的球会被狠狠弹开。碰撞检测模式Collision Detection这是解决高速物体穿透问题的关键我们会在后面详细展开。插值Interpolation用于平滑基于物理运动的物体的渲染。物理计算在固定的时间步长Fixed Timestep中进行而渲染帧率Update是波动的。这会导致基于物理运动的物体看起来有“抖动”。Interpolate模式使用上一帧的物理状态来平滑当前帧的渲染Extrapolate则尝试预测下一帧的状态。对于由玩家直接控制或需要极度平滑视觉的对象如跟随摄像机开启插值很有必要。约束Constraints可以冻结物体在某个或某些轴上的移动或旋转。这是实现2D游戏效果冻结Z轴移动和X、Y轴旋转或制作滑动门的常用手段。实操心得对于完全由动画或脚本驱动、但需要与其他物理物体交互的物体比如一个被动画控制的摆锤可以勾选Is Kinematic。这样它本身不受物理力影响但它的运动会以巨大的质量去影响其他非Kinematic的刚体。常用于陷阱、移动平台。2.2 碰撞体物体的物理轮廓碰撞体定义了物体的“物理形状”。它不一定也通常不应该和模型的视觉网格Mesh完全一致。用高精度的视觉网格做碰撞体是性能杀手。常见碰撞体类型与选用指南碰撞体类型性能开销精度典型应用场景盒型碰撞体 (Box Collider)极低低AABB包围盒墙壁、地板、箱子、门、桌子等规则物体。球形碰撞体 (Sphere Collider)极低低球体、角色粗略的、炮弹、能量球。胶囊碰撞体 (Capsule Collider)低中第三人称/第一人称角色控制器的首选。能很好地模拟人体且上下坡时不易卡住。网格碰撞体 (Mesh Collider)非常高高可贴合复杂网格极其复杂的静态环境如一块奇形怪状的岩石且必须勾选“Convex”才能与其它Mesh Collider碰撞。动态物体慎用。车轮碰撞体 (Wheel Collider)中特殊专为车辆模拟设计包含悬挂、摩擦力等高级参数。一个重要的概念复合碰撞体Compound Collider。一个游戏对象可以附加多个基本碰撞体Box, Sphere, Capsule来近似复杂形状。例如一个椅子可以由一个盒体座位和四个胶囊体腿组合而成。这比使用一个Mesh Collider性能要好得多也更容易控制。踩坑记录早期我曾用一个精细的角色视觉网格直接生成Mesh Collider结果游戏在移动端上帧率暴跌。后来改用胶囊体几个小盒体用于模拟手臂伸展组成的复合碰撞体效果几乎一样性能提升巨大。黄金法则用最简单的碰撞体形状去近似你的物体。2.3 物理材质定义碰撞表面的“性格”物理材质决定了两个表面接触时的行为主要是摩擦力和弹性反弹系数。你可以把它想象成给物体表面覆盖了一层橡胶、冰面或钢铁。动态摩擦力 (Dynamic Friction)物体已经运动时与表面接触产生的摩擦力。静态摩擦力 (Static Friction)物体从静止到开始运动所需要克服的摩擦力。通常静态摩擦力略大于动态摩擦力。弹力 (Bounciness)取值范围0到1。0表示完全无弹性碰撞如橡皮泥砸地1表示完全弹性碰撞理想情况能量无损失。两个碰撞物体的弹力值会通过Bounce Combine模式默认为平均进行合并得出最终弹力效果。摩擦力合并/弹力合并 (Friction/Bounce Combine)定义当两个拥有不同物理材质的物体碰撞时如何计算最终的摩擦力和弹力。有“平均”、“最小”、“最大”、“相乘”等模式。例如一个弹力为0.8的篮球砸在弹力为0.3的泥地上如果使用“平均”模式则合并弹力为0.55。3. 碰撞检测的两种模式离散与连续这是Unity碰撞检测中最核心、也最容易出问题的地方。物理引擎默认的更新频率FixedUpdate是有限的通常每秒50次即0.02秒一次。在这0.02秒内物体会移动一段距离。如果这段距离超过了它自身或对方碰撞体的尺寸就可能发生“隧道效应”——物体从障碍物的一侧直接穿越到了另一侧没有触发任何碰撞检测。3.1 离散检测 (Discrete)默认的性价比之选这是默认设置。物理引擎只在每个固定的物理时间步Fixed Timestep检查物体是否相交。如果物体在两步之间移动得足够快它就可能“跳过”检测。适用场景大多数低速或中等速度运动的物体。比如场景中缓缓移动的平台、受重力下落的箱子、角色的一般行走和跳跃。它的性能开销最小。3.2 连续检测 (Continuous)针对高速运动的保险为了解决“隧道效应”Unity提供了连续检测模式。它通过预测物体在时间步间的运动轨迹形成一个“扫掠体”来检测是否会发生碰撞。Continuous仅检测该刚体与静态碰撞体即没有附加刚体的碰撞体之间的连续碰撞。性能开销中等。Continuous Dynamic检测该刚体与所有标记为Continuous或Continuous Dynamic的刚体之间的连续碰撞。这是最彻底的检测模式性能开销也最大。Continuous Speculative一种基于推测的连续检测在某些边缘情况下比Continuous Dynamic更稳定但原理更复杂一般情况不常用。配置策略非常重要给高速运动的物体如子弹、发射物、高速移动的玩家的刚体设置Continuous Dynamic。给可能会被高速物体撞击的静态障碍物如薄墙、地板边缘所对应的碰撞体勾选Continuous检测注意静态物体没有刚体这个设置在碰撞体组件上。对于大量低速运动的物体如一堆碎屑、粒子坚持使用Discrete以节省性能。避坑指南不要无脑地将所有物体都设为Continuous Dynamic我曾在一个有上百个物理小物件的场景中全开了连续检测物理计算耗时直接增加了300%。正确的做法是按需启用。通常只有玩家子弹和玩家角色本身需要Continuous Dynamic关键的薄型障碍物设为Continuous静态碰撞体其他一律Discrete。4. 碰撞与触发两种交互逻辑的本质区别这是概念上的分水岭混淆两者会导致逻辑错误。4.1 碰撞 (Collision)碰撞是物理的、有反馈的交互。两个物体发生碰撞时物理引擎会计算碰撞力并根据质量、速度、物理材质等参数自动改变它们的运动状态反弹、滑动、停止。同时它会调用一系列碰撞事件函数。关键事件函数OnCollisionEnter(Collision collisionInfo): 当碰撞开始时调用一次。OnCollisionStay(Collision collisionInfo): 在碰撞持续的每一帧物理帧调用。OnCollisionExit(Collision collisionInfo): 当碰撞结束时调用一次。Collision参数包含了丰富的碰撞信息如接触点contacts、相对速度relativeVelocity、碰撞法线normal等可用于实现更复杂的逻辑比如根据碰撞点播放特效。4.2 触发 (Trigger)触发是逻辑的、无形的交互。当一个碰撞体被勾选为Is Trigger后它就变成了一个“感应区域”。物理引擎会忽略它的碰撞响应物体可以穿过它但会触发另一套事件函数。至少有一个参与方必须有刚体组件。关键事件函数OnTriggerEnter(Collider other): 当其他碰撞体进入触发器范围时调用一次。OnTriggerStay(Collider other): 当其他碰撞体停留在触发器内时每帧调用。OnTriggerExit(Collider other): 当其他碰撞体离开触发器范围时调用一次。Collider参数是对方碰撞体的引用你可以通过它获取对方的游戏对象other.gameObject等信息。如何选择需要物理反馈阻挡、弹开时用碰撞。例如角色与墙壁、球与地面、汽车与障碍物。仅需检测物体进入/离开某个区域不改变其物理运动时用触发。例如拾取道具的区域、关卡检查点、敌人警戒范围、陷阱的伤害区域。常见错误试图在OnTriggerStay里通过修改Rigidbody.velocity来模拟物理推动。这通常会导致奇怪的行为因为触发器本身不参与物理求解。正确的做法是如果需要在这个区域内有物理效果如减速力场应该使用碰撞并通过OnCollisionStay施加力AddForce或直接修改速度同时可能需要调整物理材质来改变摩擦力。5. 碰撞过滤图层与碰撞矩阵在复杂的游戏场景中并非所有物体都需要相互碰撞。让子弹和子弹之间、远处的装饰物和玩家之间进行碰撞检测纯属浪费性能。Unity使用图层Layer和碰撞矩阵Collision Matrix来高效地管理碰撞关系。工作流程定义图层在Tags and Layers设置中创建有意义的图层如Player,Enemy,Bullet,Environment,Pickup,IgnoreRaycast等。分配图层将场景中的游戏对象分配到对应的图层。配置碰撞矩阵进入Edit - Project Settings - Physics在底部的Layer Collision Matrix中通过复选框精确控制哪些图层能与哪些图层发生碰撞。最佳实践示例Bullet层只与Enemy和Environment层碰撞不与Bullet或Pickup层碰撞。Pickup层只与Player层碰撞。Environment层与几乎所有层都碰撞除了IgnoreRaycast等特殊层。可以创建一个NoCollision层用于那些纯粹视觉装饰、不需要任何碰撞的物体。通过精细配置碰撞矩阵你可以大幅减少物理引擎需要处理的碰撞对Pair数量这是提升物理性能最有效的手段之一。6. 物理查询射线检测与形状投射除了被动的碰撞检测我们经常需要主动进行物理查询比如判断玩家前方是否有障碍物射线检测或者判断一个角色能否移动到某个位置形状投射。6.1 射线检测 (Raycast)这是最常用、最高效的物理查询方法。它从一点向一个方向发射一条无形的射线并返回击中的第一个或多个碰撞体信息。// 简单的单点射线检测 RaycastHit hitInfo; if (Physics.Raycast(transform.position, transform.forward, out hitInfo, 10f)) { Debug.Log(击中了: hitInfo.collider.gameObject.name); Debug.Log(击中点: hitInfo.point); Debug.Log(距离: hitInfo.distance); } // 带图层遮罩的射线检测只检测特定层 int layerMask 1 LayerMask.NameToLayer(Enemy); // 只检测Enemy层 if (Physics.Raycast(transform.position, transform.forward, out hitInfo, 10f, layerMask)) { // 只处理击中Enemy层的逻辑 }高级用法Physics.SphereCast: 球体投射相当于发射一个球体用于判断一个有一定体积的物体如角色前方是否有空间。Physics.BoxCast: 盒体投射。Physics.CapsuleCast: 胶囊体投射是角色移动检测如判断是否着地、是否碰到侧墙的利器。Physics.RaycastAll: 返回射线路径上所有击中的物体用于实现穿透弹等效果。6.2 形状检测 (Overlap)用于检测在某个位置、某个形状内是否存在其他碰撞体。它不关心“如何到达那里”只关心“那里有什么”。// 检测角色脚下是否有地面球形检测 Collider[] hitColliders Physics.OverlapSphere(transform.position Vector3.down * 0.5f, 0.4f); bool isGrounded hitColliders.Length 0; // 检测一个区域内的所有敌人盒形检测 Vector3 halfExtents new Vector3(5, 2, 5); // 检测盒的大小 Collider[] enemies Physics.OverlapBox(centerPoint, halfExtents, Quaternion.identity, enemyLayerMask);性能警告射线和形状检测虽然强大但每帧进行大量检测尤其是在Update中同样会消耗性能。一个常见的优化是将检测频率降低例如在FixedUpdate中进行或者每2-3帧检测一次。对于AI的视野检测尤其需要注意。7. 实战构建一个健壮的角色控制器理论说再多不如动手实现一个。这里我们不直接用CharacterController组件而是用刚体胶囊碰撞体来构建以深入理解碰撞检测的每个环节。7.1 基础设置创建一个胶囊体作为角色模型。添加Rigidbody组件。冻结Y轴旋转防止摔倒设置质量为合理值如70碰撞检测模式设为Continuous Dynamic防止高速移动时穿墙。添加Capsule Collider组件调整高度和半径以匹配模型。创建一个脚本PlayerMovement.cs并挂载。7.2 移动与地面检测我们使用力AddForce或直接修改速度rigidbody.velocity来实现移动。地面检测使用胶囊体投射CapsuleCast或底部球体检测OverlapSphere这比简单的射线检测更可靠能应对斜坡和台阶边缘。public class PlayerMovement : MonoBehaviour { public float moveSpeed 10f; public float jumpForce 7f; public LayerMask groundLayer; // 在Inspector中指定哪些层是地面 public float groundCheckRadius 0.4f; public Transform groundCheckPoint; // 一个在角色脚底的空物体 private Rigidbody rb; private bool isGrounded; void Start() { rb GetComponentRigidbody(); } void FixedUpdate() // 物理操作放在FixedUpdate中 { // 1. 地面检测 isGrounded Physics.CheckSphere(groundCheckPoint.position, groundCheckRadius, groundLayer); // 2. 水平移动使用速度控制响应更快 float moveX Input.GetAxis(Horizontal); float moveZ Input.GetAxis(Vertical); Vector3 moveDirection new Vector3(moveX, 0, moveZ).normalized; Vector3 targetVelocity moveDirection * moveSpeed; // 只改变水平速度保留垂直速度重力/跳跃 rb.velocity new Vector3(targetVelocity.x, rb.velocity.y, targetVelocity.z); // 3. 跳跃 if (isGrounded Input.GetButtonDown(Jump)) { rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); // 使用冲量模式瞬间施加力 } } // 可选在Scene视图中绘制地面检测范围便于调试 void OnDrawGizmosSelected() { if (groundCheckPoint null) return; Gizmos.color Color.yellow; Gizmos.DrawWireSphere(groundCheckPoint.position, groundCheckRadius); } }7.3 解决“爬坡”和“卡墙”问题上面的简单移动在遇到斜坡或墙角时可能会卡住。因为我们的移动直接设置了速度没有考虑斜坡法线方向。改进方案使用Rigidbody.MovePosition配合射线/胶囊体检测进行碰撞预测。但更高级和常见的做法是使用“角色移动”力模式并利用OnCollisionStay来获取碰撞信息进行更复杂的响应。对于生产级角色控制器许多开发者会选择使用或参考Unity官方的CharacterController组件它内部处理了很多这类碰撞细节或者使用像Kinematic Character Controller这样的第三方高级解决方案。但理解其底层基于碰撞检测的原理对于调试和自定义功能至关重要。8. 性能优化与常见问题排查物理计算是CPU密集型任务。不当的使用会导致帧率下降尤其是在移动设备上。8.1 性能优化清单精简碰撞体形状重申这是第一要务。用简单形状组合。善用碰撞矩阵禁用不必要的层间碰撞。静态物体标记为静态确保永远不会移动的环境物体地形、建筑在Inspector右上角勾选Static标签。这允许Unity对其进行批量优化如构建静态碰撞树。减少动态刚体数量物理引擎最擅长处理少量动态物体。对于大量小物体如碎片考虑使用粒子系统或简单的非物理动画。控制检测频率和范围对于AI的视野检测、子弹的碰撞检测可以适当降低频率或使用距离判断提前剔除。避免在Update中调用密集的物理查询移到FixedUpdate中或使用协程间隔执行。谨慎使用Mesh Collider仅用于极其复杂的静态地形并尽量使用简化后的网格。调整物理时间步长Edit - Project Settings - Time中的Fixed Timestep默认是0.02s50Hz。降低此值如0.04s可以减少物理更新频率提升性能但会降低物理模拟的平滑度。这是一个权衡。8.2 常见问题排查表问题现象可能原因解决方案高速物体穿透障碍物碰撞检测模式为Discrete将高速物体的刚体设为Continuous Dynamic并将薄障碍物设为Continuous静态碰撞体。物体抖动或“游泳”多个碰撞体重叠物理更新与渲染更新不同步检查碰撞体是否意外交叉为刚体开启Interpolation。OnTrigger事件不触发双方都没有刚体或其中一方刚体为Kinematic且另一方为Trigger确保至少一方有非Kinematic刚体。检查碰撞矩阵是否禁止了这两层的交互。角色在斜坡上卡住或弹跳碰撞体形状不合适物理材质摩擦力/弹力过高使用Capsule Collider代替Box Collider。调整物理材质的摩擦力或通过脚本在检测到斜坡时施加一个沿斜坡方向的力。物理表现不一致PC vs. 移动端帧率波动导致物理计算次数不同确保所有物理相关的操作AddForce,velocity修改都在FixedUpdate中进行而非Update。FixedUpdate的调用间隔是固定的。性能突然下降动态刚体数量激增复杂Mesh Collider每帧大量射线检测使用Profiler的Physics模块分析。检查是否在循环中实例化了大量带刚体的物体或是否有物体意外从静态变为动态。理解Unity的物理引擎与碰撞检测是一个从“知其然”到“知其所以然”的过程。它不像渲染那样直观但却是游戏交互真实感的灵魂。开始的时候可能会被各种奇怪的Bug困扰但每解决一个问题你对这套系统的掌控力就加深一分。记住没有银弹最好的方案总是来自于对项目具体需求是写实的物理模拟还是风格化的平台跳跃和性能约束的深刻理解。多实验多调试利用好Scene视图中的物理调试可视化如碰撞体边框你就能让游戏里的每一个碰撞都恰到好处。