2026/10/26 19:16:36

Unity物理系统与碰撞检测实战:核心参数、配置技巧与问题排查

Unity物理系统与碰撞检测实战:核心参数、配置技巧与问题排查 物理系统和碰撞检测这块内容在Unity项目里属于那种“平时不显眼、一出问题就要命”的模块。我做过的项目里角色穿墙、子弹打不中、物体抖动、触发器莫名其妙不响应追根溯源十有八九都落在物理配置和碰撞检测的细节上。这篇内容就是围绕Unity引擎下的C#物理系统与碰撞检验展开把刚体、碰撞体、触发器、层碰撞矩阵、射线检测、连续碰撞这些核心点拆开讲透同时补上大量实际项目里踩过的坑和调参经验。不管你是刚接触Unity物理的新手还是已经写过一段时间但总觉得物理表现“不太对劲”的开发者应该都能从里面找到能直接抄作业的配置思路和排查方法。下面我按设计思路、核心细节、实操流程、问题排查四个大块来讲中间穿插参数计算和现场记录尽量做到看完就能上手改。1. 物理系统整体设计与方案选型思路1.1 为什么Unity物理要分“物理世界”和“表现层”两层来理解很多人一开始学Unity物理容易把Transform和Rigidbody混着用结果就是物体该动的时候不动、不该抖的时候狂抖。我习惯把Unity的物理体系理解成两个世界一个是物理世界由Rigidbody、Collider、物理材质、关节等组成由物理引擎在固定时间步长里做积分运算另一个是表现层就是Transform最终呈现出来的位置和旋转。物理引擎算完之后会把结果同步回Transform但这个同步是有时序的。这个区分为什么重要因为一旦你在FixedUpdate之外直接改Transform物理引擎是不知道的它下一帧还会按自己算的位置覆盖回去表现出来就是“我明明设置了位置它又弹回去了”。我见过不少项目在Update里直接给带刚体的物体赋值position然后抱怨碰撞检测不准本质就是绕过了物理世界的计算。所以整体设计的第一条原则凡是参与物理模拟的物体位移和旋转尽量交给Rigidbody去驱动不要手动改Transform。需要瞬移的场景用Rigidbody.position或者MovePosition并且注意瞬移后物理引擎需要重新同步碰撞状态。这条原则定下来后面很多诡异问题就少了一半。1.2 固定时间步长与物理更新的关系为什么它决定了碰撞精度Unity物理默认的固定时间步长是0.02秒也就是每秒50次物理更新。这个值在Project Settings的Time面板里可以改。它直接决定了物理模拟的“采样频率”进而影响碰撞检测的精度。举个直观的例子一个物体以每秒20米的速度移动在0.02秒的步长里它一帧就移动了0.4米。如果这个物体的碰撞体厚度只有0.2米那么它完全有可能在两次物理采样之间“穿过”一个薄墙这就是典型的**穿透tunneling**问题。计算过程很简单单步位移 速度 × 固定步长 20 × 0.02 0.4米大于碰撞体厚度0.2米穿透风险成立。解决思路有三条一是降低固定步长比如改成0.01代价是物理计算量翻倍二是给高速物体开启连续碰撞检测Continuous Collision DetectionCCD三是加厚碰撞体或者用射线做补充检测。这三条在实际项目里往往是组合使用的具体怎么选后面章节会展开。这里先建立一个认知物理步长是碰撞精度的地基调参之前先算清楚单步位移和碰撞体尺寸的关系。1.3 碰撞体选型Box、Sphere、Capsule、Mesh到底怎么挑碰撞体的选型直接关系到性能和检测稳定性。我的经验排序是这样的碰撞体类型性能开销检测稳定性典型适用场景Sphere最低最高球形投射物、球形触发区域Capsule低高角色控制器、人形单位Box低高箱子、平台、建筑块Mesh凸包中中不规则但需要精确的物体Mesh非凸高低静态复杂地形需勾选Convex关闭核心原则是能用基本几何体就不用Mesh能用凸包就不用非凸。非凸Mesh碰撞体在动态物体上不仅性能差而且物理引擎对它的支持有限动态刚体要求碰撞体是凸的容易出现检测异常。我做过一个道具散落的场景一开始给每个道具都用Mesh Collider帧率直接掉了一半后来换成Box加少量Sphere组合性能回来了视觉上玩家根本看不出差别。角色为什么普遍用Capsule因为胶囊体在斜面、台阶上的滑动表现最平滑没有Box的棱角卡顿问题同时比Sphere更贴合人形。这是一个经过大量项目验证的选型新手直接照做就行。1.4 层碰撞矩阵被低估的性能与逻辑双料开关Unity的Layer Collision Matrix层碰撞矩阵在Project Settings的Physics面板里它决定了哪些层之间会发生碰撞。这个东西很多人配置完就忘了但它其实是性能和逻辑的双重开关。性能上关闭不需要的层间碰撞物理引擎就少做一大批broadphase粗筛计算。逻辑上它能让“玩家子弹不打玩家自己”“UI触发区不干扰场景物体”这类需求不用写一行代码就实现。我习惯在项目初期就把层规划好比如Player、Enemy、Bullet、Environment、TriggerZone、Pickup这几层然后在矩阵里把Bullet和Bullet、Bullet和Pickup这类不需要的组合关掉。注意层碰撞矩阵关闭的是“碰撞对”的检测但射线检测Raycast默认不受它影响射线需要单独用layerMask参数控制。这一点经常被混淆后面排查章节会专门讲。2. 核心细节解析与实操要点2.1 Rigidbody关键参数逐个拆解质量、阻力、插值怎么设Rigidbody是物理模拟的核心组件参数不多但每个都有讲究。我按实际调参顺序说。Mass质量默认1。质量影响的是碰撞时的动量交换和受力响应。很多人以为质量越大下落越快这是错的重力加速度和质量的响应在物理引擎里是解耦的忽略空气阻力时。质量真正影响的是碰撞冲量分配比如一个质量10的物体撞质量1的物体前者几乎不受影响后者被弹飞。做推箱子玩法时箱子的质量要明显大于角色否则角色一碰箱子就飞。Drag阻力和Angular Drag角阻力默认0.05左右。这两个值控制速度衰减。做冰面滑行就把Drag调小做水中漂浮就调大。我踩过的坑是给投射物设了较大的Drag结果子弹飞出去没多远就停了排查半天才发现是阻力吃掉了速度。Use Gravity是否受重力。飞行类投射物通常关掉自己做抛物线计算或者用受力控制。Interpolate插值这个参数对视觉流畅度影响巨大。物理以50Hz更新渲染可能是60Hz甚至更高两者不同步就会看到物体“一跳一跳”。开启Interpolate后渲染帧会在两个物理帧之间做插值视觉上就顺滑了。几乎所有玩家能看到的动态物理物体都应该开Interpolate这是性价比极高的一个设置。Collision Detection碰撞检测模式Discrete离散、Continuous连续、Continuous Dynamic连续动态、Continuous Speculative连续推测。默认Discrete高速物体必须改成Continuous或Continuous Dynamic。区别在于Continuous Dynamic会对该物体与其他动态物体之间也做连续检测开销更大但更稳。子弹这类高速小物体我一般直接上Continuous Dynamic。Constraints约束可以冻结某个轴的位移或旋转。做2D横版但用3D物理时冻结Z轴位移和X、Y轴旋转是常规操作能避免物体莫名其妙侧翻。2.2 碰撞体与物理材质摩擦力与弹性的组合逻辑碰撞体Collider负责形状物理材质Physic Material负责表面的摩擦和弹性。物理材质有两个核心参数Dynamic Friction动态摩擦、Static Friction静态摩擦、Bounciness弹性以及Friction Combine和Bounce Combine两个组合模式。组合模式决定了两个物体接触时各自的材质参数怎么合并Average平均、Minimum取小、Maximum取大、Multiply相乘。默认是Average。这里有个实操技巧如果想让某个物体“绝对不弹”把它的Bounciness设为0同时把Bounce Combine设为Minimum这样无论和什么弹性材质接触结果都是0。同理想让角色在斜坡上不滑把摩擦的Combine设为Maximum并给高摩擦值。我做过一个弹球玩法球体材质Bounciness设0.9地面设0.8Combine用Average实际弹跳高度衰减很自然。后来想做一个“超级弹板”就把弹板的Bounciness设1.0且Combine设Maximum球一碰就满血弹起效果很直观。这套组合逻辑理解了弹性表现基本可以不用写代码纯靠配置调出来。2.3 触发器Trigger与碰撞Collision的本质区别Collider上有个Is Trigger勾选项。勾上之后这个碰撞体不再产生物理阻挡只会在其他碰撞体进入、停留、离开时回调OnTriggerEnter/Stay/Exit。不勾则是实体碰撞回调OnCollisionEnter/Stay/Exit。两者的关键差异在于Trigger回调不携带碰撞冲量信息Collision回调携带ContactPoint和冲量。所以需要做“撞击力度判定”比如撞击伤害、破碎效果时必须用Collision只是做“进入区域检测”比如拾取、传送门、伤害区域用Trigger更轻量。还有一个容易忽略的点Trigger事件要触发至少一方必须有Rigidbody。两个纯静态碰撞体都没刚体之间即使勾了Trigger也不会触发回调。这是新手最常见的“触发器不响应”原因我在排查章节会再强调。另外Trigger的Stay回调是每物理帧都调用的如果里面做了重逻辑比如每帧查找组件、每帧实例化性能会很难看。我的习惯是Stay里只做状态标记真正的处理放到Update或者用计时器节流。2.4 射线检测与形状投射精确查询的两种武器射线检测Raycast是物理查询里用得最多的工具从枪械命中判定到地面检测、视线遮挡都靠它。核心API是Physics.Raycast参数里最容易被忽视的是layerMask和QueryTriggerInteraction。layerMask决定射线只和哪些层交互QueryTriggerInteraction决定是否忽略触发器。默认情况下Raycast会命中触发器如果你的场景里有大量触发区域射线可能打在这些看不见的触发器上导致“明明对着墙却打不中”。我一般显式传QueryTriggerInteraction.Ignore除非确实需要检测触发区。形状投射SphereCast、BoxCast、CapsuleCast是射线的“加粗版”适合检测有体积的物体比如角色胶囊体的移动检测。SphereCast的一个经典坑是当球体起始位置已经和碰撞体重叠时它可能检测不到需要配合Physics.OverlapSphere做前置判断。这个细节在角色贴墙移动时特别容易暴露。// 地面检测的典型写法注意layerMask和触发器忽略 public bool CheckGrounded() { float rayLength 0.1f; Vector3 origin transform.position Vector3.up * 0.05f; return Physics.Raycast(origin, Vector3.down, rayLength, LayerMask.GetMask(Environment), QueryTriggerInteraction.Ignore); }上面这段代码里射线起点稍微抬高避免从碰撞体内部发出长度0.1米对应角色脚底到地面的容差层掩码只检测环境层。这几个细节都是实际调出来的直接抄能少走弯路。3. 实操过程与核心环节实现3.1 从零搭建一个稳定的角色物理控制器我按实际搭建顺序走一遍。第一步角色根节点挂RigidbodyMass设70人形量级Drag设0Angular Drag设0.05Use Gravity勾上Interpolate选InterpolateCollision Detection选ContinuousConstraints里冻结X、Z轴旋转防止角色摔倒翻滚除非你要做布娃娃。第二步挂Capsule Collider高度和半径按模型调整注意胶囊体的中心点要对齐角色脚底到头顶的几何中心否则会出现“脚陷进地面”或“悬空”的视觉问题。第三步移动逻辑放在FixedUpdate里用Rigidbody.MovePosition或者直接改velocity。我倾向改velocity因为MovePosition是运动学式的位移会绕过部分物理响应。水平移动用velocity的XZ分量垂直方向保留物理引擎算的重力这样跳跃和下落才自然。void FixedUpdate() { Vector3 move new Vector3(inputX, 0, inputZ).normalized * moveSpeed; Vector3 vel rb.velocity; rb.velocity new Vector3(move.x, vel.y, move.z); }这段代码的关键是保留vel.y只覆盖水平速度。如果直接rb.velocity move重力就被抹掉了角色会悬浮。这个坑我见过太多人踩。第四步地面检测用上一节的Raycast写法配合Coyote Time离开平台后的短暂容错跳跃窗口提升手感。Coyote Time的实现就是记录最后一次着地时间跳跃时判断当前时间减去它是否小于阈值比如0.1秒。3.2 高速投射物的碰撞检测配置与穿透规避子弹、箭矢这类高速小物体是穿透问题的重灾区。我的标准配置流程Rigidbody的Collision Detection设为Continuous DynamicMass设很小0.01Drag设0Use Gravity按需。Collider用Sphere或Capsule半径不要太小太小更容易穿透一般0.05到0.1米。即便如此极高速比如每秒100米以上的投射物仍可能穿透。这时候补一条射线检测在发射时从起点到终点做一次Raycast如果命中就直接结算伤害不等物理碰撞。这叫**hitscan即时命中**方案很多射击游戏对子弹就是这么处理的物理子弹只做视觉表现。// 发射时的即时命中检测 if (Physics.Raycast(muzzle.position, direction, out RaycastHit hit, range, LayerMask.GetMask(Enemy, Environment), QueryTriggerInteraction.Ignore)) { // 结算命中 }实测下来hitscan加物理子弹视觉的组合既保证了命中判定准确又有飞行过程的观感是目前比较稳的方案。纯物理子弹在高速场景下永远有穿透风险别指望调参数能100%解决。3.3 触发器区域的完整实现拾取、伤害区、传送门以拾取物为例走一遍。拾取物挂Collider并勾Is Trigger挂一个脚本处理OnTriggerEnter。玩家角色本身有Rigidbody和Collider所以触发条件满足。脚本里判断进入者的Tag或Layer然后执行拾取逻辑并销毁自身。void OnTriggerEnter(Collider other) { if (other.CompareTag(Player)) { // 加分数、播放音效、销毁 Destroy(gameObject); } }伤害区稍微复杂一点因为需要“持续伤害”。用OnTriggerStay配合计时器每0.5秒结算一次伤害而不是每物理帧都结算。这里有个细节OnTriggerStay在物体静止时也可能不被调用物理引擎的休眠机制所以伤害区最好给进入的物体一个“唤醒”处理或者用OverlapBox定期检测替代。我实际项目里更倾向后者因为可控性更强。传送门则是Enter时把玩家位置瞬移到目标点注意瞬移后要重置刚体速度否则玩家会带着进入时的速度飞出去。瞬移用rb.position赋值然后rb.velocity Vector3.zero。3.4 物理关节与约束的实战用法关节Joint在需要“连接”关系时很有用。Fixed Joint把两个物体焊死Hinge Joint做门和摆锤Spring Joint做弹簧。我用得最多的是Hinge Joint做可开关的门门板挂Rigidbody和Hinge JointconnectedBody设为门框或留空连到世界设置好锚点和轴向然后通过motor驱动或者直接施加力来开关。关节的坑主要在锚点Anchor和连接轴Axis的坐标系上。锚点是相对于自身Rigidbody的局部坐标轴向也是局部方向。设错了表现就是门绕着奇怪的轴转或者直接飞出去。我的调试方法是先在Scene视图里把Gizmo看清楚锚点小球要落在铰链位置轴向箭头要指向旋转轴方向确认无误再运行。Configurable Joint是万能关节但参数极多新手不建议一上来就用。需要复杂约束时先用简单关节组合实在不行再上Configurable Joint并且逐个参数试。4. 常见问题与排查技巧实录4.1 碰撞不触发、触发器不响应的排查顺序这是被问得最多的一类问题。我整理了一个排查顺序按可能性从高到低排查项检查内容常见错误刚体是否存在碰撞双方至少一方有Rigidbody两个静态碰撞体互撞永远不触发层碰撞矩阵两层之间的勾选是否打开新建层默认可能没勾选Is Trigger设置触发器和实体碰撞的回调不同勾了Trigger却监听OnCollision碰撞体尺寸碰撞体是否真的重叠碰撞体缩放为0或位置偏移脚本挂载回调脚本是否挂在有碰撞体的物体上挂在子物体但碰撞体在父物体休眠状态刚体是否处于休眠静止物体需要WakeUp按这个表从上往下查90%的问题能定位。我遇到过一个特别隐蔽的碰撞体在子物体上脚本在父物体上OnCollisionEnter写在父物体脚本里结果因为碰撞体不在同一物体上回调根本没进父脚本。Unity的碰撞回调是发给碰撞体所在物体及其刚体所在物体的跨层级时要理清归属。4.2 物体抖动、穿模、卡墙的成因与解决抖动通常来自三个原因一是物理和渲染不同步开Interpolate能缓解二是碰撞体之间有微小重叠物理引擎每帧都在“推开”它们表现为高频抖动解决方法是调整碰撞体尺寸留出间隙或者用Physics.ComputePenetration做手动分离三是多个力同时作用比如重力加自定义力需要检查力的叠加逻辑。穿模分两种高速穿透前面讲过用CCD和hitscan解决和挤压穿透两个物体互相挤压把薄物体挤穿。挤压穿透的根源是物理求解器的迭代次数不够可以在Project Settings里调高Solver Iterations默认6复杂场景可到10到15代价是性能。我做过一个堆叠玩法物体堆高了就互相穿插把Solver Iterations从6调到12后明显改善。卡墙是角色控制器常见问题本质是胶囊体和墙面碰撞后水平速度被物理响应吃掉角色贴着墙动不了。解决思路是用CapsuleCast预先检测移动方向如果前方有墙就沿墙滑动把速度投影到墙面切向而不是硬顶。这个逻辑写起来不复杂但能极大改善贴墙手感。4.3 性能优化物理开销从哪里省物理性能优化我按收益排序第一减少动态刚体数量能静态的就设Static能休眠的就让它休眠第二简化碰撞体Mesh换基本几何体第三用好层碰撞矩阵关掉无关碰撞对第四控制Fixed Timestep不要盲目调小0.02够用就别改0.01第五射线检测加layerMask别让射线扫全场景。还有一个容易忽略的点OnTriggerStay和OnCollisionStay里的逻辑。这两个回调每物理帧都触发如果里面有GetComponent、Find、实例化这类操作开销会成倍放大。我的做法是把需要的数据在Enter时缓存下来Stay里只做轻量判断。实测数据参考一个场景从200个动态刚体降到80个其余改静态物理耗时从每帧3.2ms降到1.1ms左右再把Mesh Collider全换成Box又降到0.6ms。这个量级在移动端很关键。4.4 物理调试工具与可视化技巧Unity自带的Physics DebuggerWindow Analysis Physics Debugger非常好用能可视化碰撞体、接触点、休眠状态。排查“为什么没碰撞”时先打开它看碰撞体是否真的存在和重叠比盲猜快得多。另外OnCollisionEnter里打印contact points能快速定位碰撞位置void OnCollisionEnter(Collision col) { foreach (ContactPoint p in col.contacts) { Debug.DrawRay(p.point, p.normal, Color.red, 2f); } }这段代码在Scene视图里画出碰撞点和法线方向对不对、位置准不准一目了然。我调弹跳和斜面滑动时经常用这招比看数字直观。最后分享一个习惯给所有物理相关的关键参数质量、摩擦、弹性、检测模式在Inspector里加Tooltip注释写清楚为什么设这个值。项目做大了之后回头调参时这些注释能救命尤其是多人协作时别人一看就知道这个0.9的弹性是有意为之还是随手填的。物理系统和碰撞检测这块说到底就是“理解引擎的时序和规则然后在规则内做配置和补充”。规则本身不复杂复杂的是各种边界情况和组合效应。我个人的体会是遇到物理问题先别急着改代码打开Physics Debugger看一眼实际状态往往问题就暴露了。参数调优没有银弹都是算清楚单步位移、量好碰撞体尺寸、配好层矩阵一步步试出来的。