Cocos Creator 3D物理交互:碰撞检测与触发器实战指南

Cocos Creator 3D物理交互:碰撞检测与触发器实战指南
1. 项目概述为什么3D物理交互是游戏体验的基石在Cocos Creator里折腾3D项目尤其是涉及到角色移动、物体交互、战斗判定这些核心玩法时物理引擎的碰撞检测和触发器绝对是绕不开的坎。很多开发者特别是从2D项目转过来的朋友初次接触3D物理时容易懵圈明明在编辑器里看着两个物体挨着了怎么就是没触发事件刚体加了一堆性能怎么就莫名其妙卡顿了触发器Trigger和碰撞体Collider到底该用哪个这些问题背后是对3D物理引擎工作原理理解不透彻。今天我就结合自己踩过的无数个坑把Cocos Creator 3.x内置的物理引擎无论是基于Cannon.js还是ammo.js中关于碰撞检测和触发器的那些门道掰开揉碎了讲清楚。这不仅仅是几个API的调用更关乎你游戏的手感、性能和逻辑的严谨性。一个响应精准、效率优化的物理交互系统是让玩家沉浸其中的关键一步。无论你是在做一款跑酷游戏、FPS射击还是带有解谜元素的RPG吃透这部分内容都能让你事半功倍。2. 核心概念辨析碰撞体、刚体、触发器与物理世界在深入细节之前我们必须先统一语言理清几个最基础也最容易混淆的概念。很多问题都源于概念的误用。2.1 物理组件三巨头RigidBody, Collider, Trigger首先Cocos Creator的3D物理系统是组件化的。一个物体想要参与物理模拟通常需要以下组件之一或组合刚体RigidBody这是物理模拟的“灵魂”。它为节点赋予了物理属性如质量mass、速度velocity、阻尼damping等。刚体决定了物体如何响应力和碰撞。一个带有刚体的物体会受到重力影响与其他刚体碰撞时会根据物理定律动量守恒等产生反弹、旋转等运动。简单说刚体管“运动”。碰撞体Collider这是物理交互的“身体”或“边界”。它定义了物体在物理世界中的形状和范围用于检测与其他物体的接触。Cocos Creator提供了多种3D碰撞体如盒型BoxCollider、球型SphereCollider、胶囊型CapsuleCollider和网格型MeshCollider。碰撞体本身不产生物理反馈如反弹它只负责“报告”接触事件。你可以把它想象成一个隐形的、不可穿透的“壳”。触发器Trigger触发器本质上是一个特殊的碰撞体。当你勾选碰撞体组件上的isTrigger属性时它就变成了一个触发器。触发器的关键特性是它不会产生物理碰撞反馈。两个触发器或者一个触发器与一个普通碰撞体/刚体接触时它们会相互穿透但物理引擎会生成“触发事件”通知你。这就像是一个无形的感应区域常用于检测玩家进入某个区域如宝箱触发范围、存档点、拾取物品金币、血包等逻辑。重要心得最经典的组合是“刚体 碰撞体”。刚体提供物理属性质量、重力碰撞体提供形状。如果节点只有碰撞体而没有刚体它就是一个静态的、不可移动的障碍物如地面、墙壁。如果节点只有刚体而没有碰撞体几乎不会这样它就无法参与碰撞检测。2.2 物理世界PhysicsWorld与模拟步进所有物理计算都发生在一个独立的物理世界PhysicsWorld中。在Cocos Creator中你可以通过PhysicsSystem.instance访问它。物理世界有自己的重力、速度迭代次数等全局设置。这里有一个至关重要的概念物理模拟是离散的。引擎并不是每时每刻都在计算而是以一个固定的时间间隔默认为1/60秒约16.67ms进行“步进”step。在每一步进中引擎计算所有刚体的新位置、速度并检测碰撞。这就引出了一个常见问题为什么我的物体有时会“穿模”如果物体的速度非常快比如子弹在一帧的物理步进中它从墙的一侧直接移动到了另一侧中间没有与墙的碰撞体在任何一个步进点上发生重叠那么这次碰撞就会被漏掉。为了解决这个问题就需要用到连续碰撞检测CCD我们会在后面详细讨论。2.3 碰撞矩阵Collision Matrix谁和谁能碰撞不是所有物体都需要相互碰撞。子弹和子弹之间可能不需要碰撞敌人和敌人之间可能有避免堆叠的需求。Cocos Creator通过碰撞分组Group和碰撞矩阵来高效管理这一点。每个碰撞体或刚体都有一个group属性属于某个分组如 DEFAULT、PLAYER、ENEMY、BULLET、PROP等。在项目设置 - 物理中有一个碰撞矩阵表格。你可以在这里定义任意两个分组之间是否启用碰撞检测。分组A \ 分组BDEFAULTPLAYERENEMYBULLETDEFAULT✅✅✅❌PLAYER✅❌✅❌ENEMY✅✅❌✅BULLET❌❌✅❌示例玩家和玩家不碰撞子弹只与敌人碰撞子弹与玩家和默认物体都不碰撞配置技巧合理规划碰撞分组是优化性能的第一步。减少不必要的碰撞检测对性能提升非常明显尤其是在移动端。对于永远不会发生交互的物体如背景装饰物可以将其设置为不与任何分组碰撞或者直接不挂物理组件。3. 碰撞检测全流程解析与事件监听理解了基础概念我们来看碰撞检测是如何工作的以及如何编写代码来响应它。3.1 碰撞检测的生命周期从开始接触到分离一次完整的碰撞非触发器过程会触发三个关键事件按顺序是onCollisionEnter当两个碰撞体的形状开始接触的那一帧触发。这是最常用的事件用于处理碰撞开始的逻辑如播放撞击音效、扣血、产生粒子特效。onCollisionStay在接触持续的每一帧触发。注意是物理更新的每一帧而不是渲染帧。如果你的游戏逻辑帧率如30帧低于物理帧率60帧它可能在一逻辑帧内被多次触发。常用于处理持续接触的效果如站在灼热地面上持续掉血、物体被持续挤压。onCollisionExit当两个碰撞体分离的那一帧触发。用于处理接触结束的逻辑如离开毒气区域停止扣血、松开一个被推动的物体。对于触发器Trigger则有对应的一组三个事件onTriggerEnteronTriggerStayonTriggerExit它们的触发时机与碰撞事件类似但本质是“穿过”而非“撞击”。3.2 如何编写事件监听代码在Cocos Creator中你需要在一个挂载在节点上的组件脚本中实现这些回调函数。组件必须继承自Component。import { _decorator, Component, Collider, ITriggerEvent, ICollisionEvent } from cc; const { ccclass, property } _decorator; ccclass(MyPhysicsComponent) export class MyPhysicsComponent extends Component { onLoad() { // 获取碰撞体组件 const collider this.getComponent(Collider); if (collider) { // 注册碰撞事件监听器 collider.on(onCollisionEnter, this.onCollisionEnter, this); collider.on(onCollisionStay, this.onCollisionStay, this); collider.on(onCollisionExit, this.onCollisionExit, this); // 注册触发器事件监听器如果isTrigger为true collider.on(onTriggerEnter, this.onTriggerEnter, this); collider.on(onTriggerStay, this.onTriggerStay, this); collider.on(onTriggerExit, this.onTriggerExit, this); } } // 碰撞事件回调 onCollisionEnter(event: ICollisionEvent) { console.log(碰撞开始 with:, event.otherCollider.node.name); // event.otherCollider 是与之发生碰撞的另一个碰撞体 // event.otherCollider.node 是另一个节点 // 可以通过分组或节点名、标签来判断碰撞对象 if (event.otherCollider.group 2) { // 假设2是ENEMY分组 this.handleEnemyCollision(); } } onCollisionStay(event: ICollisionEvent) { // 持续碰撞逻辑 } onCollisionExit(event: ICollisionEvent) { console.log(碰撞结束 with:, event.otherCollider.node.name); } // 触发器事件回调 onTriggerEnter(event: ITriggerEvent) { console.log(触发进入 with:, event.otherCollider.node.name); // 例如玩家进入宝箱范围显示“按F打开”的UI if (event.otherCollider.node.name Player) { this.showOpenHint(); } } onTriggerStay(event: ITriggerEvent) { // 持续触发逻辑例如在治疗区域内持续回血 } onTriggerExit(event: ITriggerEvent) { console.log(触发离开 with:, event.otherCollider.node.name); // 例如玩家离开宝箱范围隐藏提示UI this.hideOpenHint(); } // 记得在组件销毁时取消监听防止内存泄漏 onDestroy() { const collider this.getComponent(Collider); if (collider) { collider.off(onCollisionEnter, this.onCollisionEnter, this); // ... 取消所有已注册的事件 } } }关键细节与避坑指南事件注册时机通常在onLoad或start生命周期中注册。确保此时碰撞体组件已经初始化完成。event对象回调函数接收的事件对象包含了所有关键信息最重要的是event.otherCollider通过它可以获取到发生交互的另一个物体。性能注意onCollisionStay和onTriggerStay每物理帧都会触发避免在这里执行沉重的逻辑如复杂的数学计算、频繁的实例化销毁操作。可以考虑设置一个计时器每隔几帧执行一次逻辑。分组判断优先使用碰撞分组 (event.otherCollider.group) 来判断交互对象比通过节点名 (node.name) 或标签 (node.tag) 判断性能更高、更规范。因为分组检查发生在物理引擎底层过滤掉了大量不必要的交互。3.3 射线检测Raycast非接触式查询除了被动等待碰撞事件主动查询物理世界更为常用这就是射线检测。它就像从一点发射一道激光检测沿途击中的第一个或所有物体。常用于鼠标点击拾取3D物体。第一人称射击游戏的子弹命中判定配合高速子弹穿模问题。角色移动前的障碍物探测如防止卡墙。import { geometry, PhysicsSystem } from cc; // 创建一条射线从原点(0,0,0)沿Z轴正方向发射长度100 const ray new geometry.Ray(0, 0, 0, 0, 0, 1); const maxDistance 100; const mask 0xffffffff; // 检测所有分组 // 方法1检测所有命中可能性能开销大 const results: PhysicsSystem.PhysicsRayResult[] []; if (PhysicsSystem.instance.raycast(ray, mask, maxDistance, results)) { for (const result of results) { console.log(击中了:, result.collider.node.name, 距离:, result.distance); // result.hitPoint 是世界坐标中的命中点 // result.hitNormal 是命中面的法线 } } // 方法2只检测第一个命中更高效 const result PhysicsSystem.instance.raycastClosest(ray, mask, maxDistance); if (result) { console.log(最近击中了:, result.collider.node.name); // 可以在此处处理命中逻辑如造成伤害 const hurtComp result.collider.node.getComponent(EnemyHealth); if (hurtComp) { hurtComp.takeDamage(10); } }射线检测的优化技巧合理使用raycastClosest如果你只关心最近的命中如子弹命中一定要用raycastClosest而不是raycast前者在找到第一个有效命中后就会停止性能好得多。善用检测掩码mask不要总是用全掩码0xffffffff。你可以通过分组来计算掩码只检测你关心的物体。例如只检测ENEMY和PROP分组。const enemyGroup 1 2; // 假设ENEMY分组索引是2 const propGroup 1 3; // 假设PROP分组索引是3 const rayMask enemyGroup | propGroup; // 只检测敌人和道具避免每帧进行大量射线检测尤其是在移动端。对于非即时性需求如AI的视野检测可以降低检测频率比如每3-5帧检测一次。4. 高级议题与性能优化实战掌握了基础用法我们来看看那些让项目更稳健、更高效的高级技术和避坑经验。4.1 解决“穿模”问题连续碰撞检测CCD如前所述高速移动的物体子弹、下落的玩家容易穿过薄墙。Cocos Creator的物理引擎支持连续碰撞检测CCD。如何启用CCD在刚体组件上启用找到高速移动物体的RigidBody组件将useCCD属性勾选为true。理解原理与代价CCD不是魔法。它通过在两帧之间进行额外的插值检测来预测碰撞会显著增加计算开销。因此绝对不要给所有刚体都启用CCD只给那些确实需要的高速移动物体启用。配合设置刚体速度CCD的触发通常与刚体的线性速度 (linearVelocity) 有关。引擎内部有一个阈值当速度超过阈值时CCD才会生效。这个阈值有时可以调整但更常见的做法是确保你的高速物体速度设置合理。替代方案对于子弹这类超高速物体一个更常见且高效的做法是不使用真实的物理刚体运动而是使用射线检测。每一帧计算子弹从上一帧位置到当前帧位置的线段并对这条线段进行射线检测。如果击中则处理命中逻辑。这完全避免了穿模且性能可控。4.2 物理材质PhysicsMaterial与交互反馈碰撞不仅仅是检测还有反馈比如弹力、摩擦力。这由物理材质控制。你可以在资源管理器创建.physics-material资源并赋值给碰撞体组件的material属性。摩擦力Friction影响物体沿表面滑动的难易程度。值在0-1之间。0像冰面1像砂纸。通常需要为角色控制器脚下的碰撞体设置合适的摩擦力。弹性Restitution即弹力系数。0表示完全非弹性碰撞如橡皮泥1表示完全弹性碰撞理想情况下的超级弹球。大于1会获得能量不真实但可用于特效。注意两个碰撞体碰撞时的综合弹性通常取两者中较大的值但具体公式依赖底层引擎。滚动摩擦力Rolling Friction专门抵抗旋转的摩擦力对于球、轮胎等物体很重要。配置经验默认的物理材质default-physics-material通常摩擦力为0.5弹性为0.1是一个不错的起点。为不同材质的物体金属、木头、橡胶创建不同的物理材质能极大提升物理交互的真实感。过高的弹性会导致物体在碰撞后不断弹跳难以稳定可能需要增加刚体的线性阻尼linearDamping和角度阻尼angularDamping来消耗能量使其更快停下。4.3 性能优化黄金法则3D物理计算是性能消耗大户。以下是我总结的几条铁律精简碰撞体形状永远优先使用基础形状Box, Sphere, Capsule。MeshCollider网格碰撞体虽然形状精确但性能开销是基础形状的数十倍甚至上百倍。对于复杂的模型如一棵树、一个角色使用一个或多个基础形状如胶囊体几个盒子来近似其轮廓这是行业标准做法。善用碰撞分组如前所述通过碰撞矩阵禁用所有不必要的碰撞对。这是零成本就能获得的性能提升。控制动态刚体数量受重力影响、会被力推动的刚体称为动态刚体。场景中同时活跃的动态刚体数量应严格控制例如移动端建议不超过50个。对于大量的小型可交互物体如一堆金币可以考虑使用简化物理或非物理方案。将静态物体标记为静态对于永远不会移动的地形、建筑确保其节点只有碰撞体没有刚体。物理引擎会将它们优化为静态对象享受最好的性能。休眠Sleeping机制当刚体速度几乎为零并持续一段时间后物理引擎会将其置为“休眠”状态不再计算其物理。确保你的刚体allowSleep属性为true默认是。不要每一帧都去唤醒静止的物体。减少onCollisionStay中的操作再次强调这里是性能热点。如果需要在持续接触时每帧都做判断可以考虑在onCollisionEnter中启动一个计时器或标记在update中根据标记执行逻辑并在onCollisionExit中清除标记。4.4 常见问题排查实录问题1碰撞事件不触发。检查1碰撞分组确认两个物体的碰撞分组在碰撞矩阵中是启用状态。检查2刚体与碰撞体至少有一方需要有刚体组件才能产生非触发器的碰撞事件。如果双方都只有碰撞体无刚体它们会相互穿透不会触发onCollisionEnter。检查3节点缩放碰撞体的尺寸依赖于节点的缩放。如果节点缩放为0碰撞体实际上不可见。检查节点和其父节点的scale属性是否为(1,1,1)。检查4组件启用状态检查碰撞体组件和刚体组件左上角的复选框是否被勾选启用。检查5事件监听注册确认脚本已挂载且onLoad或start中的事件监听代码已执行。可以在onLoad里加个console.log调试。问题2触发器isTrigger事件不触发。检查触发器事件要求至少有一方是触发器。如果A是触发器B是普通碰撞体会触发。如果A和B都是触发器也会触发。如果A和B都是普通碰撞体isTrigger都为false则触发的是碰撞事件而非触发器事件。问题3物体抖动或陷入地面。这是经典问题。通常是因为两个碰撞体相互嵌入过深物理引擎在每一帧都试图将它们推开导致振荡。解决方案增加碰撞体的contactOffset接触偏移或引擎的defaultContactOffset在项目物理设置中。这个值定义了两个物体多近时开始计算碰撞力。适当增加如从0.01增加到0.05可以提前产生排斥力避免嵌入。检查碰撞体尺寸。确保用于角色控制的胶囊体或盒子底部不要离“脚底”太近留一点空隙。对于地面等静态物体可以尝试稍微增加其碰撞体的厚度。问题4物理模拟不稳定在不同设备上表现不一致。确保使用固定的时间步长进行物理更新。在Cocos Creator中这通常是引擎自动处理的。但要避免在update中直接修改刚体的位置 (position)而应该使用力 (applyForce) 或冲量 (applyImpulse)或者设置速度 (linearVelocity)。直接改position会干扰物理引擎的插值计算。如果游戏帧率波动很大可以考虑在项目设置中锁定最大帧率为物理模拟提供一个更稳定的环境。5. 实战案例构建一个简单的3D角色交互系统让我们用一个综合案例把上面的知识串起来。目标是创建一个3D角色可以移动、跳跃能拾取场景中的物品触发器并与敌人碰撞体发生碰撞受伤。5.1 角色控制器设置节点结构创建一个空节点Player作为根节点。其下创建一个子节点Model存放视觉模型如glTF模型再创建一个子节点ColliderNode。物理组件在Player根节点上添加RigidBody组件。设置质量、阻尼并勾选lockRotation的X和Z轴防止角色摔倒。在ColliderNode上添加CapsuleCollider组件调整其高度和半径以匹配角色模型。将ColliderNode的位置Y轴向上调整使其底部与角色脚底对齐。移动脚本在Player节点上挂载自定义脚本PlayerController.ts。在update中读取输入如键盘WASD计算移动方向向量然后通过this.getComponent(RigidBody).setLinearVelocity(targetVelocity)来设置刚体的线性速度实现移动。这是最简单直接的移动方式但手感可能偏“滑”。更高级的做法是使用力 (applyForce) 或冲量 (applyImpulse)。跳跃使用射线检测判断角色是否着地从脚底向下发射短射线检测地面。当按下跳跃键且着地时给刚体施加一个向上的冲量rigidBody.applyImpulse(new Vec3(0, jumpForce, 0))。5.2 物品触发器实现创建物品创建一个节点Coin加上视觉模型如一个方块或金币模型。添加触发器为Coin节点添加SphereCollider组件勾选isTrigger。调整球体大小作为拾取范围。编写拾取逻辑创建脚本Coin.ts挂载在Coin上。// Coin.ts onTriggerEnter(event: ITriggerEvent) { // 判断触发对象是否是玩家 if (event.otherCollider.node.name Player) { console.log(拾取到金币); // 播放拾取音效、粒子特效 this.playPickupEffect(); // 增加玩家金币数可以通过全局管理器 GameManager.instance.addCoin(1); // 销毁自身或设置为非激活放入对象池 this.node.destroy(); } }5.3 敌人碰撞体与伤害判定创建敌人类似角色创建Enemy节点包含模型和碰撞体如BoxCollider。为其添加刚体但可以设置质量为0或勾选isKinematic运动学刚体如果你希望它由代码控制移动而非物理驱动。伤害区域有时我们不想让敌人的整个身体都造成伤害比如碰到手臂就掉血不合理。可以在敌人节点下创建一个子节点DamageArea为其添加一个BoxCollider并勾选isTrigger。这个触发器代表有效的伤害范围。玩家受伤逻辑在玩家的PlayerController.ts脚本中监听碰撞事件。// PlayerController.ts 中补充 onCollisionEnter(event: ICollisionEvent) { // 判断碰撞对象是否是敌人的伤害区域 if (event.otherCollider.node.name DamageArea) { // 获取敌人的伤害值组件 const enemy event.otherCollider.node.parent.getComponent(Enemy); if (enemy) { this.takeDamage(enemy.attackPower); // 可能还需要一个击退效果 this.applyKnockback(event.otherCollider.node.worldPosition); } } }5.4 使用分组优化为上述对象设置分组Player: 分组PLAYEREnemy: 分组ENEMYEnemy下的DamageArea: 分组ENEMY_ATTACK(或复用ENEMY)Coin: 分组ITEM在项目设置的碰撞矩阵中配置PLAYER与ENEMY_ATTACK碰撞 ✅PLAYER与ITEM触发 ✅ (因为ITEM是触发器)PLAYER与PLAYER不碰撞 ❌ENEMY_ATTACK与ITEM不交互 ❌这样物理引擎底层会直接忽略大量不必要的检测比如敌人之间不会相互碰撞卡住玩家的子弹如果另有分组也可以精确配置只与敌人碰撞。通过这样一个完整的案例你应该能清晰地看到碰撞检测、触发器、分组、事件监听是如何协同工作构建出一个可玩、高效、易维护的3D游戏交互核心的。记住物理引擎是工具理解其原理遵循最佳实践才能让它为你所用而不是被它层出不穷的“怪现象”所困扰。多实践多调试积累的经验会让你在应对更复杂的物理交互时游刃有余。