9.7 KiB
事件系统
**本文档引用文件** - [GameEvent.ts](file://assets/script/game/common/config/GameEvent.ts) - [event.md](file://doc/core/common/event.md) - [MessageManager.ts](file://extensions/oops-plugin-framework/assets/core/common/event/MessageManager.ts) - [SingletonModuleComp.ts](file://assets/script/game/common/SingletonModuleComp.ts) - [TopComp.ts](file://assets/script/game/map/TopComp.ts) - [MInfoComp.ts](file://assets/script/game/map/MInfoComp.ts) - [move.ts](file://assets/script/game/map/move.ts) - [TalComp.ts](file://assets/script/game/hero/TalComp.ts)目录
事件常量分类与业务含义
基于 GameEvent.ts 枚举文件,游戏全局事件按功能模块可分为以下几类:
战斗流程事件
MissionStart:关卡开始时触发,初始化战斗环境MissionWin:玩家胜利时触发,进入结算流程MissionLoss:玩家失败时触发,显示失败界面FightStart:战斗正式开始,激活战斗逻辑FightEnd:战斗结束,无论胜负均触发NewWave:新一波敌人出现,用于刷新怪物生成
英雄相关事件
HeroLvUp:英雄升级时触发,用于属性更新和UI反馈HeroUnlock:新英雄解锁时触发,通知UI展示获取动画HeroDead:英雄死亡时触发,处理死亡逻辑和成就判断HeroSelect:选择出战英雄时触发,更新队伍配置HeroSkillSelect:技能选择阶段触发,激活技能选择界面
资源更新事件
GOLD_UPDATE:金币数量变化时触发,用于UI金币显示更新DIAMOND_UPDATE:钻石数量变化时触发MEAT_UPDATE:游戏内特定资源"肉"的数量更新MISSION_UPDATE:关卡进度更新,用于顶部UI显示当前关卡
界面与交互事件
ShopOpen:商店界面打开时触发HerosOpen:英雄列表界面打开RestOpen:休息界面打开HeroInfoOpen:英雄详情界面打开GuideStart、GuideEnd:新手引导流程控制
地图移动事件
MAP_MOVE_END_LEFT:地图向左移动到达边界MAP_MOVE_END_RIGHT:地图向右移动到达边界CardsClose:卡牌选择界面关闭
卡牌与技能事件
CardRefresh:卡牌刷新时触发UseHeroCard:使用英雄卡牌UseSkillCard:使用技能卡牌UseTalentCard:使用天赋卡牌CastSkill:技能施放
Section sources
事件监听机制与内存管理
持续监听与单次监听的差异
oops.message.on() 用于注册持续监听的事件,监听器会一直存在直到显式移除。适用于需要长期响应的事件,如资源更新、状态变化等。
oops.message.on(GameEvent.GOLD_UPDATE, this.onGoldUpdate, this);
oops.message.once() 用于注册只触发一次的事件监听,事件响应后监听器自动移除。适用于一次性流程,如初始化完成、首次加载等场景。
oops.message.once(GameEvent.GameServerConnected, this.onHandler, this);
从 MessageManager.ts 的实现可以看出,once() 方法通过创建一个包装函数 _listener,在事件触发后立即调用 off() 移除自身,确保只执行一次。
内存泄漏防范措施
为防止内存泄漏,必须在对象销毁时移除所有事件监听。通常在 onDestroy() 或 reset() 生命周期方法中执行:
protected onDestroy() {
oops.message.off(GameEvent.GOLD_UPDATE, this.onGoldUpdate, this);
}
MessageManager 在注册事件时会检查重复注册,并发出警告,避免同一对象对同一事件的重复监听。
Section sources
事件系统解耦设计与应用示例
解耦模块间通信
事件系统通过发布-订阅模式实现模块间的松耦合通信。发送方无需知道接收方的存在,接收方也无需主动轮询状态变化。
英雄升级事件(HeroLvUp)的完整流程
- 事件触发:当英雄经验值满足升级条件时,触发
HeroLvUp事件 - UI更新:UI组件监听
HeroLvUp事件,更新英雄等级显示和属性面板 - 成就判断:成就系统监听该事件,判断是否达成"快速升级"等成就条件
- 天赋系统响应:某些天赋(如"每升5级攻击力+10%")在等级变化时触发效果
金币更新事件的实际应用
在 SingletonModuleComp.ts 中,当金币数量变化时触发 GOLD_UPDATE 事件:
updateGold(gold: number) {
this.vmdata.gold += gold;
// ... 更新云端数据
oops.message.dispatchEvent(GameEvent.GOLD_UPDATE);
}
在 TopComp.ts 中,顶部UI组件监听该事件并执行视觉反馈:
onGoldUpdate(event: string, data: any) {
tween(this.node.getChildByName("bar").getChildByName("gold").getChildByName("num").getComponent(Label).node)
.to(0.1, { scale: v3(1.2, 1.2, 1) })
.to(0.1, { scale: v3(1, 1, 1) })
.start();
}
这种设计使得金币逻辑与UI展示完全分离,任何模块都可以独立修改而不影响其他部分。
地图移动事件的循环机制
move.ts 文件展示了 MAP_MOVE_END_LEFT 和 MAP_MOVE_END_RIGHT 事件的闭环设计:
- 地图移动组件监听边界到达事件
- 当到达边界时,重置位置并派发对应的边界事件
- 其他组件可以监听这些事件执行相应逻辑
flowchart TD
A[地图移动] --> B{到达右边界?}
B --> |是| C[重置到左边界]
C --> D[派发MAP_MOVE_END_LEFT]
D --> E[其他组件响应]
B --> |否| F{到达左边界?}
F --> |是| G[重置到右边界]
G --> H[派发MAP_MOVE_END_RIGHT]
H --> I[其他组件响应]
Diagram sources
Section sources
事件命名规范与作用域管理
命名规范
事件名称采用大写常量格式,使用有意义的描述性名称。遵循以下原则:
- 使用名词或名词短语,如
GOLD_UPDATE、HeroLvUp - 模块相关事件添加前缀,如
MAP_MOVE_END_LEFT - 业务流程事件使用动词,如
MissionWin、FightStart - 避免缩写,确保名称清晰可读
作用域管理
事件系统通过第三个参数 this 管理作用域,确保回调函数在正确的上下文中执行。这解决了 JavaScript/TypeScript 中常见的 this 上下文丢失问题。
在注册事件时,必须传入正确的对象引用,以便在移除监听时能够精确匹配。
oops.message.on(GameEvent.GOLD_UPDATE, this.onGoldUpdate, this);
这里的 this 确保了:
- 回调函数在正确的对象实例上下文中执行
- 移除监听时能够准确找到对应的监听器
- 避免不同实例间的监听器混淆
Section sources
事件调试与常见问题解决方案
调试技巧
事件监听器dump
可以通过访问 MessageManager 的内部 events 对象来查看当前所有注册的事件监听器,便于调试和性能分析。
事件触发跟踪
在开发环境中,可以临时修改 dispatchEvent 方法,添加日志输出,跟踪所有事件的触发情况。
flowchart LR
A[事件注册] --> B[事件派发]
B --> C{是否有监听器?}
C --> |是| D[执行所有监听器]
D --> E[回调函数执行]
C --> |否| F[无操作]
常见问题及解决方案
事件重复注册
问题:同一对象对同一事件多次注册,导致回调执行多次。
解决方案:
- 在
onLoad()中注册事件,在onDestroy()中移除 - 使用
MessageManager的重复注册警告功能及时发现 - 在复杂场景下,使用标志位确保只注册一次
this上下文丢失
问题:回调函数中的 this 指向错误的对象。
解决方案:
- 严格按照
oops.message.on(event, handler, this)格式注册 - 避免使用箭头函数作为回调,除非明确不需要绑定作用域
- 在 TypeScript 中利用类型检查确保第三个参数正确
内存泄漏
问题:对象销毁后事件监听器未移除,导致对象无法被垃圾回收。
解决方案:
- 严格遵守生命周期,在
onDestroy()中调用off() - 对于临时组件,使用
once()替代on() - 使用事件管理器批量管理事件的注册和移除
事件命名冲突
问题:不同模块使用相同名称的事件导致意外行为。
解决方案:
- 使用模块前缀,如
MAP_、HERO_ - 在
GameEvent枚举中统一管理所有事件名称 - 避免使用过于通用的名称
Section sources