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