Unity MyFramework 用法说明(九):使用 EventSystem 处理不确定的业务响应

上一篇介绍了如何使用 Command 封装一段明确、完整的业务逻辑。

当调用方明确知道一次操作必须执行哪些逻辑时,应该直接调用 Command;但有些情况下,数据发生变化以后,发送方并不知道哪些模块需要响应,这时就适合使用 EventSystem。

项目地址:

github.com/ZHOURUIH/My...

一、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,才能在保持代码清晰的同时降低系统之间的直接依赖。

相关推荐
SmalBox3 小时前
【节点】[EvaluateSimulationCaustics节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox20 小时前
【节点】[EvaluateSimulationAdditionalData节点]原理解析与实际应用
unity3d·游戏开发·图形学
_zhourui_h_21 小时前
Unity MyFramework 用法说明(八):逻辑就是对象,使用 Command 封装跨系统业务
unity3d
qiqizizzz2 天前
Unity编译提速#05 | 认识 Assembly Definition:给代码划定边界
unity3d
_zhourui_h_2 天前
Unity MyFramework 用法说明(七):使用 RedPointSystem 构建树形红点
unity3d
SmalBox2 天前
【节点】[EvaluateScatteringColor节点]原理解析与实际应用
unity3d·游戏开发·图形学
SmalBox3 天前
【节点】[EvaluateRefractionData节点]原理解析与实际应用
unity3d·游戏开发·图形学