资讯动态

Cocos Creator 3.6 2D碰撞监听全解析:从组件到全局事件管理

发布时间:2026/8/9 13:56:47 来源:尧图企业网站定制
1. 项目概述为什么2D碰撞监听是游戏开发的核心在Cocos Creator 3.6里做2D游戏碰撞检测和监听是绕不开的基础功能。无论是做一个简单的平台跳跃游戏还是复杂的弹幕射击游戏你都需要知道“什么时候两个物体碰上了”以及“碰上之后该干什么”。很多新手朋友在官方文档里看了BoxCollider2D和PhysicsSystem2D的API后依然会卡在具体实现上监听器怎么挂回调函数怎么写全局监听和组件监听到底用哪个参数怎么调才不会有“鬼畜”的穿透或者抖动这篇文章我就以一个踩过无数坑的“老司机”身份带你从最基础的BoxCollider2D组件配置开始一步步深入到PhysicsSystem2D的全局回调把2D物理碰撞监听这件事掰开揉碎了讲清楚。我会结合一个具体的“发射子弹击中敌人”的案例把编辑器操作、代码编写、参数调试、性能优化和常见坑点都过一遍。目标是让你看完之后不仅能自己搭出一套稳定可靠的碰撞系统更能理解背后的原理做到举一反三。2. 核心思路与方案选型组件监听 vs 全局监听在动手写代码之前我们必须先理清Cocos Creator 3.6提供的两种主流碰撞监听方式以及它们各自的适用场景。选错了方案后期可能会带来巨大的重构成本。2.1 组件级监听onBeginContact 与 onEndContact这是最直观、最常用的方式。你需要在一个挂载了碰撞体组件如BoxCollider2D的节点上再挂载一个自定义脚本并在这个脚本里实现特定的回调函数。工作原理当物理引擎检测到两个碰撞体发生接触或分离时会遍历相关节点上的所有组件寻找并调用那些定义了onBeginContact或onEndContact方法的组件。适用场景逻辑与特定物体强绑定比如一个“宝箱”节点它被“玩家”碰撞后要打开并播放特效。这个“打开”的逻辑天然属于宝箱自身。逻辑简单、对象明确你很清楚是“谁”需要响应碰撞事件。小型项目或原型开发快速验证想法逻辑直接写在相关节点上很方便。优点直观逻辑内聚易于理解。缺点当场景中需要监听碰撞的对象很多时每个对象上的脚本都会被频繁调用可能引发性能问题。此外事件逻辑分散在各个脚本中不利于集中管理和调试。2.2 系统级全局监听PhysicsSystem2D.instance.on这是更强大、更集中的管理方式。你可以在一个全局管理器比如GameManager或某个常驻节点脚本中订阅物理系统发出的事件。工作原理PhysicsSystem2D是一个单例它会在每一帧物理计算后广播所有发生的碰撞事件。你只需要在一处监听这些事件然后根据碰撞双方的标签Tag、分组Group或自定义属性进行分发和处理。适用场景需要集中管理碰撞逻辑比如一个“伤害计算系统”所有攻击命中、子弹击中敌人都需要在这里统一处理伤害公式、暴击判断等。对象数量庞大比如有成百上千颗子弹和敌人使用全局监听可以避免在每个子弹和敌人身上都挂载脚本性能更优。需要复杂的过滤和决策比如“友军伤害是否开启”、“某种道具只对特定职业生效”等规则在中心控制器里判断更清晰。解耦需求强烈你希望碰撞检测逻辑与具体的游戏对象实体分离便于单元测试和模块替换。优点性能通常更好尤其对象多时逻辑集中易于扩展和维护解耦彻底。缺点需要自己写事件分发逻辑架构上稍复杂对新手理解有一定门槛。我的选择建议对于中小型项目可以从组件监听入手快速实现功能。当项目规模扩大或者你明确感觉到碰撞逻辑开始变得混乱时应果断迁移到全局监听。在本次教程中我会两种方式都详细实现让你看到完整的对比。3. 基础搭建从零配置一个可碰撞的场景理论说再多不如动手做。我们创建一个最简单的场景一个地面静态一个玩家方块受重力下落一个敌人方块静态。玩家落下后会与地面和敌人发生碰撞。3.1 物理系统与碰撞分组配置首先确保物理系统已经启用并正确配置。启用物理系统打开项目设置 - 功能裁剪确保物理系统2D和2D模块没有被裁剪掉。配置物理世界在项目设置 - 物理 - 物理 2D中设置重力。对于2D横版游戏Y轴重力通常是负值比如(0, -500)。允许休眠可以提升性能但调试时可先关闭。规划碰撞分组Group这是避免不必要碰撞、优化性能的关键。在项目设置 - 物理 - 碰撞矩阵中默认有DEFAULT分组。我们添加几个PLAYER(分组索引 1)ENEMY(分组索引 2)GROUND(分组索引 3)BULLET(分组索引 4) 然后在碰撞矩阵中勾选哪些分组之间需要检测碰撞。例如PLAYER与ENEMY、GROUND需要碰撞。BULLET与ENEMY需要碰撞。BULLET与PLAYER、GROUND可能不需要避免误伤和地面阻挡所以不勾选。ENEMY与GROUND可能需要如果敌人也站在地上。 这个矩阵是性能优化的第一道关卡务必根据游戏设计仔细规划。3.2 创建节点与碰撞体组件地面 (Ground)创建一个空节点重命名为Ground。添加SpriteRenderer组件选择一个矩形图片如白色方块并拉宽它看起来像一条地面。添加BoxCollider2D组件。勾选Editing在场景视图中将绿色线框调整到与地面精灵大小一致。在BoxCollider2D组件中设置Group为刚才创建的GROUND。因为是静止的地面我们不需要添加RigidBody2D组件。没有刚体的碰撞体是“静态碰撞体”它不会移动但可以与其他物体发生碰撞。玩家 (Player)创建一个空节点重命名为Player。添加SpriteRenderer组件选择一个不同颜色的方块。添加BoxCollider2D组件调整大小匹配精灵。设置Group为PLAYER。关键一步添加RigidBody2D组件。这是让物体参与物理运动如重力、速度的核心。保持其Type为Dynamic动态刚体。这样玩家就会受到重力下落。敌人 (Enemy)类似地面创建Enemy节点添加SpriteRenderer和BoxCollider2DGroup设为ENEMY。为了简单我们先不给他加RigidBody2D让它作为一个静态的障碍物。现在运行场景你会看到玩家方块自由落体砸在地面上并停住。基础的物理碰撞已经生效了但我们还“听”不到这次碰撞。4. 实现组件级碰撞监听让我们给玩家和敌人添加“耳朵”让它们能听到碰撞事件。4.1 为Player节点添加监听脚本在Player节点上创建并挂载一个脚本例如PlayerController.ts。import { _decorator, Component, BoxCollider2D, Contact2DType, IPhysics2DContact, RigidBody2D, Vec2 } from cc; const { ccclass, property } _decorator; ccclass(PlayerController) export class PlayerController extends Component { // 可以通过属性检查器拖拽赋值也可以代码获取 private _collider: BoxCollider2D | null null; private _rigidBody: RigidBody2D | null null; start() { // 获取组件 this._collider this.getComponent(BoxCollider2D); this._rigidBody this.getComponent(RigidBody2D); if (this._collider) { // 注册单个碰撞体的回调函数 this._collider.on(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this); this._collider.on(Contact2DType.END_CONTACT, this._onEndContact, this); // 如果需要持续接触的每一帧都回调可以监听 STAY_CONTACT // this._collider.on(Contact2DType.STAY_CONTACT, this._onStayContact, this); } } onDestroy() { // 非常重要组件销毁时务必取消注册防止内存泄漏和报错 if (this._collider) { this._collider.off(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this); this._collider.off(Contact2DType.END_CONTACT, this._onEndContact, this); } } private _onBeginContact(selfCollider: BoxCollider2D, otherCollider: BoxCollider2D, contact: IPhysics2DContact | null) { // selfCollider: 本组件的碰撞体即Player的碰撞体 // otherCollider: 与selfCollider发生碰撞的另一个碰撞体 console.log(Player 开始接触: ${otherCollider.node.name}); // 示例如果碰到敌人玩家被弹开 if (otherCollider.group 2) { // ENEMY 分组索引是2 console.log(撞到敌人了); if (this._rigidBody) { // 施加一个向上的冲量 this._rigidBody.applyLinearImpulse(new Vec2(0, 500), this._rigidBody.getWorldCenter(), true); } } // 示例根据Tag判断 // if (otherCollider.tag 10) { ... } } private _onEndContact(selfCollider: BoxCollider2D, otherCollider: BoxCollider2D, contact: IPhysics2DContact | null) { console.log(Player 结束接触: ${otherCollider.node.name}); } // private _onStayContact(...) { ... } }关键点解析注册与反注册监听器必须在start或onEnable中注册并在onDestroy或onDisable中取消。这是铁律否则会导致幽灵回调或错误。回调参数onBeginContact提供了三个参数其中otherCollider包含了碰撞对象的所有信息通过它的node、group、tag可以精确判断碰撞对象是谁。物理操作时机在碰撞回调函数内部可以安全地修改刚体的速度、施加力或冲量这是处理碰撞反馈如反弹、击退的标准做法。4.2 为Enemy节点添加监听脚本同理给Enemy节点也加一个脚本EnemyController.ts逻辑可以简单一些比如打印日志。import { _decorator, Component, BoxCollider2D, Contact2DType } from cc; const { ccclass, property } _decorator; ccclass(EnemyController) export class EnemyController extends Component { start() { const collider this.getComponent(BoxCollider2D); if (collider) { collider.on(Contact2DType.BEGIN_CONTACT, this._onHit, this); } } onDestroy() { const collider this.getComponent(BoxCollider2D); if (collider) { collider.off(Contact2DType.BEGIN_CONTACT, this._onHit, this); } } private _onHit(selfCollider: BoxCollider2D, otherCollider: BoxCollider2D) { // 假设只有玩家的子弹BULLET分组或玩家本身能触发受伤 if (otherCollider.group 1 || otherCollider.group 4) { // PLAYER 或 BULLET console.log(Enemy 被 ${otherCollider.node.name} 击中了); // 这里可以触发敌人受伤动画、减血、死亡等逻辑 // this.node.destroy(); // 例如直接销毁 } } }运行游戏当玩家落到地面或碰到敌人时控制台会打印出相应的日志并且玩家碰到敌人时会有一个小小的反弹。组件级监听的基本流程就走通了。5. 进阶实现PhysicsSystem2D全局监听现在我们来构建一个更优雅、更强大的全局碰撞管理系统。我们将创建一个PhysicsManager单例脚本负责所有碰撞事件的接收与分发。5.1 创建全局物理管理器创建一个名为PhysicsManager.ts的脚本它不挂载到任何场景节点而是作为纯逻辑的单例。import { _decorator, Component, PhysicsSystem2D, EPhysics2DDrawFlags, IPhysics2DContact, Collider2D } from cc; import { EventTarget } from cc; const { ccclass } _decorator; // 定义一个自定义事件系统用于分发碰撞事件 export const physicsEventTarget new EventTarget(); // 定义事件名和事件数据接口提高代码可读性和类型安全 export enum PhysicsEventType { PLAYER_HIT_ENEMY player-hit-enemy, BULLET_HIT_ENEMY bullet-hit-enemy, PLAYER_ON_GROUND player-on-ground, // ... 可以定义更多事件 } export interface ICollisionEventData { selfCollider: Collider2D; otherCollider: Collider2D; contact?: IPhysics2DContact | null; } ccclass(PhysicsManager) export class PhysicsManager extends Component { private static _instance: PhysicsManager null!; public static get instance(): PhysicsManager { return PhysicsManager._instance; } // 可选开启物理调试绘制便于观察碰撞体形状 private _debugDraw: boolean false; onLoad() { if (PhysicsManager._instance PhysicsManager._instance ! this) { this.destroy(); return; } PhysicsManager._instance this; // 防止切换场景时被销毁 // this.node.setParent(director.getScene()); // DontDestroyOnLoad(this.node); this._initPhysicsSystem(); } private _initPhysicsSystem() { // 开启物理系统调试绘制可选开发阶段非常有用 if (this._debugDraw) { PhysicsSystem2D.instance.debugDrawFlags EPhysics2DDrawFlags.Aabb | EPhysics2DDrawFlags.Pair | EPhysics2DDrawFlags.CenterOfMass | EPhysics2DDrawFlags.Joint | EPhysics2DDrawFlags.Shape; } // 注册全局碰撞回调 if (PhysicsSystem2D.instance) { // 注意这里注册的是物理系统的事件不是碰撞体的事件 PhysicsSystem2D.instance.on(Contact2DType.BEGIN_CONTACT, this._onGlobalBeginContact, this); PhysicsSystem2D.instance.on(Contact2DType.END_CONTACT, this._onGlobalEndContact, this); // PhysicsSystem2D.instance.on(Contact2DType.PRE_SOLVE, this._onGlobalPreSolve, this); // PhysicsSystem2D.instance.on(Contact2DType.POST_SOLVE, this._onGlobalPostSolve, this); } } onDestroy() { if (PhysicsSystem2D.instance PhysicsManager._instance this) { PhysicsSystem2D.instance.off(Contact2DType.BEGIN_CONTACT, this._onGlobalBeginContact, this); PhysicsSystem2D.instance.off(Contact2DType.END_CONTACT, this._onGlobalEndContact, this); } PhysicsManager._instance null!; } private _onGlobalBeginContact(colliderA: Collider2D, colliderB: Collider2D, contact: IPhysics2DContact | null) { // 所有碰撞事件都会进入这里 // colliderA 和 colliderB 是发生碰撞的两个碰撞体顺序不确定。 // 1. 提取分组信息 const groupA colliderA.group; const groupB colliderB.group; // 2. 根据分组组合分发到具体的游戏逻辑事件 // 示例玩家(1) 碰到 敌人(2) if ((groupA 1 groupB 2) || (groupA 2 groupB 1)) { // 确定哪个是玩家哪个是敌人 const playerCollider groupA 1 ? colliderA : colliderB; const enemyCollider groupA 2 ? colliderA : colliderB; console.log([全局监听] 玩家 ${playerCollider.node.name} 撞到敌人 ${enemyCollider.node.name}); // 派发自定义事件让游戏逻辑模块如伤害系统、UI系统去处理 physicsEventTarget.emit(PhysicsEventType.PLAYER_HIT_ENEMY, { selfCollider: playerCollider, otherCollider: enemyCollider, contact } as ICollisionEventData); } // 示例子弹(4) 碰到 敌人(2) if ((groupA 4 groupB 2) || (groupA 2 groupB 4)) { const bulletCollider groupA 4 ? colliderA : colliderB; const enemyCollider groupA 2 ? colliderA : colliderB; console.log([全局监听] 子弹 ${bulletCollider.node.name} 击中敌人 ${enemyCollider.node.name}); physicsEventTarget.emit(PhysicsEventType.BULLET_HIT_ENEMY, { selfCollider: bulletCollider, otherCollider: enemyCollider, contact } as ICollisionEventData); // 全局管理器可以直接处理一些通用逻辑比如子弹命中后消失 // bulletCollider.node.destroy(); // 注意在回调中直接销毁节点通常是安全的但最好下一帧处理避免迭代器问题。 // this.scheduleOnce(() { bulletCollider.node.destroy(); }); } // 示例玩家(1) 碰到 地面(3) - 玩家落地 if ((groupA 1 groupB 3) || (groupA 3 groupB 1)) { const playerCollider groupA 1 ? colliderA : colliderB; console.log([全局监听] 玩家 ${playerCollider.node.name} 落地); physicsEventTarget.emit(PhysicsEventType.PLAYER_ON_GROUND, { selfCollider: playerCollider, otherCollider: (groupA 3 ? colliderA : colliderB), contact } as ICollisionEventData); } } private _onGlobalEndContact(colliderA: Collider2D, colliderB: Collider2D, contact: IPhysics2DContact | null) { // 处理接触结束的逻辑比如玩家离开地面 const groupA colliderA.group; const groupB colliderB.group; if ((groupA 1 groupB 3) || (groupA 3 groupB 1)) { const playerCollider groupA 1 ? colliderA : colliderB; console.log([全局监听] 玩家 ${playerCollider.node.name} 离开地面); } } }5.2 在场景中初始化管理器并消费事件初始化管理器在场景中创建一个空节点如GameManager挂载PhysicsManager脚本。确保它在场景加载时最早运行可以通过编辑器的执行顺序调整。消费事件修改之前的PlayerController和EnemyController移除它们自身的碰撞监听注册代码this._collider.on改为监听全局管理器发出的事件。例如在EnemyController的start方法中start() { // 不再注册 collider.on // 改为监听全局事件 physicsEventTarget.on(PhysicsEventType.BULLET_HIT_ENEMY, this._onBulletHit, this); physicsEventTarget.on(PhysicsEventType.PLAYER_HIT_ENEMY, this._onPlayerHit, this); } onDestroy() { // 记得取消监听 physicsEventTarget.off(PhysicsEventType.BULLET_HIT_ENEMY, this._onBulletHit, this); physicsEventTarget.off(PhysicsEventType.PLAYER_HIT_ENEMY, this._onPlayerHit, this); } private _onBulletHit(data: ICollisionEventData) { // 判断被击中的敌人是不是自己 if (data.otherCollider.node this.node) { // otherCollider 是敌人碰撞体 console.log(Enemy(${this.node.name}) 被子弹击中事件来自全局管理器); // 处理受伤逻辑比如减血 // this._takeDamage(data.selfCollider); // selfCollider 是子弹碰撞体 } } private _onPlayerHit(data: ICollisionEventData) { if (data.otherCollider.node this.node) { console.log(Enemy(${this.node.name}) 被玩家撞到); } }全局监听的优势立刻体现逻辑集中所有碰撞判断规则都在PhysicsManager的_onGlobalBeginContact一个方法里一目了然。性能优化Enemy和Player脚本里没有了大量的on/off调用每个对象只监听自己关心的一两个自定义事件。高度解耦EnemyController不再需要知道碰撞是怎么检测的它只关心“我被击中了”这个业务事件。伤害计算、音效播放、得分更新等模块都可以独立监听这个事件互不干扰。易于扩展新增一种碰撞类型如“道具碰到玩家”只需在管理器中添加一个if判断并派发新事件所有相关系统就能自动响应。6. 核心参数详解与高级配置理解了基本流程后我们来深入看看那些影响碰撞行为的“旋钮”该怎么调。6.1 Collider2D 关键属性Sensor (感应器)勾选后碰撞体变为“触发器”。物理引擎会检测到它与其他碰撞体的重叠/分离并触发回调但不会产生实际的物理碰撞效果即不会推开物体。常用于制作伤害区域、拾取区域、触发器机关等。Density (密度)、Friction (摩擦力)、Restitution (弹性系数)这三个属性只有在碰撞体附属于一个RigidBody2D时才会生效。它们共同决定了物体的质量密度 * 体积和碰撞时的物理反应。调整它们可以模拟出木头、橡胶、钢铁等不同材质的感觉。Restitution为0表示完全非弹性碰撞如橡皮泥为1表示完全弹性碰撞理想情况下的超级弹球。通常设置在0到1之间。Offset (偏移)允许碰撞体形状相对于节点中心发生偏移。比如一个角色碰撞体可能需要在脚部而不是身体中心。6.2 RigidBody2D 关键属性Type (类型)Static静态刚体质量无限大不受力影响通常用于地面、墙壁。Kinematic运动学刚体。不受力影响但可以通过设置velocity速度来移动。常用于平台、移动的障碍物或者需要完全由代码控制的角色在一些不使用物理驱动移动的游戏中。Dynamic动态刚体。完全参与物理模拟受重力、力、碰撞影响。大部分可互动的物体都用这个。Animated动画刚体。是Kinematic的一个变种其位置和旋转由动画系统驱动但可以与Dynamic刚体发生碰撞并影响它们。常用于复杂的动画角色。Allow Sleep (允许休眠)如果开启当一个动态刚体速度几乎为零且一段时间没有碰撞时物理引擎会停止对其计算以节省性能。对于大量静止的物体如掉落后堆在一起的箱子很有用。但在需要持续检测的场景如一个随时可能被推开的箱子最好关闭。Fixed Rotation (固定旋转)防止刚体在碰撞中旋转。对于2D横版游戏的角色通常需要勾选防止角色被撞得东倒西歪。6.3 碰撞过滤Group 和 Mask这是性能优化和逻辑控制的重中之重。前面我们在项目设置里配置了分组但每个碰撞体还有两个相关属性Group这个碰撞体属于哪个分组。Mask (在Cocos Creator中通过碰撞矩阵隐式定义)这个碰撞体能与哪些分组发生碰撞。工作原理当两个碰撞体A和B可能碰撞时物理引擎会检查(A的Group 与 B的Mask) 有交集并且(B的Group 与 A的Mask) 有交集。两者都为真才会进行昂贵的形状相交检测和回调触发。最佳实践为所有游戏对象规划清晰的分组如Player, Enemy, Bullet, Ground, Item, Trigger等。在项目设置 - 物理 - 碰撞矩阵中只勾选必要的交叉格。例如Bullet和Bullet之间通常不需要碰撞检测这能立刻减少大量的无效计算。对于Sensor类型的触发器如果不需要物理响应其分组设置可以更宽松但逻辑过滤仍需在回调函数中通过group或tag进行。7. 实战构建一个完整的子弹射击碰撞案例让我们用全局监听方案实现一个玩家发射子弹击中敌人的完整例子。创建子弹预制体 (BulletPrefab)创建一个Bullet节点添加SpriteRenderer和CircleCollider2D子弹用圆形更合适。设置Group为BULLET。添加RigidBody2DType设为Dynamic。勾选Allow Sleep并设为false子弹一直在飞不应休眠勾选Fixed Rotation。设置Gravity Scale为0子弹通常不受重力影响。在Bullet节点上挂载一个Bullet.ts脚本主要控制移动和生命周期。// Bullet.ts - 简化的子弹逻辑 import { _decorator, Component, RigidBody2D, Vec2, v2 } from cc; const { ccclass, property } _decorator; ccclass(Bullet) export class Bullet extends Component { property public speed: number 800; private _rigidBody: RigidBody2D | null null; private _direction: Vec2 v2(1, 0); // 默认向右 start() { this._rigidBody this.getComponent(RigidBody2D); if (this._rigidBody) { this._rigidBody.linearVelocity new Vec2(this._direction.x * this.speed, this._direction.y * this.speed); } // 3秒后自动销毁防止堆积 this.scheduleOnce(() { this.node.destroy(); }, 3); } public setDirection(dir: Vec2) { this._direction dir.normalize(); } }修改 PhysicsManager我们已经在上一步的全局监听中处理了BULLET_HIT_ENEMY事件。当事件触发时我们可以在管理器中直接销毁子弹节点或派发事件让其他系统处理。// 在 _onGlobalBeginContact 的子弹击中敌人分支里 if ((groupA 4 groupB 2) || (groupA 2 groupB 4)) { // ... 派发事件 ... // 建议下一帧销毁避免在当前物理回调迭代中修改节点树可能的问题 this.scheduleOnce(() { bulletCollider.node.destroy(); }); }创建子弹管理器 (BulletManager)负责生成和回收子弹。可以使用对象池优化性能。// BulletManager.ts import { _decorator, Component, Node, instantiate, Prefab, Vec3, v3 } from cc; import { NodePool } from cc; const { ccclass, property } _decorator; ccclass(BulletManager) export class BulletManager extends Component { property(Prefab) bulletPrefab: Prefab | null null; private _bulletPool: NodePool new NodePool(); start() { // 初始化对象池 for (let i 0; i 20; i) { let bullet instantiate(this.bulletPrefab!); this._bulletPool.put(bullet); } } public spawnBullet(startPos: Vec3, direction: Vec3) { let bullet: Node null; if (this._bulletPool.size() 0) { bullet this._bulletPool.get(); } else { bullet instantiate(this.bulletPrefab!); } // 初始化子弹位置、方向 bullet.setWorldPosition(startPos); bullet.parent this.node; // 挂载到管理器节点下或场景根节点 const bulletComp bullet.getComponent(Bullet) as any; // 获取脚本组件 if (bulletComp bulletComp.setDirection) { bulletComp.setDirection(v3(direction.x, direction.y, 0)); } bullet.active true; return bullet; } public recycleBullet(bullet: Node) { bullet.active false; this._bulletPool.put(bullet); } }修改 PhysicsManager 以使用对象池当子弹需要销毁时通知BulletManager回收。// 在PhysicsManager中 private _onGlobalBeginContact(...) { // ... 子弹击中敌人逻辑 ... // this.scheduleOnce(() { bulletCollider.node.destroy(); }); // 改为回收 this.scheduleOnce(() { // 假设你能访问到BulletManager实例 BulletManager.instance.recycleBullet(bulletCollider.node); }); }玩家射击在PlayerController中添加射击逻辑调用BulletManager.instance.spawnBullet生成子弹。至此一个使用全局监听、对象池优化、分组过滤的完整2D碰撞射击系统就搭建完成了。这套架构清晰、高效足以支撑中小型2D游戏的碰撞需求。8. 避坑指南与性能优化“鬼畜”抖动或穿透原因通常是因为RigidBody2D的速度过快在一帧内穿过了另一个碰撞体。物理引擎的离散检测Discrete Detection可能捕捉不到。解决启用连续碰撞检测 (CCD)在RigidBody2D组件上将Bullet属性设为true对于高速子弹非常有效。增加物理更新频率在项目设置 - 物理 - 物理 2D中减小Fixed Time Step如从1/60改为1/120但会增加CPU开销。使用射线检测预判对于特别重要的碰撞如玩家与地面可以在移动前先用PhysicsSystem2D.raycast进行检测。回调函数内进行耗时操作碰撞回调在物理线程中调用如果在这里进行复杂的计算、加载资源、创建大量对象会严重阻塞物理模拟导致游戏卡顿。应将复杂的逻辑推迟到主线程更新循环中处理例如使用scheduleOnce或标记一个状态在update里处理。内存泄漏务必在onDestroy或onDisable中取消事件监听无论是组件监听(collider.off)还是全局监听(physicsEventTarget.off)。否则节点销毁后回调函数可能仍被引用导致内存无法释放和潜在的错误。碰撞分组配置错误这是最常见的问题之一。明明觉得应该碰撞的两个物体却没有触发回调。第一件事就是去检查项目设置中的碰撞矩阵确保对应的分组交叉框被勾选上了。Sensor 与 非Sensor 的混淆如果你希望两个物体“重叠”但不互相推开只触发事件那么至少其中一个的碰撞体需要勾选Sensor。如果两个都是非Sensor它们会尝试推开对方这可能不是你想要的触发器效果。性能瓶颈控制碰撞体数量每个活跃的Dynamic刚体都会增加计算负担。对于静止的物体使用Static类型。对于大量相同物体如子弹使用对象池。简化碰撞形状PolygonCollider2D虽然精确但比BoxCollider2D和CircleCollider2D更耗性能。尽量用简单的形状组合来近似复杂形状。善用碰撞矩阵这是最有效的优化手段杜绝不必要的碰撞检测对。调试技巧开启PhysicsSystem2D.instance.debugDrawFlags可以在场景中直观地看到所有碰撞体的轮廓、AABB包围盒、刚体中心等对于调整碰撞体位置和大小、排查碰撞问题有奇效。在碰撞回调中打印详细的日志包括碰撞双方的name、group、tag可以帮助你快速定位是哪个物体、哪种碰撞没有按预期触发。9. 常见问题排查速查表问题现象可能原因排查步骤碰撞没有触发任何回调1. 碰撞分组未匹配。2. 碰撞体未启用。3. 脚本未正确注册监听。1. 检查项目设置-物理-碰撞矩阵。2. 检查碰撞体组件enabled是否为true。3. 在start或onEnable中加断点或日志确认on被调用。物体直接穿过没有碰撞效果1. 速度过快穿透。2. 至少一方是Sensor。3. 刚体类型为Kinematic且未处理碰撞。1. 对高速物体启用Bullet(CCD)。2. 检查碰撞体Sensor属性。3.Kinematic刚体需手动处理碰撞响应。回调函数触发了但物体表现异常如抖动1. 在回调中错误地修改了物理状态如重复施加力。2. 碰撞体形状或位置有误。1. 检查回调逻辑避免每帧STAY_CONTACT时重复操作。2. 开启调试绘制检查碰撞体是否与精灵匹配。游戏卡顿物体多时更明显1. 动态刚体数量过多。2. 碰撞矩阵配置太宽泛。3. 在碰撞回调中进行了复杂操作。1. 使用对象池将静止物体设为Static。2. 收紧碰撞矩阵取消不必要的检测。3. 将回调中的复杂逻辑移到主线程。节点销毁后报错如读取null属性事件监听未正确移除。确保在onDestroy中调用collider.off或全局事件管理器的off。onBeginContact被多次触发同一对碰撞体在同一时间可能因多个接触点产生多次回调。这是正常现象。如果逻辑上只需要一次可以使用标志位hasHandled在第一次触发后忽略后续。掌握2D碰撞监听就像是拿到了构建游戏交互世界的钥匙。从简单的BoxCollider2D配置到复杂的PhysicsSystem2D全局事件管理每一步都需要理解其背后的物理规则和引擎机制。我个人的经验是在项目初期就采用全局监听架构虽然前期会多花一点时间设计事件系统但随着游戏逻辑的增长你会发现这份投入在维护性和扩展性上带来了巨大的回报。最后多利用调试绘制功能它能让你“看见”碰撞的世界很多问题就一目了然了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价