
1. 项目概述为什么2D碰撞监听是个“坑”做2D游戏碰撞检测和监听是绕不开的核心功能。在Cocos Creator 3.6这个版本里引擎提供了两套物理后端供我们选择经典的Box2D和引擎内置的物理系统。乍一看这给了开发者选择的自由但实际用起来你会发现从API调用、碰撞事件的触发逻辑到性能表现这两套系统存在着不少微妙的差异。很多开发者尤其是从Cocos Creator 2.x升级上来或者初次接触3.x物理系统的朋友很容易在这里栽跟头。我自己就踩过不少坑。比如明明设置了碰撞分组为什么Box2D里两个物体穿过去了而内置物理里却触发了事件又比如为什么在Box2D里监听onBeginContact很稳定换到内置物理后碰撞事件有时会漏报或者重复触发这些问题往往不是代码写错了而是对两套系统底层机制的理解不够透彻。这篇内容就是把我趟过的这些“坑”和填坑的经验系统地梳理给你。无论你是正在做一款2D游戏还是在为项目选择物理方案希望这些实战心得能帮你少走弯路把精力更多地花在游戏玩法本身而不是和物理引擎较劲。2. 核心差异解析Box2D与内置物理的底层逻辑要避开坑首先得明白你脚下的路是怎么铺的。Cocos Creator 3.6里这两套物理系统虽然目标一致但“出身”和“性格”截然不同。2.1 Box2D老牌劲旅的严谨与“固执”Box2D是C编写的、久经沙场的2D物理引擎以模拟真实、稳定可靠著称。Cocos Creator通过JavaScript绑定将其引入。它的工作流程非常经典且严谨物理世界更新在每一帧引擎驱动Box2D世界进行物理模拟积分计算更新所有刚体的位置、速度。碰撞检测Box2D内部使用连续碰撞检测CCD和分离轴定理SAT等算法精确计算哪些形状发生了接触或重叠。接触点求解对于发生碰撞的物体Box2D会计算接触点、法向量、穿透深度等详细信息并准备施加冲量来解决穿透问题。事件回调在物理步长的特定阶段通常是求解器迭代之后Box2D会遍历所有接触并根据接触状态的变化开始接触、持续接触、结束接触通过Cocos Creator封装的接口触发对应的JavaScript回调函数如onBeginContact。Box2D的“固执”体现在它对物理模拟的绝对权威。事件触发严格依赖于物理世界的模拟结果。如果一个刚体被设置为传感器isSensor: true它只报告碰撞事件不参与物理力的响应这个逻辑非常清晰。2.2 内置物理系统引擎原生的灵活与“变通”内置物理系统是Cocos Creator团队用TypeScript实现的物理模块。它的优势在于与引擎其他部分如渲染、动画、UI的深度集成和无缝对接避免了跨语言绑定的开销。它的工作逻辑更贴近游戏引擎的帧循环与引擎帧同步物理更新紧密耦合在引擎的主循环中。基于包围盒的快速检测为了提高性能内置物理系统通常会先使用AABB轴对齐包围盒进行粗粒度的碰撞筛选然后再进行更精确的形状检测如圆形、矩形、多边形。事件派发机制碰撞事件的触发可能更直接地与引擎的事件系统挂钩。它的检测和事件派发逻辑可能为了性能和易用性做了一些优化或简化。这种“变通”带来了灵活性但有时也导致其行为与开发者从Box2D那里建立的直觉不符。例如在某些边缘情况下为了保持帧率的平滑内置物理可能会合并或跳过一些快速发生的碰撞事件。2.3 关键差异对比表为了更直观我把核心差异总结成下表特性维度Box2D内置物理系统出身第三方C库通过绑定接入Cocos Creator原生TypeScript实现核心优势物理模拟准确性高功能全面社区资料丰富与引擎集成度高无绑定开销启动可能更快事件触发精度高严格遵循物理模拟步骤较高但可能因性能优化策略有细微差异传感器行为明确只触发事件无物理响应行为一致但底层实现逻辑不同性能特点复杂场景下模拟开销稳定但JS-C通信有成本简单场景轻量深度集成可能减少开销复杂场景需评估调试可视化需通过PhysicsSystem2D.instance.debugDrawFlags开启有独立的调试绘制组件或系统设置学习曲线需理解Box2D的一些概念世界、夹具、形状更符合Cocos组件化思维API可能更“亲民”注意选择哪一套没有绝对答案。如果你的游戏对物理真实性要求极高比如一款复杂的平台解谜游戏或者你之前有Box2D经验那么Box2D可能是更稳妥的选择。如果你的游戏物理简单比如点击消除类或者非常看重包体大小和启动速度想追求极致的引擎一体化体验那么内置物理系统值得尝试。最忌讳的是在项目中期来回切换那会引入大量不可预知的问题。3. 碰撞监听实战从配置到代码的避坑要点理解了底层差异我们来看具体怎么用。这里面的坑往往藏在细节里。3.1 物理组件的正确配置无论是Box2D还是内置物理使用前都必须确保物理组件正确挂载和配置。1. 刚体组件RigidBody2D 这是物理物体的核心。常见的坑是忘记启用或类型设置错误。// 在节点上添加 RigidBody2D 组件 const rigidBody this.node.addComponent(RigidBody2D); // 坑1忘记设置类型默认为静态Static不会运动 rigidBody.type RigidBody2D.Type.Dynamic; // 动态物体参与全部物理模拟 // rigidBody.type RigidBody2D.Type.Kinematic; // 运动物体不受力但可通过速度控制 // rigidBody.type RigidBody2D.Type.Static; // 静态物体完全不动性能最好 // 坑2没有启用刚体 rigidBody.enabled true; // 坑3Box2D需特别注意线性阻尼和角阻尼 // 过高的阻尼会让物体瞬间“粘住”感觉像没发生碰撞一样 rigidBody.linearDamping 0.1; // 线性阻尼默认值可能偏高根据手感调整 rigidBody.angularDamping 0.1; // 角阻尼对于内置物理这些参数同样有效但内部计算方式可能有别需要实际测试手感。2. 碰撞体组件BoxCollider2D, CircleCollider2D, PolygonCollider2D等 碰撞体定义了物体的物理形状。这里的坑主要关于尺寸、偏移和传感器设置。const collider this.node.addComponent(BoxCollider2D); // 坑4尺寸Size是本地坐标系下的受节点缩放影响 collider.size new Vec2(100, 50); // 假设节点scale是(1,1) // 如果节点scale是(2,2)最终物理世界中的大小会是(200,100) // 坑5偏移Offset容易忽略 // 如果你希望碰撞框不在节点中心可以调整offset collider.offset new Vec2(10, 0); // 向右偏移10像素 // 坑6传感器Sensor的误解 collider.sensor true; // 设置为传感器 // 传感器会检测碰撞并触发事件但物理上会“穿透”对方不产生力的作用。 // **重要**在Box2D中两个传感器碰撞也会触发onBeginContact。在内置物理中这一行为应当一致但务必测试验证。3. 物理分组与掩码Group Mask 这是控制“谁和谁碰撞”的核心规则也是最容易混乱的地方。它是一套位掩码系统。Group物体属于哪个分组一个整数通常用预定义的枚举如PHYSICS_GROUP。Mask物体会与哪些分组的物体进行碰撞检测也是一个整数位掩码。踩坑重灾区理解“检测”与“响应”的分离。// 假设定义分组 enum PhysicsGroup { DEFAULT 1 0, // 二进制 0001 PLAYER 1 1, // 二进制 0010 ENEMY 1 2, // 二进制 0100 ITEM 1 3, // 二进制 1000 } // 玩家节点配置 const playerCollider playerNode.getComponent(Collider2D); playerCollider.group PhysicsGroup.PLAYER; // 属于PLAYER组 // Mask希望玩家能与ENEMY和ITEM碰撞检测 playerCollider.mask PhysicsGroup.ENEMY | PhysicsGroup.ITEM; // 0010 | 1000 1010 // 敌人节点配置 const enemyCollider enemyNode.getComponent(Collider2D); enemyCollider.group PhysicsGroup.ENEMY; // 敌人只与PLAYER检测碰撞 enemyCollider.mask PhysicsGroup.PLAYER; // 0010 // 物品节点配置 const itemCollider itemNode.getComponent(Collider2D); itemCollider.group PhysicsGroup.ITEM; itemCollider.mask PhysicsGroup.PLAYER; // 0010坑7碰撞发生的必要条件是A的Mask包含B的Group并且B的Mask也包含A的Group。这是一个“与”操作。在上例中玩家和敌人能碰撞因为(PLAYER.mask ENEMY.group) ! 0且(ENEMY.mask PLAYER.group) ! 0。如果敌人mask0则即使玩家mask包含了敌人也不会发生碰撞。坑8Box2D特有在Box2D中如果两个物体都是静态Static刚体无论分组掩码如何设置它们之间永远不会触发碰撞事件。这是Box2D出于性能的优化。如果需要两个静态物体检测碰撞比如两个陷阱区域至少其中一个需要设置为**运动学Kinematic**刚体或者使用传感器。3.2 碰撞事件监听代码的写法监听碰撞事件通常有两种方式在组件内实现回调接口或者直接监听事件。方法一实现接口推荐更清晰import { _decorator, Component, ICollision2DContact, Collider2D } from cc; const { ccclass, property } _decorator; ccclass(PlayerController) export class PlayerController extends Component implements IPhysics2DContact { onLoad() { // 获取碰撞体组件 const collider this.getComponent(Collider2D); if (collider) { // 坑9忘记注册回调这是最常犯的错误。 collider.on(onCollisionEnter, this.onCollisionEnter, this); collider.on(onCollisionStay, this.onCollisionStay, this); collider.on(onCollisionExit, this.onCollisionExit, this); // 对于触发器传感器使用以下事件 // collider.on(onTriggerEnter, this.onTriggerEnter, this); // collider.on(onTriggerStay, this.onTriggerStay, this); // collider.on(onTriggerExit, this.onTriggerExit, this); } } // 碰撞开始 onCollisionEnter (selfCollider: Collider2D, otherCollider: Collider2D, contact: ICollision2DContact | null) { console.log([碰撞开始] ${selfCollider.node.name} 撞上了 ${otherCollider.node.name}); // contact对象包含了碰撞的详细信息法向量、穿透深度等但在内置物理中可能为null或信息简化 // 坑10不要假设contact对象总是可用的特别是在内置物理下需要做判空处理。 if (contact) { // 可以获取碰撞点等信息 } // 根据otherCollider分组进行逻辑处理 // ... } // 碰撞持续中每帧调用 onCollisionStay (selfCollider: Collider2D, otherCollider: Collider2D, contact: ICollision2DContact | null) { // 可用于持续伤害、摩擦音效等 } // 碰撞结束 onCollisionExit (selfCollider: Collider2D, otherCollider: Collider2D, contact: ICollision2DContact | null) { console.log([碰撞结束] ${selfCollider.node.name} 离开了 ${otherCollider.node.name}); } onDestroy() { // 坑11组件销毁时务必移除事件监听防止内存泄漏和报错。 const collider this.getComponent(Collider2D); if (collider) { collider.off(onCollisionEnter, this.onCollisionEnter, this); // ... 移除其他事件 } } }方法二使用事件监听器this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this); // 这是输入事件类比 // 物理事件监听方式类似但更推荐上面的接口方式作用域更明确。坑12onCollisionStay的调用频率。这个事件在碰撞持续期间每帧都会触发。如果在onCollisionStay里执行非常耗时的操作比如每帧实例化对象、发起网络请求会迅速导致性能卡顿。对于持续效果如掉血应该使用计时器或标志位来控制频率例如每秒执行一次。坑13onTriggerXXXvsonCollisionXXX。务必分清onCollisionXXX用于非传感器碰撞体。物体会发生物理碰撞反弹、阻挡。onTriggerXXX用于传感器碰撞体。物体互相穿透仅用于触发事件。 用错了事件类型会导致回调永远无法被触发。4. Box2D专属深坑与应对策略如果你选择了Box2D那么下面这些坑需要额外警惕。4.1 刚体类型与碰撞事件的微妙关系前面提到两个静态刚体在Box2D中不产生碰撞事件。但这只是冰山一角。动态 vs 动态最常规的组合碰撞事件触发正常。动态 vs 运动学正常触发。运动学物体可以通过rigidBody.linearVelocity来移动它会推动动态物体。动态 vs 静态正常触发。运动学 vs 运动学在Box2D中两个运动学刚体之间也不会产生碰撞事件。如果你有两个都由代码控制移动的敌人希望它们互相阻挡需要至少一个是动态刚体或者使用额外的非物理碰撞检测。运动学 vs 静态正常触发。应对策略在设计关卡和角色时提前规划好刚体类型。对于需要相互碰撞但又不受物理力影响的环境物体比如移动平台可以考虑使用动态刚体但设置极大的质量mass或者锁定旋转模拟运动学效果。4.2 物理步长与帧率解耦带来的问题Box2D的物理世界更新PhysicsSystem2D.instance.step通常与渲染帧率解耦以保持物理模拟的稳定性。这可能导致事件触发时机与渲染帧不同步一次物理步长内可能发生多次碰撞分解但onCollisionStay只在渲染帧检查时报告一次状态。在极高速度或极低帧率下可能会错过短暂的碰撞事件比如子弹穿过薄墙。“抖动”问题如果物理更新频率如60Hz与渲染帧率如30Hz不匹配动态物体在渲染时插值的位置可能看起来有微小的抖动。应对策略调整物理步长可以在项目设置或代码中调整PhysicsSystem2D.instance.fixedTimeStep固定时间步长和maxSubSteps最大子步数。更小的fixedTimeStep和更多的maxSubSteps能提高模拟精度但消耗更多CPU。// 在初始化脚本中 PhysicsSystem2D.instance.fixedTimeStep 1 / 60; // 每秒60次物理更新 PhysicsSystem2D.instance.maxSubSteps 10; // 允许每帧最多补10个子步对于高速物体使用CCD对于子弹、飞镖等高速移动的物体启用连续碰撞检测CCD。const rigidBody node.getComponent(RigidBody2D); rigidBody.bullet true; // 启用CCD对性能有影响谨慎使用使用射线检测作为补充对于关键的高速碰撞检测不要完全依赖碰撞事件。可以在物体移动前朝移动方向发射一条射线PhysicsSystem2D.instance.raycast进行预检测。4.3 内存管理与刚体销毁Box2D对象刚体、形状是在C层分配的。在JavaScript层销毁一个带有刚体的节点时需要确保物理世界中的对应对象也被正确清理。坑14直接销毁节点可能导致的崩溃。如果物理世界还在引用这个刚体而你直接node.destroy()可能会在后续的物理步长中引发访问错误尤其是在Web平台。安全销毁流程destroyEntity() { const rigidBody this.node.getComponent(RigidBody2D); const collider this.node.getComponent(Collider2D); // 1. 首先禁用并清理物理组件 if (rigidBody) { rigidBody.enabled false; // 对于Box2D有时需要将刚体从世界中移除但Cocos封装后通常不需要手动调用 // 更常见的做法是确保组件先被禁用。 } if (collider) { collider.enabled false; // 移除事件监听如果之前已注册 collider.off(onCollisionEnter, this.onCollisionEnter, this); } // 2. 等待一帧确保物理系统已处理完禁用状态 this.scheduleOnce(() { // 3. 再销毁节点 this.node.destroy(); }, 0); }更简单的做法是在销毁前先将节点移出场景或设置为非激活状态让物理系统自然清理。5. 内置物理系统的“特性”与调试技巧内置物理系统虽然集成度高但也有自己的一套脾气。5.1 事件触发的一致性问题在我实测中内置物理系统在以下场景下事件触发可能不如Box2D稳定多物体快速连续碰撞例如一堆方块同时落下堆叠。可能会观察到onCollisionEnter和onCollisionExit事件配对不整齐或者onCollisionStay的触发帧率有波动。极小穿透下的碰撞当两个物体刚好擦边接触穿透深度极小时内置物理可能会因为浮点数精度或优化策略判定为“未碰撞”从而不触发事件。调试与应对开启调试绘制这是最重要的调试手段。在脚本中或项目设置里开启物理调试。// 在onLoad或start中 import { PhysicsSystem2D } from cc; PhysicsSystem2D.instance.debugDrawFlags // EPhysics2DDrawFlags.Aabb | // 绘制包围盒 // EPhysics2DDrawFlags.Pair | // 绘制粗检测对 // EPhysics2DDrawFlags.CenterOfMass | // 绘制质心 EPhysics2DDrawFlags.Shape | // 绘制碰撞形状最有用 EPhysics2DDrawFlags.Joint | // 绘制关节 EPhysics2DDrawFlags.Contact; // 绘制接触点 // 或者简化为全部绘制 // PhysicsSystem2D.instance.debugDrawFlags 0xffff;在场景中看到红色的碰撞形状线框就能直观判断碰撞体大小、位置是否正确是否真的发生了接触。增加碰撞体“皮肤”对于需要可靠检测的碰撞可以适当将碰撞体尺寸设置得比视觉精灵大一点点比如大1-2个像素创造一个微小的“缓冲区”避免因精度问题导致检测失败。日志与状态记录在关键的碰撞回调函数里详细打印日志包括时间戳、双方节点名、碰撞点等。通过日志分析事件序列是否合乎预期。5.2 性能与精度权衡内置物理系统可能在算法上做了一些取舍以提升性能。例如它的碰撞分解可能不如Box2D的GJK/EPA算法精确。这可能导致“粘墙”或“卡角”物体在复杂地形尤其是凹多边形或锐角移动时可能被轻微卡住。碰撞响应不够真实反弹角度、旋转力矩等可能感觉“有点软”或“有点怪”。应对策略简化碰撞形状尽量使用简单的碰撞体如矩形、圆形组合来近似复杂图形避免使用过于复杂的多边形碰撞体。多个简单形状的性能和稳定性通常优于一个复杂形状。调整物理材质通过PhysicsMaterial调整摩擦系数friction和恢复系数restitution即弹性。内置物理系统对这些参数可能更敏感多调试找到适合的手感。const material new PhysicsMaterial2D(); material.friction 0.2; // 摩擦系数 material.restitution 0.5; // 弹性系数 const collider this.getComponent(Collider2D); collider.material material;对于平台游戏可以考虑自己实现一部分角色控制逻辑如射线检测地面而不是完全依赖物理引擎的碰撞和力这样控制感更直接。5.3 与TiledMap等瓦片地图的碰撞集成使用Tiled地图编辑器创建关卡并导入碰撞层是2D游戏的常见做法。这里也有坑。坑15瓦片碰撞体过大或位置不对。在Tiled中设置的碰撞形状导入Cocos Creator后其位置和大小是基于瓦片坐标系和锚点的需要仔细调整。操作流程与避坑在Tiled中为图层添加cc.TiledObjectGroup类型并绘制碰撞矩形/多边形。在Cocos Creator中使用TiledMap组件加载.tmx文件。编写脚本在运行时解析碰撞层并生成对应的碰撞体。import { _decorator, Component, TiledMap, TiledObjectGroup, RigidBody2D, BoxCollider2D, Vec2 } from cc; const { ccclass, property } _decorator; ccclass(TileMapCollisionManager) export class TileMapCollisionManager extends Component { property(TiledMap) tiledMap: TiledMap | null null; start() { if (!this.tiledMap) return; // 假设你的碰撞层叫 collision-layer const objectGroup this.tiledMap.getObjectGroup(collision-layer); if (!objectGroup) return; const objects objectGroup.getObjects(); for (const obj of objects) { // obj 包含 x, y, width, height, polygon 等信息 if (obj.width obj.height) { // 矩形碰撞 const node new cc.Node(TileCollider); node.parent this.node; // 挂到地图节点下 node.setPosition(obj.x obj.width / 2, - (obj.y obj.height / 2)); // 注意坐标系转换Tiled的Y轴向下Cocos的Y轴向上。 const rigidBody node.addComponent(RigidBody2D); rigidBody.type RigidBody2D.Type.Static; // 地图碰撞体通常是静态的 const collider node.addComponent(BoxCollider2D); collider.size new Vec2(obj.width, obj.height); // 注意这里没有考虑Tiled地图的全局缩放和瓦片大小偏移可能需要额外计算。 } // 处理多边形碰撞... } } }关键点坐标转换。Tiled地图的坐标系原点在左上角Y轴向下。Cocos Creator世界坐标系原点在中心Y轴向上。obj.x和obj.y是Tiled坐标系中的位置需要根据地图的锚点、瓦片尺寸、图层偏移等进行转换。一个常见的错误是直接使用obj.x, obj.y导致生成的碰撞体位置完全不对。务必在生成后开启物理调试绘制检查碰撞框是否与视觉瓦片对齐。6. 常见问题排查与实战技巧实录当碰撞监听不按预期工作时别慌按照以下步骤排查大部分问题都能定位。6.1 排查清单从零开始检查物理系统启用了吗检查Project Settings - Physics 2D确保enabled是勾选的。这是最根本的一步。刚体启用了吗检查节点上的RigidBody2D组件enabled属性是否为true类型是否正确Dynamic/Kinematic/Static碰撞体启用了吗检查Collider2D组件enabled属性是否为true尺寸和偏移设置是否正确开启调试绘制确认红色线框是否出现在你期望的位置和大小。分组掩码匹配吗仔细计算双方的group和mask。用console.log打印出来看看按位与的结果是否为0。记住是双向检测。事件监听注册了吗确认在onLoad或start中已经通过collider.on(onCollisionEnter, ...)注册了回调函数。检查函数名是否拼写正确。回调函数被调用了吗在回调函数里第一行就加console.log看看事件是否触发。如果不触发回到前面步骤检查。如果触发继续检查你的业务逻辑。是传感器吗如果用了传感器sensortrue你监听的是onTriggerEnter而不是onCollisionEnter反之亦然。节点缩放了吗碰撞体的size是本地尺寸最终大小受节点scale影响。一个size为(50,50)的碰撞体如果节点scale是(2,2)实际是(100,100)。检查节点及其父节点的缩放值。刚体速度或力过大过高的速度可能导致物体一帧就穿过了另一个碰撞体错过碰撞检测。考虑启用CCDrigidBody.bullet true或使用射线检测预判。销毁逻辑有问题吗碰撞发生后如果立即销毁了其中一个节点可能导致另一个节点的onCollisionExit事件无法被触发或者触发时访问已销毁的节点导致报错。确保销毁前事件监听已移除并且逻辑健壮。6.2 实战技巧与心得为碰撞体命名在回调函数里通过otherCollider.node.name来判断碰撞对象很容易因为节点改名而出错。更好的做法是使用**标签Tag**或自定义组件。// 方法A使用标签需要先在项目设置中定义标签 // otherCollider.node.tag Tag.Player // 方法B检查是否有特定组件 const enemy otherCollider.getComponent(EnemyController); if (enemy) { // 处理与敌人的碰撞 } const item otherCollider.getComponent(ItemController); if (item) { // 处理与物品的碰撞 }使用碰撞矩阵简化管理在Project Settings - Physics 2D - Collision Matrix中可以可视化地配置哪些分组之间可以碰撞。这里配置的是默认的碰撞关系。代码中设置的mask会覆盖这里的全局设置。建议先在这里规划好大框架然后在特殊物体上用代码微调mask。“一次性”碰撞处理有时候我们只关心碰撞的第一次接触比如吃到金币后金币消失。可以在onCollisionEnter中处理逻辑并立即禁用或销毁自身的碰撞体防止同一碰撞在同一帧或下一帧再次触发事件。onCollisionEnter(selfCollider, otherCollider) { if (otherCollider.group PhysicsGroup.ITEM) { this.pickUpItem(); // 拾取逻辑 // 方法1禁用碰撞体防止再次触发 selfCollider.enabled false; // 方法2如果物品需要立即消失 // otherCollider.node.destroy(); // 注意销毁对方节点要小心确保对方没有后续逻辑要执行。 } }处理穿透和卡顿如果物体偶尔被卡住除了检查形状和物理材质可以尝试稍微增加物理步长fixedTimeStep如从1/60调到1/50给求解器更多时间。对于动态角色可以适当增加rigidBody.linearDamping线性阻尼来平滑运动。避免将碰撞体放置得过于紧密留出微小的空隙。测试要全面不要只在编辑器的静止状态下测试。用各种速度、各种角度去撞击你的碰撞体。特别是快速移动和旋转下的碰撞最容易暴露问题。构建到真机尤其是移动设备上测试因为性能差异可能导致物理模拟表现不同。最后我的个人体会是2D物理碰撞没有银弹。Box2D强大但稍显笨重内置物理轻便但需要更多调试。选定一套系统后深入理解它的规则通过调试绘制和详细日志把它“摸透”比在两者之间摇摆更有效率。大部分令人头疼的碰撞问题归根结底都是配置错误、理解偏差或精度问题。耐心地、系统地按照排查清单走一遍结合调试视图你总能找到那个隐藏的坑。