Unity碰撞检测从入门到精通:核心原理、代码实现与性能优化
1. 项目概述碰撞检测在Unity中的核心地位如果你刚开始接触Unity或者已经做了一些简单的移动、旋转那么“碰撞检测”绝对是你从“玩具Demo”迈向“可交互游戏”的第一个关键门槛。我刚开始学Unity那会儿觉得让一个方块动起来挺简单但怎么让它撞到墙停下来或者踩到陷阱掉血这个问题困扰了我好几天。后来才明白这背后就是一套叫做“物理系统”的机制在起作用而碰撞检测就是这套机制的“感官系统”。简单来说碰撞检测就是让游戏世界中的物体“知道”自己碰到了谁。没有它你的角色会穿墙而过子弹会无视敌人宝箱也永远打不开。Unity为我们封装好了一套非常成熟的物理引擎我们开发者要做的就是理解它的规则并学会用代码去“监听”碰撞事件。这不仅仅是写几行OnCollisionEnter那么简单里面涉及到刚体Rigidbody、碰撞器Collider、触发器Trigger、物理材质Physics Material等一系列组件的协同工作以及它们背后性能开销的权衡。这篇笔记我会结合自己踩过的无数个坑把Unity碰撞检测从入门到能用的核心要点掰开揉碎了讲。我会重点解释“为什么”要这么设置而不仅仅是“怎么做”。比如为什么有时候碰撞没反应为什么物体会抖动触发器Trigger和碰撞器Collider到底该用哪个理解了这些你才能灵活应对各种游戏需求无论是做一个平台跳跃游戏还是一个需要精确物理模拟的弹球游戏。2. 碰撞检测的核心原理与组件拆解在Unity中实现碰撞检测本质上是在协调三个核心部分刚体Rigidbody、碰撞器Collider和物理引擎。你可以把它们想象成一个协作团队碰撞器定义了物体的“形状边界”刚体赋予了物体“物理属性”如质量、重力而物理引擎则是背后的“裁判”每帧计算这些边界和属性判断它们是否相交并决定如何反应反弹、滑动、停止。2.1 碰撞器Collider物体的“形状外壳”碰撞器组件定义了游戏对象在物理世界中的体积和形状。Unity提供了多种基本碰撞器Box Collider盒型碰撞器最常用性能最好适用于箱子、墙壁、地面等方形物体。Sphere Collider球形碰撞器适用于球、弹丸等。Capsule Collider胶囊碰撞器非常适合用于角色控制器因为它结合了圆柱的连贯性和球体的平滑顶端能很好地处理斜坡和台阶。Mesh Collider网格碰撞器可以完全贴合3D模型的复杂形状但性能开销最大通常只用于静态且形状不规则的环境物体如岩石、复杂地形。注意对于会移动的物体尤其是大量生成的物体如子弹、敌人绝对不要使用Mesh Collider。它的计算成本太高会严重拖慢游戏。对于复杂形状的动态物体通常的做法是使用多个简单的Box/Sphere Collider来“拼凑”出近似形状这被称为“复合碰撞器”。每个碰撞器都有一个Is Trigger复选框。这是碰撞检测中第一个关键抉择点。勾选它碰撞器就变成了一个“幽灵”物理引擎不会对它进行真实的物理碰撞计算没有反弹、阻挡但它仍然可以检测到与其他碰撞器的重叠并通过代码通知我们。这用于需要感知区域但不需要物理阻挡的场景比如收集品、陷阱触发区域、剧情触发点。2.2 刚体Rigidbody物体的“物理实体”刚体组件告诉物理引擎“这个物体需要受物理定律支配”。只有至少一个碰撞交互方拥有刚体碰撞检测才会发生。刚体负责管理物体的质量、阻力、重力影响并响应物理力如AddForce。这里有一个非常重要的概念运动学Kinematic与非运动学。在刚体组件上你可以勾选Is Kinematic。勾选后该物体将不受物理引擎的力重力、碰撞力影响但你仍然可以通过直接修改它的Transform位置、旋转来移动它并且它能够检测到与其他刚体的碰撞。这常用于玩家控制的角色当你用代码控制移动时、移动的平台或者由动画驱动的物体。一个黄金法则在典型的碰撞场景中比如一个球撞墙至少需要一方有刚体且非Kinematic另一方至少有碰撞器。如果双方都有非Kinematic刚体它们会相互影响如果一方是Kinematic刚体它可以去撞动非Kinematic刚体但自己不会被撞动。2.3 物理层的碰撞矩阵Layer Collision Matrix这是新手最容易忽略但也最可能导致“明明有碰撞器却没反应”问题的地方。Unity允许你为物体设置图层Layer并在Edit - Project Settings - Physics中定义一个矩阵来决定哪些层之间的物体会发生碰撞。例如你可以设置“Player”层和“Enemy”层相互碰撞但“Player”层和“Player”层之间不碰撞防止玩家挤在一起。或者让“Bullet”层只与“Enemy”和“Environment”层碰撞而忽略“Player”层如果是友军火力。实操心得项目一开始就规划好物理层。通常我会定义这些层Default, Player, Enemy, Bullet, Environment, TriggerOnly, IgnoreRaycast等。然后在碰撞矩阵中仔细配置这能避免大量意外的物理交互和性能浪费。3. 碰撞与触发两种检测方式的代码实现与选择理解了组件我们来看代码如何响应。Unity通过消息方法Message来通知我们碰撞事件。你需要把这些方法写在挂载了碰撞器或刚体的物体所附加的脚本中。3.1 碰撞检测OnCollisionEnter/Stay/Exit当两个物体都没有勾选Is Trigger且根据碰撞矩阵应该发生物理碰撞时这些方法会被调用。using UnityEngine; public class CollisionExample : MonoBehaviour { // 当碰撞开始时调用一次第一帧接触 void OnCollisionEnter(Collision collisionInfo) { // collisionInfo 包含丰富的碰撞信息 Debug.Log(gameObject.name 撞到了 collisionInfo.gameObject.name); // 获取接触点通常是一个数组因为碰撞可能涉及多个点 ContactPoint contact collisionInfo.contacts[0]; Debug.Log(接触点位置: contact.point); // 获取碰撞的相对速度可用于计算伤害等 float impactSpeed collisionInfo.relativeVelocity.magnitude; Debug.Log(撞击速度: impactSpeed); // 例如如果撞到敌人销毁自己 if (collisionInfo.gameObject.CompareTag(Enemy)) { Destroy(gameObject); } } // 当碰撞持续时每帧调用 void OnCollisionStay(Collision collisionInfo) { // 常用于持续性的效果比如站在岩浆上持续掉血 // Debug.Log(持续碰撞中...); } // 当碰撞分离时调用一次 void OnCollisionExit(Collision collisionInfo) { Debug.Log(gameObject.name 离开了 collisionInfo.gameObject.name); } }关键点解析Collision参数这是碰撞信息的宝库。除了上面用到的collisionInfo.rigidbody可以获取对方刚体可能为nullcollisionInfo.collider获取对方碰撞器。谁调用这些方法在发生碰撞的两个物体的脚本上都可能被调用只要该脚本挂在了物体上。例如球撞墙球上的脚本的OnCollisionEnter和墙上的脚本的OnCollisionEnter都会触发。3.2 触发检测OnTriggerEnter/Stay/Exit当至少有一个碰撞器勾选了Is Trigger且它们的边界发生重叠时这些方法会被调用。没有物理阻挡效果。using UnityEngine; public class TriggerExample : MonoBehaviour { // 当触发开始时调用一次 void OnTriggerEnter(Collider otherCollider) { Debug.Log(gameObject.name 进入了 otherCollider.gameObject.name 的触发区域); // 典型应用拾取物品 if (otherCollider.CompareTag(PickUp)) { // 增加分数、生命值等 GameManager.Instance.AddScore(10); // 销毁被拾取的物品 Destroy(otherCollider.gameObject); } // 典型应用进入陷阱区域 if (otherCollider.CompareTag(Trap)) { // 玩家扣血 PlayerHealth health GetComponentPlayerHealth(); if (health ! null) { health.TakeDamage(5); } } } // 当触发持续时每帧调用 void OnTriggerStay(Collider otherCollider) { // 例如站在治疗区域内持续回血 if (otherCollider.CompareTag(HealingZone)) { PlayerHealth health GetComponentPlayerHealth(); if (health ! null) { health.Heal(1 * Time.deltaTime); // 每秒回复1点生命值 } } } // 当触发结束时调用一次 void OnTriggerExit(Collider otherCollider) { Debug.Log(gameObject.name 离开了 otherCollider.gameObject.name 的触发区域); // 例如离开安全区提示 if (otherCollider.CompareTag(SafeZone)) { UIManager.Instance.ShowMessage(已离开安全区); } } }关键点解析Collider参数这里传入的是对方的碰撞器组件otherCollider。要获取对方游戏对象需要用otherCollider.gameObject。性能注意OnTriggerStay每帧都会调用如果里面有复杂的计算如查找组件、物理计算务必做好优化。可以使用一个计时器来降低检测频率而不是每帧都执行。3.3 如何选择碰撞 vs 触发这个选择取决于你的游戏需求特性碰撞检测触发检测物理反应有物体会被阻挡、反弹无物体会直接穿过性能开销较高需计算物理反馈较低只检测重叠适用场景墙壁、地面、需要物理交互的物体球、箱子收集品、陷阱区域、传感器、剧情触发器、技能范围代码方法OnCollisionXXXOnTriggerXXX参数类型CollisionCollider经验之谈对于玩家角色控制器我通常使用胶囊碰撞器刚体Is Kinematic勾选并用代码控制移动。这样既能通过OnCollisionEnter检测到与墙壁的碰撞用于处理滑墙、蹬墙跳等又能通过触发器检测地面OnTriggerStay判断是否着地实现起来非常灵活。而敌人的子弹如果不需要物理反弹通常设为触发器只用于检测命中。4. 高级应用与性能优化实战掌握了基础我们来看看如何在实际项目中用好碰撞检测并避开性能陷阱。4.1 射线检测Raycasting精准的单点检测有时候你不需要两个体积的碰撞只需要知道“前方有没有东西”。比如玩家射击、鼠标点击拾取物体、判断角色是否着地从脚底向下发射短射线。这就是射线检测的用武之地。using UnityEngine; public class RaycastExample : MonoBehaviour { public float raycastDistance 10f; public LayerMask groundLayer; // 在Inspector中指定只检测“Ground”层 void Update() { // 从当前物体位置向下发射一条射线 Ray ray new Ray(transform.position, Vector3.down); RaycastHit hitInfo; // 用于存储射线击中的信息 // 发射射线并只检测指定层 if (Physics.Raycast(ray, out hitInfo, raycastDistance, groundLayer)) { Debug.Log(射线击中了: hitInfo.collider.gameObject.name); Debug.Log(击中点距离: hitInfo.distance); // 可以在击中点画一个调试球 Debug.DrawRay(transform.position, Vector3.down * hitInfo.distance, Color.green); } else { Debug.DrawRay(transform.position, Vector3.down * raycastDistance, Color.red); } // 另一种常用写法从摄像机向鼠标位置发射射线用于点击选择 if (Input.GetMouseButtonDown(0)) { Ray camRay Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(camRay, out RaycastHit mouseHit, 100f)) { Debug.Log(点击到了: mouseHit.collider.gameObject.name); // 可能触发一个UI面板或者让角色移动到该点 } } } }性能提示Physics.Raycast是有开销的尤其是在每帧对多个物体进行检测时。对于需要持续检测的情况如AI的视野可以考虑每几帧检测一次而不是每帧都检测。4.2 物理材质Physics Material控制碰撞行为碰撞的“感觉”可以通过物理材质来调整。创建一个物理材质Assets - Create - Physics Material你可以设置Dynamic Friction / Static Friction动/静摩擦值越大越难滑动。冰面摩擦力小橡胶摩擦力大。Bounciness弹性0为无弹性1为完全弹性能量不损失。弹球需要高弹性。Friction Combine / Bounce Combine摩擦/弹性组合模式当两个物体材质不同时如何计算最终的摩擦力和弹性。有Average平均、Multiply相乘、Minimum取小、Maximum取大等模式。将创建好的物理材质拖拽到碰撞器组件的Material属性上即可生效。4.3 刚体休眠Rigidbody Sleeping与性能物理引擎为了优化会让静止不动的刚体进入“休眠”状态不再参与每帧的物理计算。当有外力作用时它会自动“唤醒”。这是一个非常重要的自动优化机制。你需要做的是避免频繁地、每帧去移动或旋转一个非Kinematic的刚体例如在Update里直接transform.position。这会强制刚体每帧唤醒破坏休眠导致性能下降。正确的做法是通过rigidbody.AddForce()或rigidbody.MovePosition()来施加力或进行插值移动。对于确定永远静止的物体如地形、建筑不要添加刚体或者添加刚体后勾选Is Kinematic并将其置于静态碰撞层。4.4 碰撞矩阵与查询检测Overlap的精细控制除了之前提到的层碰撞矩阵你还可以在代码中更精细地控制碰撞检测。Physics.OverlapSphere: 检测指定球体内的所有碰撞器。// 检测周围5个单位内的所有敌人 Collider[] hitColliders Physics.OverlapSphere(transform.position, 5f, LayerMask.GetMask(Enemy)); foreach (var hitCollider in hitColliders) { // 处理每一个检测到的敌人 hitCollider.GetComponentEnemy().TakeDamage(10); }这常用于爆炸范围、技能AOE范围效果的瞬间检测。忽略特定碰撞有时你需要临时让两个物体穿透比如角色死亡后的无敌时间。可以通过Physics.IgnoreCollision来实现。public Collider playerCollider; public Collider enemyCollider; // 在某个时机调用例如角色获得无敌状态时 Physics.IgnoreCollision(playerCollider, enemyCollider, true); // 忽略碰撞 // ... 无敌时间结束 Physics.IgnoreCollision(playerCollider, enemyCollider, false); // 恢复碰撞5. 常见问题排查与实战避坑指南这里汇总了我自己和很多初学者常遇到的问题以及解决方法。5.1 问题一碰撞/触发事件根本没有触发这是最常见的问题。请按以下清单逐一排查双方都有碰撞器吗至少两个物体都要有碰撞器组件。至少一方有刚体吗对于碰撞检测至少一方要有非Kinematic刚体。对于触发检测至少一方有刚体Kinematic或非Kinematic均可。碰撞器是触发器吗如果希望触发OnTriggerXXX确保至少一个碰撞器勾选了Is Trigger如果希望触发OnCollisionXXX确保两个碰撞器都没勾选Is Trigger。物理层设置正确吗检查Edit - Project Settings - Physics确认两个物体所在的层Layer在碰撞矩阵Collision Matrix中是打勾的允许碰撞。物体在运动吗如果两个物体在游戏开始时就已经静态重叠了OnTriggerEnter或OnCollisionEnter可能不会在启动时调用。通常需要至少一方移动一下才会触发。对于静态重叠的触发器可以在Start()方法中手动用Physics.OverlapBox等查询一次。脚本挂对了吗包含事件方法的脚本必须挂在发生碰撞的其中一个游戏对象上。5.2 问题二物体碰撞后剧烈抖动或穿透移动方式错误如果你在Update中通过transform.Translate或直接修改transform.position来移动一个带有非Kinematic刚体的物体就会和物理引擎的计算冲突导致抖动或穿透。正确做法是对于需要物理模拟的物体受重力、碰撞影响使用Rigidbody.AddForce()对于需要精确控制位置、不受物理力影响的物体如玩家角色给刚体勾选Is Kinematic然后在FixedUpdate中使用Rigidbody.MovePosition()。碰撞器尺寸或位置不对检查碰撞器组件的大小Size和中心Center是否与模型可视部分匹配。一个比模型小很多的碰撞器会导致视觉上撞到了但物理上还没碰到。速度过快如果物体移动速度极快每帧移动距离超过自身尺寸可能会在物理更新间隔内“穿越”另一个薄碰撞器。这被称为“子弹穿透”问题。解决方案增加碰撞器的厚度。使用Rigidbody.interpolation插值来平滑运动。对于子弹等高速物体改用射线检测从上一帧位置到当前帧位置发射射线来代替碰撞检测。5.3 问题三OnTriggerStay调用不稳定或卡顿OnTriggerStay在物体重叠期间每帧的物理更新FixedUpdate都会调用。如果帧率波动调用间隔也会波动。如果里面逻辑复杂会导致卡顿。优化技巧在方法内部使用计时器而不是每帧都执行核心逻辑。private float damageInterval 0.5f; // 每0.5秒伤害一次 private float timer; void OnTriggerStay(Collider other) { if (other.CompareTag(Player)) { timer Time.deltaTime; if (timer damageInterval) { timer 0; other.GetComponentPlayerHealth().TakeDamage(1); // 执行伤害逻辑 } } } void OnTriggerExit(Collider other) { timer 0; // 离开区域重置计时器 }5.4 问题四如何获取碰撞的精确点和法线这在制作打击特效、声音根据撞击点播放时非常有用。Collision对象的contacts数组存储了所有接触点信息。void OnCollisionEnter(Collision collision) { foreach (ContactPoint contact in collision.contacts) { Debug.DrawRay(contact.point, contact.normal, Color.white, 2f); // 画出法线 // 在接触点生成一个火花特效 // Instantiate(sparkEffect, contact.point, Quaternion.LookRotation(contact.normal)); } // 通常取第一个接触点就够了 Vector3 hitPoint collision.contacts[0].point; Vector3 hitNormal collision.contacts[0].normal; }5.5 性能优化终极清单简化碰撞形状动态物体用Box/Sphere/Capsule静态复杂物体才用Mesh Collider并尽量使用简化的碰撞网格在模型导入设置中生成。善用图层通过图层碰撞矩阵禁用不必要的碰撞对如子弹和子弹之间。让刚体休眠避免不必要的力或位移打扰静态刚体。减少每帧的物理查询将射线检测、Overlap检测的频率降低例如每3帧一次。控制物理更新频率在Project Settings - Time中Fixed Timestep决定了物理更新的频率。默认0.02秒50Hz。增加此值如0.04秒会降低物理精度但提升性能适用于对物理要求不高的游戏。降低此值则相反。使用触发器代替复杂碰撞如果不需要物理反馈只用触发器检测事件性能更好。碰撞检测是Unity物理交互的基石初学时会觉得规则繁多但一旦理清“刚体-碰撞器-图层-事件方法”这条主线并理解碰撞与触发的本质区别大部分问题都能迎刃而解。最好的学习方法就是动手实验创建几个立方体和球体给它们不同的组件和设置观察控制台的输出和物体的行为很快你就能建立起直观的理解。记住在复杂的游戏场景中性能往往比物理精度更重要所以从项目初期就要养成优化物理设置的好习惯。