ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Unity物理系统实战:从碰撞检测到爆炸效果的完整实现与优化

2026/8/10 16:27:10 拓冰建站 浏览量
Unity物理系统实战:从碰撞检测到爆炸效果的完整实现与优化 1. 项目概述当物理遇上游戏从碰撞到爆炸的实战之旅如果你正在用Unity开发游戏尤其是动作、射击、赛车或者任何需要物体交互的类型那么物理系统绝对是你绕不开的核心。很多新手朋友可能会觉得Unity的物理系统是个“黑盒”把刚体Rigidbody和碰撞器Collider一挂物体就能掉下来、能撞开似乎就够用了。但当你真正想实现一个“有分量”的碰撞反馈或者一个看起来“很带感”的爆炸效果时往往会发现事情没那么简单——要么物理表现轻飘飘像纸片要么爆炸效果假得一眼就能看出来性能还可能瞬间崩掉。我自己在项目里就踩过不少坑。比如早期做一个射击游戏子弹打中墙壁期望的是火花四溅并留下弹痕结果要么没反应要么整个墙体莫名其妙被炸飞。又比如实现一个爆炸桶玩家射击后它应该先剧烈膨胀再碎裂冲击波推开周围的杂物。如果只用简单的Instantiate一个预制体然后AddForce效果会非常生硬缺乏那种真实的、分阶段的物理破坏感。所以今天我想结合一个完整的实战案例带你深入Unity物理系统的腹地。我们不止要搞懂OnCollisionEnter和OnTriggerEnter的区别更要弄明白如何利用物理材质Physic Material、关节Joints、射线Raycast甚至刚体插值Interpolation来提升手感。最终的目标是实现一个从精确碰撞检测开始到引发一个包含冲击波、碎片飞溅、受力衰减、屏幕震动等元素的复合型爆炸效果。这个过程会涉及到物理引擎的参数调优、性能考量以及一些“教科书”上不会写的骚操作。无论你是刚接触Unity物理的新手还是想优化现有效果的老手相信这篇都能给你带来可以直接抄作业的干货。2. 核心思路与架构设计为什么不是简单的“挂组件”在动手写代码之前我们必须先理清思路。一个健壮、高效且表现力丰富的物理交互系统绝不是给每个物体挂上刚体和碰撞器就完事的。我们需要一个分层的、可管理的架构。2.1 物理交互的层次化设计我的经验是将物理交互分为三个层次检测层负责“有没有发生接触”。这完全是物理引擎如PhysX的职责通过碰撞器形状Box, Sphere, Mesh等和刚体的位置、速度来计算。我们的代码通过MonoBehaviour里的消息方法如OnCollisionXXX来接收结果。逻辑层负责“发生接触后该怎么办”。这是游戏逻辑的核心我们在这里判断碰撞的双方是谁、碰撞点在哪、力度多大然后决定扣血、播放音效、触发任务等。表现层负责“让玩家看到/听到/感觉到发生了什么”。这是爆炸效果、粒子、屏幕抖动、音效的舞台。这一层必须与逻辑层解耦但又需要紧密配合。很多新手会把这三层的代码全部塞进一个OnCollisionEnter方法里导致脚本臃肿难以调试和复用。我们的架构应该让它们各司其职。2.2 爆炸效果的系统性思考一个真实的爆炸效果在物理层面是多个效应的叠加瞬时冲击在爆炸中心瞬间产生一个极大的、向外的力Force或冲量Impulse。衰减这个力随着距离爆炸中心的距离增加而衰减。通常遵循平方反比或指数衰减规律。多向性力是沿着爆炸中心到物体中心的方向施加的所以物体会被“推开”而不是“向上蹦”。间接影响爆炸可能引发连锁反应比如炸塌一堵墙墙体的碎片又成为新的伤害源。因此我们不能简单地遍历场景中所有刚体然后AddForce。我们需要一个系统来管理爆炸实例计算每个受影响物体的受力并处理可能产生的性能问题比如大量刚体同时运动。2.3 工具选型与性能预考量刚体类型对于动态物体使用Rigidbody对于静态环境如地面、墙壁使用Static或Kinematic的刚体或者干脆只用碰撞器。Kinematic刚体适合由代码完全控制的物体如移动平台它不会受物理力影响但可以影响其他物体。碰撞器形状优先使用基本形状Primitive如Box、Sphere、Capsule。它们的计算效率最高。Mesh Collider虽然贴合模型但性能开销巨大应尽量避免用于频繁移动或参与复杂碰撞的物体。可以尝试用多个基本碰撞器来近似复杂形状。物理材质这是调节碰撞“手感”的关键。通过调整Friction摩擦力和Bounciness弹性你可以让物体感觉像冰面一样滑或者像皮球一样弹。为不同材质的物体金属、木头、橡胶配置不同的物理材质能极大提升真实感。射线与形状投射除了碰撞我们经常需要主动探测比如判断手雷的投掷轨迹是否会被阻挡或者爆炸的冲击波是否“看得到”某个物体。这时就需要用到Physics.Raycast或Physics.SphereCast。注意在移动平台或WebGL上物理计算是主要的性能瓶颈之一。在设计初期就要严格控制动态刚体的数量并善用Rigidbody的Sleep状态当物体静止时物理引擎会使其“休眠”以节省计算。3. 碰撞检测的深度解析与精细化实现碰撞检测是物理交互的基石。Unity提供了两套机制碰撞Collision和触发Trigger。用错场景是新手最常见的错误之一。3.1 碰撞 vs. 触发本质区别与选用准则碰撞Collision两个物体都带有非触发器的碰撞器Collider和至少一个刚体Rigidbody。物理引擎会计算它们之间的接触力阻止它们相互穿透并产生一个真实的物理反馈如反弹。你会收到OnCollisionEnter/Stay/Exit消息。适用场景需要物理模拟的实体交互。如小球落地、汽车撞墙、角色被怪物击中。代码示例void OnCollisionEnter(Collision collision) { // collision.contacts 包含碰撞点信息数组 // collision.impulse 包含碰撞冲量可用于判断碰撞强度 ContactPoint contact collision.contacts[0]; Vector3 hitPoint contact.point; Vector3 hitNormal contact.normal; // 例如在碰撞点生成一个火花特效 Instantiate(sparkEffect, hitPoint, Quaternion.LookRotation(hitNormal)); }触发Trigger将碰撞器的Is Trigger属性勾选。此时物理引擎会忽略它们的物理阻挡但依然会检测它们的重叠。你会收到OnTriggerEnter/Stay/Exit消息。适用场景需要检测区域重叠但无需物理阻挡。如拾取物品、进入关卡触发区、技能范围检测。代码示例void OnTriggerEnter(Collider other) { if (other.CompareTag(Pickup)) { Destroy(other.gameObject); playerScore; } }核心准则如果你需要物体被“挡住”或者产生力的相互作用用碰撞如果只需要知道“进入了某个区域”用触发。混淆使用会导致奇怪的物理bug比如物体鬼畜抖动或者穿模。3.2 碰撞信息的深度挖掘与利用Collision对象是一个信息宝库但很多人只用了collision.gameObject。collision.contacts: 这是一个ContactPoint数组。一次碰撞可能发生在多个点比如一个盒子的一角撞进缝隙。通常取第一个点contacts[0]作为主要碰撞点。ContactPoint.point是世界坐标的碰撞点normal是碰撞表面的法线方向。这个法线对于生成贴合的粒子效果如火花、弹痕至关重要。Quaternion.LookRotation(normal)可以让你得到一个让特效“贴在”表面的旋转。collision.impulse: 本次碰撞的冲量向量。它的magnitude大小可以用来近似衡量碰撞的剧烈程度。你可以用它来决定播放多大的撞击声或者是否要触发一个“重击”的特效。collision.relativeVelocity: 两个碰撞物体之间的相对速度。这对于计算碰撞伤害非常有用。比如两辆高速相向而行的车相撞相对速度很大伤害就应该高。3.3 实战技巧优化碰撞检测性能与精度层级碰撞矩阵Layer Collision Matrix这是Unity里被严重低估的功能。你可以在Edit - Project Settings - Physics中找到它。通过给物体分配不同的Layer如Player, Enemy, Bullet, Environment并在这个矩阵中勾选哪些Layer之间需要检测碰撞你可以精确控制物理计算的范围。务必禁用所有不必要的交叉检测比如UI层和Environment层之间完全没有必要检测碰撞。这能直接减少物理引擎每帧的计算量。碰撞器尺寸与形状优化永远让碰撞器略小于视觉模型Mesh。这可以避免物体因为浮点精度问题看起来“还没碰到”就发生了碰撞。对于角色使用胶囊体Capsule Collider通常比盒子更符合人体形状且更稳定。对于复杂的静态场景考虑使用Physics.BakeMesh将多个网格碰撞器合并或者使用更简单的代理碰撞体。善用Physics.queriesHitTriggers和Physics.queriesHitBackfaces在脚本中可以通过这些全局设置来控制射线或形状投射是否会检测到触发器或者是否检测网格的背面。在某些特定需求下如子弹只检测第一个实体的正面调整它们能避免意外情况。4. 构建一个可复用、可配置的爆炸系统现在我们进入最激动人心的部分爆炸。我们将构建一个ExplosionSystem脚本它不仅仅是一个效果播放器更是一个物理事件发生器。4.1 爆炸系统的核心参数设计首先我们定义一个ExplosionProfile脚本ableObject用于配置不同类型的爆炸如手雷、火箭筒、汽油桶。// ExplosionProfile.cs [CreateAssetMenu(fileName New Explosion Profile, menuName Physics/Explosion Profile)] public class ExplosionProfile : ScriptableObject { [Header(基础设置)] public float radius 5f; // 爆炸半径 public float baseForce 700f; // 基础力大小 public AnimationCurve forceFalloff AnimationCurve.EaseInOut(0f, 1f, 1f, 0.1f); // 力随距离衰减曲线 public LayerMask affectedLayers -1; // 影响哪些层 [Header(伤害与效果)] public float baseDamage 50f; public AnimationCurve damageFalloff; // 伤害衰减曲线 public GameObject explosionEffectPrefab; // 视觉特效预制体 public AudioClip explosionSound; [Header(高级选项)] public float upwardModifier 0.5f; // 向上的修正力让物体飞起来一点 public ForceMode forceMode ForceMode.Impulse; // 力的模式Impulse(瞬时冲量)或Force(持续力) public bool explodeInFixedUpdate true; // 是否在FixedUpdate中执行保证物理稳定性 }关键点解析AnimationCurve用于衰减这是实现真实感的关键。平方反比衰减力与距离的平方成反比可以用一条从(0,1)快速下降到(1,0)的曲线来近似。通过可视化曲线编辑器你可以轻松调整爆炸的“柔和”或“锐利”程度。比如汽油桶爆炸可能希望近处威力极大但衰减很快而冲击波手雷则希望有一个更平缓的衰减影响范围更广。LayerMask确保爆炸只影响你想要的物体比如忽略UI、忽略触发器等。ForceModeForceMode.Impulse是添加一个瞬时冲量适合爆炸这种瞬间事件。ForceMode.Force是添加一个持续的力如果你在做持续性的推力场如风扇可以用这个。4.2 爆炸力的精确计算与施加接下来是ExplosionSystem的核心方法。我们使用Physics.OverlapSphere来获取爆炸半径内的所有碰撞体。// ExplosionSystem.cs public void ExecuteExplosion(Vector3 center, ExplosionProfile profile) { // 1. 播放视觉与听觉效果 if (profile.explosionEffectPrefab ! null) Instantiate(profile.explosionEffectPrefab, center, Quaternion.identity); if (profile.explosionSound ! null) AudioSource.PlayClipAtPoint(profile.explosionSound, center); // 2. 物理计算与施加建议在FixedUpdate中调用此部分 Collider[] hitColliders Physics.OverlapSphere(center, profile.radius, profile.affectedLayers); foreach (Collider hitCollider in hitColliders) { Rigidbody rb hitCollider.attachedRigidbody; // 如果碰撞体没有刚体或者刚体是运动学Kinematic的则跳过 if (rb null || rb.isKinematic) continue; // 计算距离和方向 Vector3 direction (hitCollider.transform.position - center).normalized; float distance Vector3.Distance(hitCollider.transform.position, center); float normalizedDistance Mathf.Clamp01(distance / profile.radius); // 归一化距离 [0,1] // 根据衰减曲线计算实际力和伤害 float forceMultiplier profile.forceFalloff.Evaluate(normalizedDistance); float damageMultiplier profile.damageFalloff.Evaluate(normalizedDistance); float actualForce profile.baseForce * forceMultiplier; float actualDamage profile.baseDamage * damageMultiplier; // 施加力考虑向上修正 Vector3 forceVector direction * actualForce; forceVector.y profile.upwardModifier * actualForce; // 添加一点向上的力 rb.AddForce(forceVector, profile.forceMode); // 传递伤害需要目标物体有如IDamageable的接口 IDamageable damageable hitCollider.GetComponentIDamageable(); if (damageable ! null) { damageable.TakeDamage(actualDamage, center); } // 3. 可选高级处理破碎效果 FractureObject fracture hitCollider.GetComponentFractureObject(); if (fracture ! null normalizedDistance 0.3f) // 距离很近时触发破碎 { fracture.Shatter(actualForce, center); } } // 4. 可选屏幕震动 CameraShake.Instance?.Shake(profile.baseForce * 0.001f, 0.3f); }实操心得性能Physics.OverlapSphere是一个物理查询开销不小。避免在同一帧内进行大量爆炸。对于性能敏感的场景可以考虑使用对象池Object Pooling来管理爆炸效果实例或者将非关键的视觉特效如远端爆炸的碎片的细节等级降低。力的施加点上面的代码将力施加在刚体的质心rb.AddForce。这会产生一个纯粹的线性力物体会被推开。如果你希望物体还能旋转起来比如被炸飞的木板可以考虑使用rb.AddForceAtPosition在碰撞点施加力这样会产生一个扭矩Torque让物体旋转。这会让爆炸效果看起来更真实。归一化距离将距离除以半径得到0到1之间的值再用这个值去采样衰减曲线。这是处理各种半径爆炸的通用方法。4.3 提升真实感的进阶技巧冲击波传播效果一个真实的爆炸冲击波是逐渐扩散的。我们可以用协程Coroutine来模拟IEnumerator SimulateShockwave(Vector3 center, float maxRadius, float speed) { float currentRadius 0f; while (currentRadius maxRadius) { // 每一帧对当前半径的一个薄壳范围内的物体施加力力较小 // 可以使用Physics.OverlapSphere但设置一个很小的厚度如currentRadius-0.1f, currentRadius // 或者更高效地使用Physics.SphereCast非分配版本Physics.SphereCastNonAlloc currentRadius speed * Time.deltaTime; // ... 对薄壳内物体施加力 ... yield return null; // 下一帧继续 } }这种方法计算量更大但视觉效果特别是配合一个逐渐放大的透明球体特效极其震撼。碎片与二次爆炸被炸飞的碎片通过FractureObject生成本身应该带有刚体和碰撞器。并且这些碎片如果本身是易燃易爆物比如另一个汽油桶的碎片在受到足够大的冲击时应该能触发二次爆炸。这需要在碎片上挂载一个脚本检测受到的力或碰撞并判断是否达到引爆阈值。音效的空间化与衰减使用AudioSource.PlayClipAtPoint虽然简单但缺乏方向感。对于重要的爆炸最好实例化一个带有AudioSource的GameObject并设置其spatialBlend为1完全3D音效rolloffMode为Logarithmic对数衰减这样玩家就能通过耳机听出爆炸的方向和远近。5. 性能优化与常见问题深度排查物理效果很酷但也是性能杀手。下面是一些实战中总结的优化和排错经验。5.1 性能优化清单控制动态刚体数量这是铁律。屏幕上同时活动的动态刚体最好不超过50-100个取决于平台。对于爆炸产生的大量碎片可以考虑使用简化碰撞体碎片用Sphere或Box代替Mesh Collider。设置自动销毁碎片在飞行几秒后如果速度降到阈值以下就将其刚体设为Kinematic然后过一会儿再销毁。或者直接使用对象池回收。降低物理更新频率在Project Settings - Time中可以适当调低Fixed Timestep如从0.02调到0.04但这会影响物理精度和手感需谨慎测试。善用Rigidbody属性Sleep Threshold睡眠阈值当刚体速度低于此值时会进入休眠停止物理计算。对于碎片可以适当调高这个值让它们更快“睡着”。Collision Detection碰撞检测对于高速运动的物体如子弹使用Continuous连续检测或Continuous Dynamic防止穿模。对于低速或一般物体使用Discrete离散检测即可性能更好。Interpolation插值如果刚体运动有抖动可以尝试开启Interpolate。它通过渲染帧之间的平滑来改善视觉表现但会消耗额外内存。使用Physics.OverlapSphereNonAlloc这是Physics.OverlapSphere的高性能版本。它需要你预先分配一个Collider[]数组作为缓冲区避免GC垃圾回收分配。对于每帧都需要进行的爆炸检测比如持续燃烧区域必须使用这个方法。private Collider[] overlapBuffer new Collider[50]; // 预分配缓冲区 void Update() { int numColliders Physics.OverlapSphereNonAlloc(transform.position, radius, overlapBuffer, layerMask); for (int i 0; i numColliders; i) { // 处理 overlapBuffer[i] } }5.2 常见问题与解决方案速查表问题现象可能原因解决方案物体爆炸后抖动或抽搐1. 多个碰撞体形状复杂穿透后物理引擎反复纠正。2. 爆炸力施加过于频繁每帧都在加力。3. 刚体质量Mass差异过大小物体被大物体撞飞后速度过快。1. 简化碰撞体确保它们初始位置不穿透。2. 确保爆炸力只施加一次使用Invoke或协程控制。3. 统一或合理设置场景中刚体的质量比例避免极端差异。使用Mathf.Clamp限制刚体的最大速度。爆炸效果“很假”物体像同时被无形的手推走力的计算没有考虑距离衰减或者衰减曲线设置不当。严格按照4.2节的方法使用AnimationCurve和归一化距离来计算衰减力。调整曲线形状让近处力大远处力小。帧率在爆炸瞬间骤降1. 瞬时实例化过多特效粒子、碎片模型。2.OverlapSphere一次性检测到过多物体且每个物体都进行了复杂计算。3. 碎片刚体过多物理计算暴增。1. 使用对象池管理爆炸特效和碎片。2. 分帧处理爆炸影响将受影响物体列表存入数组用协程分几帧来处理施加力的逻辑。3. 为碎片设置更短的存活时间和更快的睡眠阈值。碰撞检测有时失灵高速穿透物体移动速度太快在物理更新间隔FixedUpdate之间移动距离超过了自身尺寸导致离散检测Discrete失效。为高速物体子弹、炮弹的刚体将Collision Detection设置为Continuous或Continuous Dynamic。同时可以考虑使用Raycast或SphereCast进行预测性的碰撞检测。爆炸后碎片悬空或缓慢下落碎片刚体进入了“睡眠”状态但后续没有力唤醒它。或者场景中持续存在微小的力如风区阻止其睡眠。检查是否错误地对碎片施加了持续的力ForceMode.Force。确保爆炸后除了重力没有其他持续力作用。可以适当调低刚体的Sleep Threshold。OnCollisionEnter不被调用1. 双方都是触发器Is Trigger。2. 其中一方没有刚体。3. 碰撞层级矩阵Layer Collision Matrix中双方层级未勾选交互。4. 刚体被设置为Kinematic且另一方也是Kinematic或静态碰撞体。1. 确认是否需要物理反应不需要则用OnTriggerEnter需要则取消触发器勾选。2. 确保至少一方有非运动学刚体。3. 去Physics Settings中检查层级碰撞矩阵。4.Kinematic刚体不会因碰撞触发OnCollisionEnter但可以通过代码移动它去触发其他物体的OnCollisionEnter。6. 从理论到实践一个手雷爆炸的完整案例让我们把所有知识串联起来实现一个经典的FragGrenade破片手雷。步骤分解手雷本体Grenade.prefab组件Rigidbody(质量0.3使用重力Drag设为0.5增加空气阻力感)Sphere Collider(半径0.1非触发器)。脚本Grenade.cs。负责投掷逻辑、碰撞检测碰到地面或敌人后触发延时爆炸、倒计时。public class Grenade : MonoBehaviour { public ExplosionProfile explosionProfile; // 配置爆炸参数 public float fuseTime 3f; // 引信时间 private bool hasExploded false; void Start() { Invoke(nameof(Explode), fuseTime); } void OnCollisionEnter(Collision collision) { // 如果碰撞很剧烈或者碰到敌人立即引爆 if (collision.impulse.magnitude 5f || collision.gameObject.CompareTag(Enemy)) { CancelInvoke(nameof(Explode)); Explode(); } } void Explode() { if (hasExploded) return; hasExploded true; ExplosionSystem.Instance.ExecuteExplosion(transform.position, explosionProfile); // 隐藏模型播放一段延迟消失的音频尾音然后销毁或回收入对象池 GetComponentMeshRenderer().enabled false; Destroy(gameObject, 2f); } }爆炸系统单例ExplosionSystem.cs设计为单例模式方便全局访问。包含上文ExecuteExplosion方法。在Awake中预加载一个爆炸特效和音效的对象池。爆炸效果配置ExplosionProfile_Frag.assetradius: 8baseForce: 1200forceFalloff: 一条从(0,1)陡峭下降到(0.5, 0.2)再平缓到(1,0)的曲线。baseDamage: 80damageFalloff: 类似力衰减但中远距离伤害降为0。upwardModifier: 0.3 (让物体稍微飞起)。affectedLayers: 包含Enemy,Destructible,PhysicsObject但不包含Player友军伤害看设计和IgnoreRaycast。被影响物体敌人挂载IDamageable接口的实现在TakeDamage方法里处理扣血和死亡。可破坏木箱挂载FractureObject脚本和IDamageable。当受到伤害超过阈值时调用Shatter方法将自己替换为一系列预破碎的碎片模型每个碎片都有刚体和碰撞器。碎片碎片预制体上有一个Debris脚本Start时给自己一个随机的小扭矩让旋转更自然并设置Invoke(nameof(StopPhysics), 3f)3秒后将刚体设为Kinematic并停止所有粒子等待对象池回收。最终效果玩家投出手雷手雷在空中划过抛物线落地碰撞或计时结束。ExplosionSystem在爆炸点生效根据配置的曲线计算范围内每个刚体受到的力和伤害。近处的敌人被炸飞并受到高额伤害木箱破碎成多个飞溅的碎片碎片在飞行后逐渐静止。屏幕伴随一次短促的震动配合震撼的音效和粒子特效一个富有冲击感的爆炸就完成了。这个案例的关键在于将所有环节参数化、可配置。通过调整ExplosionProfile你可以轻松创造出燃烧弹、冲击波手雷、黏性炸弹等不同变体而无需修改核心代码。这种数据驱动的设计对于迭代和平衡游戏体验至关重要。