今天我们来排查一个 Unity 新手和老手都容易踩的坑:山林寻宝小游戏里,玩家走到道具旁边,碰撞检测没有反应,宝箱、金币、宝石全都捡不起来。这类问题在 Unity 项目里非常典型,现象一致,但原因可能分布在多个层面。这篇文章会从项目搭建、碰撞体配置、物理系统设置、脚本事件绑定四个层面逐步拆解,最后给出一套可以直接复用的修复流程。
山林寻宝这类小游戏的核心玩法通常很简单:玩家角色在场景中探索,碰到道具后通过碰撞或触发器事件实现拾取、加分、播放音效等反馈。正因为玩法依赖碰撞检测,一旦“碰到没反应”,整个游戏闭环就断了。排查思路如果乱,很容易在脚本里反复改却看不到效果。更稳妥的做法是先搞清楚 Unity 的碰撞检测机制,再按“碰撞体 → 刚体 → 物理矩阵 → 脚本事件”的顺序逐层定位。
下面我会先给你一张速览表,然后从环境准备开始,把道具碰撞检测无响应的问题完整复现、排查并修复。文章涉及的操作均以 Unity 2021 或 2022 LTS 为参考,项目类型为 3D 核心场景,如果你的版本不同,菜单名称和 Inspector 布局会略有差异,但排查逻辑是一致的。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | Unity 3D 小游戏:山林寻宝,俯视角或第三人称视角均可 |
| 核心玩法 | 玩家触碰金币、宝石、宝箱等道具,触发拾取反馈 |
| 关键组件 | Character Controller / Rigidbody + Collider,道具 Collider(Is Trigger) |
| 常见问题 | 碰撞检测无响应、道具无法拾取、触发器事件不执行 |
| 排查范围 | 碰撞体配置、刚体运动方式、Layer 物理矩阵、Tag 设置、脚本事件绑定 |
| 适用 Unity 版本 | Unity 2020 LTS 及以上(2021/2022 LTS 测试界面更稳定) |
| 复现难度 | 低,一个空场景 + 胶囊体 + 立方体即可复现 |
| 调试方式 | Scene 视图调试、Debug.Log 输出、Physics Debugger、Profiler |
| 是否涉及 API | 涉及 OnTriggerEnter、OnCollisionEnter、Physics.IgnoreLayerCollision 等 |
| 适合读者 | Unity 初学者、刚接触物理系统的小游戏开发者、遇到碰撞不响应问题的排查者 |
从实际开发来看,碰撞检测无响应 80% 以上不是脚本问题,而是组件配置和物理系统设置问题。所以这篇文章不会只丢一个“改脚本”的结论,而是带你完整走一遍定位流程。
2. 适用场景与使用边界
这个修复流程适合以下情况:本地单机小游戏、Unity 3D 或 2D 项目、使用内置物理系统(Physics/Physics 2D)、角色通过碰撞或触发器与道具交互。
如果你用的是 CharacterController 自带移动,那么角色检测触发器事件时,需要确保脚本挂在带有 CharacterController 的游戏对象上,并且至少一方有刚体。Common 的坑是:CharacterController 本身可以推动物体,但它的碰撞事件属于“移动碰撞”,拾取类触发器建议单独用一个小范围的子碰撞体或直接依赖 CharacterController 内建的碰撞检测。
需要说明使用边界:这篇文章不覆盖自定义物理引擎、DOTS Physics、可视化脚本插件(Bolt/PlayMaker)的碰撞事件处理,也不讨论网络多人同步下的碰撞抖动问题。涉及复杂寻路、攀爬、坡面滑动的项目,物理参数会更敏感,但基础排查顺序仍然适用。
3. 环境准备与复现项目搭建
开始排查前,先建一个最小复现场景。最小复现的目的是把问题从完整项目中剥离出来,避免角色动画、地形、UI 脚本干扰判断。
3.1 创建新场景
打开 Unity Hub,新建一个 3D 项目,然后创建空场景,命名为CollisionTest。加入以下对象:
- 玩家角色:创建一个 Capsule,命名为
Player。 - 道具:创建一个 Cube,命名为
Pickup_Coin,放在地面上方一点。 - 地面:创建一个 Plane,命名为
Ground。
3.2 添加基础组件
玩家对象需要添加移动控制脚本,否则无法主动靠近道具。为了排查干净,先不做完整角色控制器,只用一个最简单的 KeyCode 移动脚本:
using UnityEngine; public class PlayerMove : MonoBehaviour { public float moveSpeed = 5f; void Update() { float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); Vector3 move = new Vector3(h, 0, v) * moveSpeed * Time.deltaTime; transform.Translate(move, Space.World); } }道具对象只需要一个 BoxCollider 和 Tag。我们先用最基础的方式复现“无响应”:
- Player 添加 CapsuleCollider。
- Pickup_Coin 添加 BoxCollider,勾选
Is Trigger。 - 新建脚本
PickupDetect.cs,挂到 Player 上,然后在 OnTriggerEnter 中输出日志。
using UnityEngine; public class PickupDetect : MonoBehaviour { private void OnTriggerEnter(Collider other) { Debug.Log("触发道具:" + other.gameObject.name); } }此时运行游戏,操作角色走向道具,观察 Console。如果没有任何日志输出,说明触发事件没有触发,问题被成功复现。
3.3 复现过程中的关键观察点
复现时建议打开 Scene 视图,一边运行一边观察物体位置。注意以下几点:
- 玩家和道具的 Collider 是否真的发生了几何接触。
- 两者之间是否有透明碰撞体阻隔。
- 地面是否比道具高,导致道具实际嵌入地面,玩家无法从侧面接触。
- 道具的 BoxCollider 尺寸是否为零。
很多“无响应”其实是由这些肉眼可见的配置问题引起的。先用场景视图确认物理接触,再进入代码排查。
4. 碰撞检测无响应的第一层排查:Collider 与 Rigidbody 配置
在最小复现场景中,如果触发器事件没有触发,第一步检查 Collider 和 Rigidbody。
4.1 Unity 碰撞检测的必要条件
Unity 的内置物理系统遵循几个固定规则:
- 两个对象至少有一个带有 Rigidbody。
- 两个对象都必须带有 Collider。
- 触发器事件(OnTriggerEnter)要求至少一方勾选了 Is Trigger。
- 碰撞事件(OnCollisionEnter)要求两方都未勾选 Is Trigger,且至少一方有 Rigidbody。
你可能会遇到“硬币有 Collider,玩家有 Rigidbody,仍然不触发”的情况。最常见的解释是:携带 Rigidbody 的对象被 Kinematic 锁定,或者移动方式没有通过物理系统驱动。
4.2 Rigidbody 的三种运动模式
| 模式 | isKinematic | 说明 | 是否触发碰撞事件 |
|---|---|---|---|
| Dynamic | false | 受重力和物理力影响,标准动态刚体 | 是 |
| Kinematic | true | 不受物理力驱动,但可以主动移动 | 可触发触发器事件,不触发标准碰撞事件 |
| Static | 无 Rigidbody | 完全静止,通过 Transform 移动不产生物理效果 | 依赖对方刚体 |
这里要特别强调:如果玩家用 transform.Translate 直接移动,且玩家是 Dynamic Rigidbody,物理引擎会把这种移动视为“瞬移”,在高速移动时可能产生穿透,导致触发器事件不稳定。更推荐的方式是使用 Rigidbody.MovePosition 或 Rigidbody.velocity。
把 PlayerMove 脚本改成物理驱动:
using UnityEngine; public class PlayerMoveRigidbody : MonoBehaviour { public float moveSpeed = 5f; private Rigidbody rb; void Start() { rb = GetComponent<Rigidbody>(); if (rb == null) { rb = gameObject.AddComponent<Rigidbody>(); } rb.constraints = RigidbodyConstraints.FreezeRotation; } void Update() { float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); Vector3 move = new Vector3(h, 0, v).normalized; Vector3 targetPos = transform.position + move * moveSpeed * Time.deltaTime; rb.MovePosition(targetPos); } }4.3 Collider 尺寸与位置
Collider 尺寸和位置是另一个高频原因。当道具是导入的 3D 模型时,Unity 不一定自动生成 Collider,或者生成的 Collider 和模型完全不匹配。你把 BoxCollider 加上后,如果 Center 偏离,实际触发区域可能根本不在道具表面。
排查方法:
- 在 Inspector 中点击
Edit Collider。 - 查看绿色线框是否贴合模型。
- 将 Collider 的 Size 调成模型实际尺寸的 0.8 到 1 倍。
- 确认 Collider 没有和地面重叠导致玩家无法触达。
5. 第二层排查:Layer、物理矩阵与标签
Collider 和 Rigidbody 配置正确并不代表一定能触发。Layer 物理矩阵会在全局层面过滤碰撞对,很多项目里的“碰不到”其实是 Layer 设置把碰撞直接屏蔽了。
5.1 检查 Layer Collision Matrix
打开Edit -> Project Settings -> Physics,查看Layer Collision Matrix。默认情况下所有层都互相碰撞,但大型项目可能为了性能或玩法需要关闭了某些层之间的碰撞。
比如 Player 在Player层,道具在Pickup层,如果矩阵中Player和Pickup的交叉点没有被勾选,两者之间就不会产生任何物理交互,事件也不会触发。
具体操作:
- 在场景中选中 Player。
- 在 Inspector 右上角 Layer 下拉框中设为
Player(没有就新增层)。 - 选中道具,设为
Pickup层。 - 回到 Physics 设置,确认 Player 行和 Pickup 列的交叉格子被勾选。
5.2 Tag 与碰撞检测的关系
Tag 不参与物理碰撞运算,只用于事件回调中的身份判断。所以如果你在 OnTriggerEnter 中通过other.CompareTag("Pickup")判断是否拾取道具,而道具的 Tag 没有被设置,那么即使碰撞触发,你的逻辑也会被挡在if外面。
这是“看起来没反应”的另一个常见原因。代码可能执行了,但日志被条件判断过滤掉,你误以为事件没触发。
建议在项目设置中明确新增PickupTag,并给所有可拾取道具统一赋值。然后在脚本中改成以下形式:
private void OnTriggerEnter(Collider other) { if (other.CompareTag("Pickup")) { Debug.Log("拾取道具:" + other.gameObject.name); other.gameObject.SetActive(false); } }5.3 检查 Physics.IgnoreLayerCollision 的运行时调用
有时脚本中会调用Physics.IgnoreLayerCollision来临时屏蔽某两层碰撞。如果这个 API 在 Start 或 Awake 中被意外执行,运行时会覆盖矩阵配置。排查方式是全局搜索IgnoreLayerCollision,确认没有错误调用。
6. 第三层排查:脚本事件与代码逻辑
组件配置全部正确后,如果仍然没有触发,就需要从脚本事件入手。
6.1 OnTriggerEnter 与 OnCollisionEnter 的选择
很多开发者在同一个对象上同时挂了 OnTriggerEnter 和 OnCollisionEnter 逻辑,但没注意对象本身的 Is Trigger 状态。当碰撞体勾选了 Is Trigger 时,只会触发 OnTriggerEnter;当未勾选时,只会触发 OnCollisionEnter。两者不会同时执行。
| 状态 | 触发事件 | 回调方法 |
|---|---|---|
| 一方 Is Trigger = true | 触发器事件 | OnTriggerEnter / OnTriggerStay / OnTriggerExit |
| 双方 Is Trigger = false | 普通碰撞 | OnCollisionEnter / OnCollisionStay / OnCollisionExit |
如果道具要支持“走进即拾取”,推荐用 Trigger;如果道具要参与物理阻挡,比如宝箱不能被角色穿过,则用普通碰撞,并手动检测碰撞对象。
6.2 脚本挂载位置
事件方法挂在哪个对象上也有讲究。OnTriggerEnter 的触发条件是“本对象”参与了碰撞。所以:
- 脚本挂在 Player 上,能收到 Player 进入其他 Collider 的事件。
- 脚本挂在道具上,也能收到道具被其他 Collider 触碰的事件。
- 脚本挂在没有 Collider 的父对象上,默认收不到子对象碰撞事件,除非使用
OnTriggerEnter(Collider other)配合子碰撞体的消息转发。
在隐藏 Bug 场景中,游戏对象是父子结构,比如 Player 的模型在最外层,碰撞体在子物体上,脚本挂在父物体上。这时碰撞事件不会传导给父物体,看起来就是“完全没反应”。解决方案是写一个事件转发脚本,或在子物体上单独挂检测脚本。
下面给出一个父物体接收子碰撞体事件的转发示例:
using UnityEngine; public class TriggerEventForwarder : MonoBehaviour { public UnityEngine.Events.UnityEvent<Collider> onTriggerEnter; private void OnTriggerEnter(Collider other) { onTriggerEnter?.Invoke(other); } }把该脚本挂到带 Collider 的子物体上,然后在 Inspector 中把回调拖到父物体的处理方法,或者在父物体中订阅事件。
6.3 事件执行但被 SetActive 或销毁干扰
一个容易被忽略的细节:当玩家触发拾取后,如果立即SetActive(false)或Destroy道具对象,后续的触发判断可能会被打断。如果是连续拾取多个道具,且多个道具位置很接近,OnTriggerExit可能没有机会触发,导致玩家站在拾取区域时会反复进入退出,表现就是道具时捡时不捡。
建议做法是拾取后先禁用 Collider,延迟销毁或隐藏:
private void OnTriggerEnter(Collider other) { if (other.CompareTag("Pickup")) { PickupItem pickup = other.GetComponent<PickupItem>(); if (pickup != null) { pickup.Collect(); } } }using UnityEngine; public class PickupItem : MonoBehaviour { public int scoreValue = 10; public void Collect() { GetComponent<Collider>().enabled = false; Debug.Log("获得金币:" + scoreValue); gameObject.SetActive(false); } }7. 道具拾取功能修复:完整示例
为了让整个流程更清晰,这里给出一套完整的“山林寻宝”拾取功能实现。场景结构:
- Player:Capsule + Rigidbody + CapsuleCollider + PlayerMoveRigidbody + PlayerPickupHandler
- Pickup_Coin:Cube + BoxCollider(Is Trigger) + Tag(Pickup) + PickupItem.cs
7.1 玩家拾取处理器
using UnityEngine; public class PlayerPickupHandler : MonoBehaviour { private int score = 0; private void OnTriggerEnter(Collider other) { if (other.CompareTag("Pickup")) { PickupItem item = other.GetComponent<PickupItem>(); if (item != null) { int value = item.Collect(); score += value; Debug.Log("当前分数:" + score); } } } }7.2 道具行为脚本
using UnityEngine; public class PickupItem : MonoBehaviour { public int scoreValue = 10; public AudioClip collectSound; private AudioSource audioSource; void Start() { audioSource = GetComponent<AudioSource>(); if (audioSource == null && collectSound != null) { audioSource = gameObject.AddComponent<AudioSource>(); } } public int Collect() { GetComponent<Collider>().enabled = false; if (audioSource != null && collectSound != null) { audioSource.PlayOneShot(collectSound); } Debug.Log("拾取道具:" + gameObject.name + ",分值:" + scoreValue); // 延迟隐藏,给音效和特效播放时间 Invoke(nameof(HideItem), 0.3f); return scoreValue; } private void HideItem() { gameObject.SetActive(false); } }7.3 修复后的验证步骤
- 运行游戏,操作玩家走向金币。
- Console 窗口输出“触发道具:Pickup_Coin”。
- 再输出“拾取道具:Pickup_Coin,分值:10”。
- 玩家靠近后金币消失,分数增加。
如果第 2 步都没出现,回到前面的配置检查;如果第 2 步出现但第 3 步没出现,检查 Tag 和 CompareTag;如果第 3 步出现但金币不消失,检查 PickupItem 脚本是否挂载到道具对象上。
8. 接口 API 与调试辅助功能
Unity 碰撞检测虽然没有网络 API 那种端口概念,但物理系统提供了一组常用的调试 API,可以用来确认碰撞是否真正发生。
8.1 Physics Debugger 与 DrawRay 调试
Unity 提供Physics Debugger窗口,可以查看所有物理体的碰撞器状态、接触点、休眠状态。打开方式:
Window -> Analysis -> Physics Debugger
在窗口中,重点看以下内容:
- 碰撞体的
Enabled状态。 - 刚体的
Is Sleeping状态。 - 当前接触点数量。
如果刚体处于休眠状态,且物体没有速度,碰撞事件可能被引擎合并优化掉。在需要持续检测的区域,可以关闭对象的自动休眠:把 Rigidbody 的Sleeping Mode改为Never Sleep。这在寻宝游戏中更容易稳定触发触发器事件。
8.2 用 Physics.Raycast 做调试辅助
当碰撞检测事件不稳定时,可以用射线检测做临时替代或辅助验证。下面的脚本在玩家前方发射一条短射线,检测是否碰到道具:
using UnityEngine; public class PickupRaycastDebug : MonoBehaviour { public float range = 2f; void Update() { if (Input.GetKeyDown(KeyCode.E)) { Ray ray = new Ray(transform.position, transform.forward); RaycastHit hit; if (Physics.Raycast(ray, out hit, range)) { if (hit.collider.CompareTag("Pickup")) { Debug.Log("射线检测到道具:" + hit.collider.name); } } else { Debug.Log("射线未命中道具"); } } } }这不会替代真正的触发器方案,但可以帮助判断 Collider 是否真实存在、Tag 是否正确。若射线能检测到道具,说明场景中的物理体本身没问题,问题在事件回调逻辑。
9. 资源占用与性能观察
碰撞检测的性能影响在寻宝类小游戏中通常不大,但如果场景中道具数量很多,比如 100 个金币、50 个宝石、20 个宝箱,需要关注几个指标。
9.1 显存与内存
Unity 物理系统不依赖显存,所以 GPU 显存不是碰撞检测的瓶颈。内存方面,主要看 Collider 组件数量和物理引擎内部数据结构。每个 Collider 都会占用一定的物理内存,100 个以下道具通常无感知。
如果你的项目是 3D 模型,且每个道具都加载了独立材质和贴图,显存和内存压力会显著上升。这时建议使用同一材质实例、图集纹理、对象池回收。
9.2 CPU 开销
碰撞检测 CPU 开销主要来自:
- 更新阶段:动态刚体位置变化触发 Broadphase 和 Narrowphase 检测。
- 触发器检测:当一个触发器与其他碰撞体重叠时,物理引擎需要每帧调用 OnTriggerStay。
- 大量静态碰撞体:场景中静态碰撞体数量多时,引擎需要维护 BVH 树。
优化手段:
- 使用
Physics.autoSyncTransforms = false,在批量移动物体后手动同步。 - 静态道具不挂 Rigidbody,保持为 Static Collider。
- 避免使用高精度 Mesh Collider,尽量用多个盒体组合。
- 触发器检测脚本里少写复杂逻辑,只在 OnTriggerEnter 里处理,不要做每帧循环。
9.3 如何排查碰撞检测性能问题
用 Profiler 打开Physics模块,观察Physics.Simulate耗时。如果单帧耗时超过 5ms,说明物理引擎计算量偏大。常见原因:
- 碰撞体数量过多。
- 刚体数量过多且状态频繁变化。
- 单个 Mesh Collider 三角面数过高。
寻宝游戏阶段,建议把道具分成区域管理,非当前区域的道具禁用 Collider,进入区域后再启用。
10. 常见问题与排查方法
下面的表格汇总了山林寻宝小游戏中碰撞检测无响应的典型问题,按照排查优先级排序。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 道具完全无法触发 | 没有 Rigidbody | 检查对象组件列表 | 给玩家或道具添加 Rigidbody |
| 道具无法触发,且玩家是 CharacterController | 角色移动没有走物理接口 | 检查 CharacterController.isGrounded 与碰撞回调 | 改用 Rigidbody.MovePosition 或依赖 CharacterController 碰撞事件 |
| 有碰撞但无日志 | Tag 不匹配 | 检查 CompareTag 分支 | 在项目设置中新增 Pickup Tag 并给道具赋值 |
| 有碰撞但日志输出多次 | OnTriggerStay 被每帧调用 | 检查事件方法 | 把逻辑放到 OnTriggerEnter 中,或增加冷却时间 |
| 碰撞发生但物体穿透 | 移动速度过快,碰撞检测模式为 Discrete | 检查 Rigidbody Collision Detection 设置 | 改为 Continuous 或 Continuous Speculative |
| 碰撞发生但拾取后残留 | SetActive 在音效播放前执行 | 检查拾取逻辑顺序 | 先禁用 Collider,延迟隐藏对象 |
| 两个对象靠近但始终不接触 | Layer Collision Matrix 关闭了碰撞对 | 检查 Physics 设置 | 勾选对应层碰撞 |
| 道具 Collider 与模型不一致 | 导入模型未生成 Collider | 场景选中模型查看线框 | 手动调整 BoxCollider Center 和 Size |
| 拾取后角色移动到其他道具无反应 | 被拾取对象销毁后,Collider 引用未清理,或 OnTriggerExit 未触发 | 检查场景对象状态 | 使用对象池,禁用而非销毁 |
| 炮台的碰撞和拾取同时存在 | 同一对象 Trigger 和非 Trigger 的碰撞体叠加 | 检查多个 Collider 配置 | 拆分拾取区域和物理阻挡区域 |
11. 最佳实践与使用建议
11.1 设计阶段就规划 Collider 层级
不要把全部碰撞检测逻辑依赖到一个对象的默认碰撞体上。建议在项目初期就规划好:
- Player 层:物理碰撞和触发器检测分离。
- Pickup 层:所有可拾取道具统一放在一层,且全部使用 Trigger。
- Terrain 层:环境地形只做静态碰撞,不参与拾取事件。
- Interactable 层:NPC、机关等需要交互对象单独分层。
这样可以在物理矩阵中精确控制哪些层之间需要碰撞,哪些层只需要触发器事件。
11.2 使用对象池优化道具管理
寻宝游戏里金币和宝石通常是重复生成、大量存在的。每次 Instantiate 和 Destroy 都会产生 GC 压力和物理系统初始化开销。更合理的方式是用对象池:
using UnityEngine; using System.Collections.Generic; public class PickupPool : MonoBehaviour { public GameObject pickupPrefab; public int poolSize = 50; private Queue<GameObject> pool = new Queue<GameObject>(); void Start() { for (int i = 0; i < poolSize; i++) { GameObject obj = Instantiate(pickupPrefab); obj.SetActive(false); pool.Enqueue(obj); } } public GameObject Spawn(Vector3 position) { GameObject obj = pool.Count > 0 ? pool.Dequeue() : Instantiate(pickupPrefab); obj.transform.position = position; obj.SetActive(true); return obj; } public void Despawn(GameObject obj) { obj.SetActive(false); pool.Enqueue(obj); } }11.3 日志分级与调试开关
在项目中增加一个静态调试开关,可以随时打开和关闭碰撞日志,避免上线后日志刷屏。
public static class DebugConfig { public static bool EnablePickupLog = true; }然后在拾取逻辑中统一判断:
if (DebugConfig.EnablePickupLog) { Debug.Log("拾取道具:" + gameObject.name); }11.4 合规提醒
如果项目接入广告 SDK、统计 SDK,或使用第三方美术资源,发布前需要注意授权和隐私合规问题。道具模型、音效、特效资源如果来自商店,建议确认许可证类型是否允许商用。游戏内若有用户生成内容或上传头像功能,需要额外评估内容审核机制。
12. 总结与下一步
这次围绕“UnUnity 山林寻宝小游戏:道具碰撞检测无响应 Bug 修复”完整的排查链路已经走了一遍。最值得记住的一点是:碰撞检测无响应时,不要急着改脚本。先检查 Collider 是否贴合模型,再确认 Rigidbody 是否存在,然后看 Layer 碰撞矩阵,最后才回到 OnTriggerEnter / OnCollisionEnter 的事件逻辑。这个顺序能避免大量无效修改。
最先应该验证的功能是“玩家走向道具,道具触发拾取反馈”,项目跑通后,再逐步增加音效、特效、积分 UI 和道具刷新逻辑。最容易踩的坑是 Transform 移动导致的物理穿透,以及 Tag 未设置导致事件被条件过滤掉。这两类问题在实际项目中出现频率最高,排查时优先检查。
后续可以继续扩展的方向包括:
- 使用 Unity MCP 或脚本化编辑器工具加速业务逻辑自动化测试。
- 将拾取系统改造成 ScriptableObject 配置,方便策划配置道具分值、音效、掉落概率。
- 为寻宝玩法加入批次刷新和区域权重控制,提升游戏节奏。
- 把物理检测升级为混合方案:大范围用 Trigger,精确交互用 Raycast 配合碰撞检测。
如果这篇文章帮你解决了一个卡了很久的 Bug,建议收藏备用。下次遇到 Unity 碰撞检测问题,可以直接按这篇文章的排查顺序过一遍,大概率能快速定位到问题所在。