Unity MyFramework 用法说明(九):使用 EventSystem 处理不确定的业务响应 📅 2026/7/18 18:54:33 上一篇介绍了如何使用 Command 封装一段明确、完整的业务逻辑。当调用方明确知道一次操作必须执行哪些逻辑时应该直接调用 Command但有些情况下数据发生变化以后发送方并不知道哪些模块需要响应这时就适合使用 EventSystem。项目地址https://github.com/ZHOURUIH/MyFramework一、EventSystem 解决什么问题以背包物品变化为例物品数量改变以后可能需要刷新背包界面 主界面的快捷栏 商店中的货币数量 装备强化界面的材料数量 任务提交界面负责修改背包数据的逻辑不应该依赖这些界面mUIBackpack?.updatePack(); mUIMain?.updateQuickItems(); mUIEquipStrengthen?.refreshMaterial(); mUINPCShop?.refreshItemMoney();因为这些界面可能没有打开也可能在以后继续增加。修改背包的逻辑只需要完成自己的核心操作然后通知外部mEventSystem.pushEventEventPackItemChanged();哪些界面需要刷新由界面自己决定。二、定义一个无参数事件创建事件类public class EventPackItemChanged : GameEvent { }这个事件不需要携带参数它只表示一件事背包中的物品已经发生变化监听者收到事件以后重新读取最新的背包数据即可。Event 的名字应该描述已经发生的事实而不是要求监听者执行某个操作。推荐EventPackItemChanged EventBuffChanged EventUnreadMailChanged EventEquipPropertyChanged不推荐EventRefreshBackpack EventOpenMail EventUpdateMainUI“刷新背包”是某个界面的处理方式而“背包物品发生变化”才是业务事实。三、在界面中监听事件背包界面可以在onGameState()中监听public override void onGameState() { base.onGameState(); mEventSystem.listenEventEventPackItemChanged( onPackItemChanged, this); updatePack(); } protected void onPackItemChanged(EventPackItemChanged param) { updatePack(); }也可以使用 Lambda 简化public override void onGameState() { base.onGameState(); mEventSystem.listenEvent( (EventPackItemChanged param) { updatePack(); }, this); updatePack(); }listenEvent的最后一个参数是监听者thisEventSystem 会记录这个监听者注册过的所有事件之后可以根据监听者统一注销。四、为什么在 onGameState 中注册LayoutScript.onGameState()会在界面每次显示时调用。而界面隐藏时LayoutScript.onHide()会自动执行mEventSystem?.unlistenEvent(this);因此界面的事件监听通常按照下面的生命周期处理界面显示 ↓ onGameState 注册事件 ↓ 界面接收事件并刷新 ↓ 界面隐藏 ↓ LayoutScript 自动注销监听不需要每个界面都重复编写注销代码也不会出现隐藏界面继续接收事件的问题。界面再次显示时onGameState()会重新注册。五、发送无参数事件背包数据修改完成以后发送事件public class CmdCharacterAddItem { public static void execute( CharacterMyself character, ObjectItem item) { character.getComPack().addItem(item); mEventSystem.pushEventEventPackItemChanged(); } }这里的职责划分是CmdCharacterAddItem 负责必须执行的核心逻辑 EventPackItemChanged 通知不确定的外部监听者Command 不需要知道当前打开了哪些界面。界面也不需要知道物品是通过拾取、购买、任务奖励还是邮件附件获得的。六、一个事件可以有多个监听者主界面也可以监听同一个事件public override void onGameState() { base.onGameState(); mEventSystem.listenEventEventPackItemChanged( onPackItemChanged, this); } protected void onPackItemChanged(EventPackItemChanged param) { updateQuickItems(); }强化界面同样可以监听public override void onGameState() { base.onGameState(); mEventSystem.listenEventEventPackItemChanged( onPackItemChanged, this); } protected void onPackItemChanged(EventPackItemChanged param) { refreshMaterialCount(); }发送方始终只有一行mEventSystem.pushEventEventPackItemChanged();新增或删除监听者时不需要修改发送事件的代码。七、定义带参数的事件有些事件只通知“发生了变化”还不够监听者还需要知道具体数据。例如角色位置变化using UnityEngine; public class EventLocationChanged : GameEvent { public Vector2Int mLastTilePosition; public Vector2Int mTilePosition; public override void resetProperty() { base.resetProperty(); mLastTilePosition Vector2Int.zero; mTilePosition Vector2Int.zero; } }事件继承自GameEvent而GameEvent又属于框架对象池体系。所以事件中的字段必须在resetProperty()中重置避免对象再次从池中取出时残留上一次的数据。八、发送带参数的事件使用ClassScope创建事件参数using var a new ClassScopeEventLocationChanged( out var param); param.mLastTilePosition lastTilePosition; param.mTilePosition newTilePosition; mEventSystem.pushEvent(param);ClassScope离开作用域以后会自动回收事件对象。不需要手动创建和销毁new EventLocationChanged();也不应该保存事件参数对象供事件分发结束以后继续使用。监听者需要的数据应当在回调执行期间读取。九、接收带参数的事件主界面可以监听位置变化public override void onGameState() { base.onGameState(); mEventSystem.listenEventEventLocationChanged( onLocationChanged, this); } protected void onLocationChanged( EventLocationChanged param) { mLocationText.setText( param.mTilePosition.x , param.mTilePosition.y); }监听回调能够直接获得事件参数不需要再到角色系统中查询一次坐标。是否传递参数可以根据事件的含义决定只关心数据已经变化 使用无参数事件 需要知道变化前后的数据 使用带参数事件不要把整个系统对象都塞进事件中。只传递监听者真正需要的信息。十、普通对象如何注销事件并不是所有监听者都是LayoutScript。普通对象监听事件后需要在自己的生命周期结束时注销public class PackChangeWatcher : ClassObject { public void init() { mEventSystem.listenEventEventPackItemChanged( onPackItemChanged, this); } public override void destroy() { mEventSystem?.unlistenEvent(this); base.destroy(); } protected void onPackItemChanged( EventPackItemChanged param) { } }注销监听者的全部事件mEventSystem.unlistenEvent(this);只注销指定类型mEventSystem.unlistenEventEventPackItemChanged(this);对象生命周期结束后仍然保留监听是事件系统中最常见的问题之一。十一、监听指定角色的事件MyFramework 还支持只监听某个角色的事件mEventSystem.listenEventEventBuffChanged( characterID, onBuffChanged, this);发送时指定角色 IDmEventSystem.pushEvent(param, characterID);这样可以用于指定角色的 Buff 变化 指定角色的状态变化 指定角色的属性变化 指定角色的受击事件当角色销毁时可以统一移除这个角色相关的监听mEventSystem.removeCharacterEvent(characterID);需要注意发送指定角色事件时框架也会先分发给该事件的全局监听者再分发给指定角色的监听者。十二、EventSystem 和 Command 如何配合Command 负责明确且必须执行的逻辑public class CmdCharacterSetLevel { public static void execute( CharacterPlayer character, int level, long levelExp) { CharacterPlayerData data character.getData(); data.mLevel level; data.mLevelExp levelExp; if (character is CharacterMyself) { using var a new ClassScopeEventLevel( out var param); param.mLevel level; mEventSystem.pushEvent(param); } mUIMain.safe()?.setLevel(level); } }其中修改角色等级数据 属于命令必须执行的逻辑 通知其他动态模块等级已经变化 适合通过事件处理两者不是二选一而是分别解决不同的问题。十三、不要滥用事件EventSystem 可以降低发布者对监听者的依赖但也会隐藏调用关系。下面这些核心逻辑不应该完全依赖事件扣除购买货币 增加购买物品 修改角色血量 执行角色死亡 保存关键业务数据因为它们是一次操作必须完成的步骤应该直接写在 Command 中。EventSystem 更适合界面刷新 红点更新 可选的业务响应 动态增加的监听逻辑 发布者不应该依赖的外部模块判断标准不是“涉及多个系统就发事件”而是发送方是否明确知道并必须保证这些逻辑被执行。十四、总结MyFramework 的 EventSystem 主要用法是1. 创建 GameEvent 子类 2. 在 onGameState 或对象初始化时监听 3. 将监听者 this 传给 EventSystem 4. 使用 pushEvent 发送事件 5. 带参数事件通过 ClassScope 创建 6. 在 resetProperty 中重置所有字段 7. 对象结束生命周期时注销监听Command 表示“现在明确要执行哪些逻辑”。Event 表示“某件事情已经发生可能有人需要知道”。把必须执行的逻辑放进 Command把不确定、可扩展的响应交给 EventSystem才能在保持代码清晰的同时降低系统之间的直接依赖。