4. 碰撞事件处理:OnCollisionEnter、OnCollisionStay、OnCollisionExit、碰撞检测模式
碰撞事件,说白了就是两个物体碰到一起时,Unity 告诉你的那一声「嘿,撞上了!」。我个人觉得,这是物理引擎里最性感的部分——没有事件回调,物理碰撞就只是一堆刚体在瞎蹦跶。
今天咱们就把 OnCollisionEnter、OnCollisionStay、OnCollisionExit 这三个老朋友聊透,再顺带把碰撞检测模式也捋清楚。
4.1 三个核心回调:什么时候触发?
这三个方法的名字已经说明了一切。但很多新手会搞混它们的触发时机,我见过有人把 Stay 当成 Enter 用,结果逻辑跑飞了。
| 回调方法 | 触发时机 | 典型用途 |
|---|---|---|
OnCollisionEnter |
两个碰撞体第一次接触的那一帧 | 播放撞击音效、扣血、触发爆炸 |
OnCollisionStay |
两个碰撞体持续接触的每一帧 | 持续伤害、摩擦力计算、压力感应 |
OnCollisionExit |
两个碰撞体分离的那一帧 | 停止音效、移除状态、清理数据 |
嗯,这里要注意:Stay 是每帧都调用的。如果你在 Stay 里做 Instantiate,那画面太美我不敢看。我曾经在项目里看到有人这么干,结果一秒钟生成了几百个粒子特效,帧率直接崩到个位数。
4.2 代码怎么写?参数里有什么?
这三个方法的签名是一样的,都接收一个 Collision 参数。这个参数里藏着很多好东西。
void OnCollisionEnter(Collision collision)
{
// 获取碰撞到的物体
GameObject other = collision.gameObject;
// 获取碰撞点信息
ContactPoint contact = collision.contacts[0];
Vector3 point = contact.point;
Vector3 normal = contact.normal;
// 获取相对速度
float impactForce = collision.relativeVelocity.magnitude;
Debug.Log($"撞到了 {other.name},冲击力:{impactForce}");
}
你想想看,有了 contact.point,你可以在撞击位置生成火花特效;有了 contact.normal,你可以让子弹反弹得更真实。这些细节,就是专业和业余的分水岭。
OnCollisionEnter 里先判断 collision.contacts.Length > 0,虽然大多数情况下它不会为空,但保一手总没错。毕竟线上出 bug 的时候,没人会同情你。
4.3 避坑指南:常见翻车现场
我做物理相关课程好几年了,总结出几个高频翻车点,你记一下。
- 两个物体都没有刚体? 那碰撞事件永远不会触发。至少其中一个要有刚体,而且不能是 Kinematic(除非你用了触发器)。
- 用了触发器(IsTrigger)? 那
OnCollision系列不会调用,你得用OnTrigger系列。这两个是两套系统,别混了。 - 脚本挂错了对象? 碰撞事件是在挂载脚本的那个物体上触发的,不是在被撞物体上。我曾经排查了半天,结果发现脚本挂在了父物体上,子物体撞了但父物体没反应。
OnCollisionStay 里频繁修改刚体的位置或旋转。这会导致物理引擎重新计算,轻则性能下降,重则物理模拟崩溃。如果你需要持续施加力,用 AddForce 而不是直接改 transform.position。
4.4 碰撞检测模式:性能与精度的取舍
Unity 提供了三种碰撞检测模式,你可以在 Rigidbody 组件的 Collision Detection 下拉菜单里找到。
| 模式 | 说明 | 性能开销 | 适用场景 |
|---|---|---|---|
| Discrete(离散) | 默认模式,每帧检测一次碰撞 | 低 | 大多数静态或低速物体 |
| Continuous(连续) | 对动态物体进行连续碰撞检测 | 中 | 高速运动的物体(如子弹) |
| Continuous Dynamic(动态连续) | 对动态和静态物体都进行连续检测 | 高 | 极高速物体,且需要与静态物体碰撞 |
为什么会有这些模式?说白了就是「穿模」问题。如果一个子弹飞得太快,一帧内从墙的左边穿到了右边,离散模式根本检测不到碰撞。连续模式会在这帧内做插值计算,确保不会漏掉。
我记得有一次做射击游戏,子弹老是莫名其妙穿过敌人身体。查了半天,发现是碰撞检测模式没改,默认的 Discrete 根本抓不住高速子弹。改成 Continuous 后,问题立刻解决。
4.5 实战组合:一个完整的碰撞处理示例
咱们来写一个完整的例子。假设你有一个「伤害区域」,玩家碰到它就会持续掉血,离开后停止。
public class DamageZone : MonoBehaviour
{
public float damagePerSecond = 10f;
private Dictionary<GameObject, float> victims = new Dictionary<GameObject, float>();
void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Player"))
{
victims[collision.gameObject] = 0f;
Debug.Log("玩家进入伤害区域");
}
}
void OnCollisionStay(Collision collision)
{
if (victims.ContainsKey(collision.gameObject))
{
victims[collision.gameObject] += damagePerSecond * Time.deltaTime;
// 这里可以触发持续伤害效果
}
}
void OnCollisionExit(Collision collision)
{
if (victims.ContainsKey(collision.gameObject))
{
victims.Remove(collision.gameObject);
Debug.Log("玩家离开伤害区域,总伤害:" + victims[collision.gameObject]);
}
}
}
这个例子虽然简单,但涵盖了三个回调的典型用法。你想想看,如果只用 Enter 和 Exit,你没法做持续伤害;如果只用 Stay,你没法知道玩家是什么时候进来的。三个配合使用,才能做出细腻的交互。
4.6 最后说两句
碰撞事件处理,说难不难,说简单也不简单。我见过太多项目因为碰撞事件没处理好,出现各种诡异 bug——比如角色卡在墙里、子弹穿模、伤害重复计算。这些问题的根源,往往就是对这三个回调的理解不够深。
嗯,今天的课就到这里。下一章咱们聊聊触发器和碰撞体的区别,以及什么时候该用触发器而不是碰撞体。那个话题更有意思,到时候见。