ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ET 框架事件系统(EventSystem)全解析:从 ECS 数据驱动到组件系统与消息分发

ET 框架事件系统(EventSystem)全解析:从 ECS 数据驱动到组件系统与消息分发 ET 框架事件系统EventSystem全解析从 ECS 数据驱动到组件系统与消息分发【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ETETUnity3D Client And C# Server Framework是一个客户端与服务端共享 C# 代码的 ECS 框架其事件系统EventSystem是贯穿整个框架运行逻辑的“神经中枢”。本文以官方文档《EventSystem》为核心结合仓库源码系统讲解 ET 中 AwakeSystem、StartSystem、UpdateSystem、DestroySystem、ChangeSystem、DeserializeSystem、LoadSystem、普通事件与消息事件的定义、触发时机与订阅方式并深入源码揭示事件注册与分发的底层原理。读完本文你将能够熟练使用 ET 的各类事件解耦业务模块并理解框架“数据驱动逻辑”的设计哲学。一、为什么要事件系统ECS 的数据与逻辑分离ET 框架基于 ECSEntity-Component-System思想设计其最重要的特性有二数据与逻辑分离实体Entity与组件Component只承载数据行为逻辑全部收敛到独立的 System 中数据驱动逻辑数据发生变化时通过事件通知各个关心的模块而不是由某个模块直接调用其他模块的方法。什么是“数据驱动逻辑”文档用了一个非常直观的 MOBA 游戏例子来说明一个 MOBA 游戏中英雄有血条血条既显示在角色头顶模型血条又显示在左上角头像 UI 上。此时服务器发送了一条扣血消息我们该如何处理第一种方式耦合在扣血消息的处理函数里既要修改英雄的血量数值又要修改角色头顶血条显示还要同时修改头像 UI 上的血条。消息处理模块直接依赖了 UI 模块和模型模块模块之间产生强耦合任何一方的改动都可能牵一发动全身。第二种方式解耦扣血消息处理函数中只修改血量数值然后抛出一个HpChange血量变化事件。头顶血条模块和头像 UI 模块各自订阅该事件在各自的订阅处理方法里只关心自己的逻辑。第二种方式下每个模块“各司其职、互不打扰”发消息的人不知道谁在听听的人不知道谁在说模块之间彻底解耦这正是 ET 事件系统存在的意义。ET 框架的运行逻辑正是由上述各类事件驱动的。二、事件系统的整体架构与事件分类在深入各类事件之前先建立整体认知。ET 的事件系统由两个核心单例协作完成EventSystem源码见 EventSystem.cs负责普通事件AEvent与 Invoke 回调的注册、发布与分发EntitySystemSingleton源码见 EntitySystemSingleton.cs负责所有带[EntitySystem]标记的组件系统Awake/Destroy/Deserialize/Serialize 等的注册与触发。两者都带有[CodeProcess]特性在框架启动时由代码类型收集器CodeTypes扫描程序集中所有打了对应特性标记的类并通过反射Activator.CreateInstance实例化、按类型登记到字典中。也就是说开发者只需要按约定写好类并打好特性框架会自动完成注册无需手工维护任何注册表。ET 提供的事件种类非常丰富并且每一种都可以被多个订阅者同时订阅一对多广播。下面逐类讲解。三、组件生命周期事件EntitySystem第一类事件是绑定在 Entity 组件上的生命周期系统。它们都以XXXSystemT抽象基类的形式存在泛型参数T是组件的具体类型开发者继承基类并重写对应方法即可完成订阅。3.1 AwakeSystem组件创建时触发仅一次触发时机组件工厂创建组件后立刻抛出且只抛出一次可携带参数。在旧版本中通过ComponentFactory.CreatePlayer()创建在当前仓库中则统一收敛到Entity.AddComponentT()系列方法。Player player ComponentFactory.CreatePlayer(); // 订阅 Player 的 Awake 事件 public class PlayerAwakeSystem : AwakeSystemPlayer { public override void Awake(Player self) { // 组件创建后的初始化逻辑 } }从源码看AwakeSystemT的定义位于 IAwakeSystem.cs其泛型约束为where T : Entity, IAwake即只有实现了IAwake标记接口的组件才能挂接 Awake 系统。更重要的是框架提供了0 到 4 个参数的重载AwakeSystemT、AwakeSystemT, A、AwakeSystemT, A, B、AwakeSystemT, A, B, C、AwakeSystemT, A, B, C, D对应的实体方法为AddComponentK()AddComponentK, P1(P1 p1)AddComponentK, P1, P2(P1 p1, P2 p2)AddComponentK, P1, P2, P3(P1 p1, P2 p2, P3 p3)以AddComponentWithIdK, P1为例源码见 Entity.cs其内部实现为public K AddComponentWithIdK, P1(long id, P1 p1, bool isFromPool false) where K : Entity, IAwakeP1, new() { Entity component this.CreateComponent(typeof(K), id, isFromPool); EntitySystemSingleton.Instance.Awake(component, p1); // 创建完成后立即触发 Awake return component as K; }可见AddComponent与AwakeSystem是一对“创建即回调”的组合参数会在创建时透传进 Awake 方法非常适合做带初始数据的组件初始化。3.2 StartSystem组件第一次 Update 前触发触发时机在组件的 UpdateSystem 第一次被调用之前抛出。// 订阅 Player 的 Start 事件 public class PlayerStartSystem : StartSystemPlayer { public override void Start(Player self) { // 首次进入每帧逻辑前的准备 } }Start 与 Awake 的区别在于Awake 是“创建即调用”Start 则是“首次进入游戏循环时调用”用于延迟初始化那些依赖运行时环境的逻辑。3.3 UpdateSystem每帧触发触发时机组件存活期间每帧抛出用于承载持续的每帧逻辑。// 订阅 Player 的 Update 事件 public class PlayerUpdateSystem : UpdateSystemPlayer { public override void Update(Player self) { // 每帧执行的逻辑 } }与之配套的还有LateUpdateSystem渲染帧后期执行等均定义在 IUpdateSystem.cs、ILateUpdateSystem.cs 中。所有需要逐帧驱动逻辑的组件如移动、状态机、技能冷却都通过此类系统挂载避免把逻辑写进组件数据类里。3.4 DestroySystem组件销毁时触发触发时机组件被删除Dispose时抛出用于释放资源、注销回调、解绑监听等收尾工作。// 订阅 Player 的 Destroy 事件 public class PlayerDestroySystem : DestroySystemPlayer { public override void Destroy(Player self) { // 释放逻辑 } } Player player ComponentFactory.CreatePlayer(); player.Dispose(); // 此处会触发 Destroy 事件DestroySystemT的定义在 IDestroySystem.cs泛型约束为where T : Entity, IDestroy。从 Entity.cs 的Dispose实现可以看到销毁链路为// 触发 Destroy 事件 if (this is IDestroy) { EntitySystemSingleton.Instance.Destroy(this); }即组件在真正释放前会先检查其是否实现了IDestroy标记接口若实现则回调所有订阅的DestroySystem。3.5 ChangeSystem组件内容变化时触发手动触发时机当组件内容发生变化时抛出必须由开发者手动触发。框架无法自动感知数据变化因此要求开发者在修改关键数据后显式调用。// 订阅 Player 的 Change 事件 public class PlayerChangeSystem : ChangeSystemPlayer { public override void Change(Player self) { // 数据变更后的联动逻辑 } } Player player ComponentFactory.CreatePlayer(); Game.EventSystem.Change(player); // 需要手动触发 ChangeSystemChangeSystem 非常适合“数据变更后同步外部表现”的场景例如数值属性变化后刷新 UI、血量变化后更新血条等。3.6 DeserializeSystem组件反序列化后触发触发时机组件从二进制流BSON 等反序列化完成后抛出。// 订阅 Player 的 Deserialize 事件 public class PlayerDeserializeSystem : DeserializeSystemPlayer { public override void Deserialize(Player self) { // 反序列化后的修复与重建逻辑 } } // 这里 player2 会触发 Deserialize 事件 Player player2 MongoHelper.FromBsonPlayer(player.ToBson());ET 使用 MongoDB 的 BSON 格式做实体序列化官方文档参见《3.2 强大的 MongoBson 库》。反序列化后对象内部状态可能缺失或失效如引用关系、运行时缓存DeserializeSystem 正是为此提供的恢复钩子。源码见 Entity.cs反序列化完成后若组件未标记IsNoDeserializeSystem则调用EntitySystemSingleton.Instance.Deserialize(this)其内部遍历执行所有IDeserializeSystem。3.7 LoadSystem重载 DLL 时触发服务端热更新触发时机当 EventSystem 重新加载程序集dll时抛出专门用于服务端热更新场景。重载 dll 后需要做一些处理例如重新注册 Handler、重建静态引用等。// 订阅 Player 的 Load 事件 public class PlayerLoadSystem : LoadSystemPlayer { public override void Load(Player self) { // dll 重载后的处理逻辑 } }3.8 生命周期事件小结系统类型触发时机是否自动触发典型用途AwakeSystem组件创建后自动AddComponent初始化、注入参数StartSystem首次 Update 前自动延迟初始化UpdateSystem每帧自动每帧驱动逻辑LateUpdateSystem渲染后期每帧自动相机跟随等DestroySystem组件Dispose时自动资源释放、解绑ChangeSystem组件内容变化手动Change()数据变更联动DeserializeSystem反序列化完成后自动反序列化修复LoadSystemdll 重载时自动服务端热更新处理四、普通事件Event模块间解耦的利器第二类事件是普通事件由开发者自己抛出最多可携带三个参数。它是模块间解耦最常用的手段事件抛出方完全不关心谁在订阅订阅方也互不知晓。此外客户端热更层Hotfix可以订阅 Mono 层的事件这为框架层与业务层协作提供了通道。4.1 抛发与订阅示例继续用开篇的血量例子int oldhp 10; int newhp 5; // 抛发 hp 变化事件 Game.EventSystem.Run(HpChange, oldhp, newhp); // UI 模块订阅血量变化事件 [Event(HpChange)] public class HpChange_ShowUI : AEventint, int { public override void Run(int a, int b) { // 更新左上角头像血条 } } // 模型头顶血条模块也订阅血量变化事件 [Event(HpChange)] public class HpChange_ModelHeadChange : AEventint, int { public override void Run(int a, int b) { // 更新角色头顶血条 } }关键点说明用[Event(HpChange)]特性声明事件名继承AEventint, int泛型参数对应事件携带的参数类型最多三个重写Run方法编写处理逻辑同一个事件可以有任意多个订阅者全部会依次收到通知抛发者只需要一行Game.EventSystem.Run(HpChange, oldhp, newhp)无需知道任何订阅者的存在。注文档示例中的Game.EventSystem.Run(HpChange, ...)是 ET 早期版本按字符串事件名分发的 API 写法用于帮助理解“抛出-订阅”模型。在当前仓库的 ET9 版本中事件发布已演进为强类型的PublishS, T(S scene, T a)泛型接口事件参数为struct类型订阅方继承AEventS, T并实现Handle方法以换取编译期类型安全和更好的性能。4.2 源码中的 Publish 实现场景类型过滤当前版本的发布核心实现在 EventSystem.cspublic void PublishS, T(S scene, T a) where S : class, IScene where T : struct { ListEventInfo iEvents; if (!this.allEvents.TryGetValue(typeof(T), out iEvents)) { return; // 无人订阅则直接返回抛出方不受影响 } int sceneType scene.SceneType; foreach (EventInfo eventInfo in iEvents) { if (!SceneTypeSingleton.IsSame(sceneType, eventInfo.SceneType)) { continue; // 按场景类型过滤只分发到匹配场景的订阅者 } if (!(eventInfo.IEvent is AEventS, T aEvent)) { Log.Error($event error: {eventInfo.IEvent.GetType().FullName}); continue; } aEvent.Handle(scene, a).Coroutine(); } }该实现透露了两个重要设计订阅者带场景类型SceneTypeEventAttribute可以声明订阅者适用的场景发布时通过SceneTypeSingleton.IsSame过滤避免事件被错误地分发到无关场景事件可以被“空发”如果没有任何人订阅Publish会直接返回抛出方不会有任何异常——这正是事件机制“解耦”的安全边界。另有一个异步版本PublishAsyncS, TEventSystem.cs它收集所有订阅者的ETTask并await ETTask.WaitAll等待全部完成后返回适合需要确保所有监听者处理完毕的场景。4.3 事件注册的底层逻辑订阅者是如何被框架发现的在EventSystem.Awake()EventSystem.cs中foreach (Type type in codeTypes.GetTypes(typeof(EventAttribute))) { IEvent obj Activator.CreateInstance(type) as IEvent; if (obj null) { throw new Exception($type not is AEvent: {type.Name}); } object[] attrs type.GetCustomAttributes(typeof(EventAttribute), false); foreach (object attr in attrs) { EventAttribute eventAttribute attr as EventAttribute; Type eventType obj.Type; EventInfo eventInfo new(obj, eventAttribute.SceneType); if (!this.allEvents.ContainsKey(eventType)) { this.allEvents.Add(eventType, new ListEventInfo()); } this.allEvents[eventType].Add(eventInfo); } }即框架启动时扫描所有打了[Event]特性的类型并实例化按事件参数类型obj.Type建立“事件类型 → 订阅者列表”的映射字典allEvents。同一事件类型对应一个订阅者列表这就是多订阅广播的数据基础。五、消息事件MessageHandler服务端消息分发第三类事件是消息事件用于服务端网络消息处理。消息事件使用MessageHandler声明并且可以携带参数来指定消息由哪一类服务器处理。[MessageHandler(AppType.Gate)] public class C2G_LoginGateHandler : AMRpcHandlerC2G_LoginGate, G2C_LoginGate { protected override void Run(Session session, C2G_LoginGate message, ActionG2C_LoginGate reply) { G2C_LoginGate response new G2C_LoginGate(); reply(reply); } }上例中[MessageHandler(AppType.Gate)]表明该处理器订阅在Gate 网关服务器上AMRpcHandlerC2G_LoginGate, G2C_LoginGate表明它处理的是“客户端 → 网关”的 RPC 请求C2G_LoginGate并回包G2C_LoginGateRun方法中的reply委托用于向客户端回发响应。消息事件是 ET 服务器架构中客户端请求进入业务逻辑的入口更详细的说明会在讲解消息模块时展开。六、更多事件数值事件与自定义扩展数值事件ET 的数值模块Numeric同样基于事件机制实现属性变化通知例如属性攻击、防御、暴击改变时抛出数值变更事件驱动伤害计算、属性面板刷新等逻辑具体会在数值组件相关文档《5.6 数值组件设计》中详解……文档明确指出“更多事件待开发者自己开发”。ET 的事件机制是开放的框架层面注册了生命周期事件、普通事件、消息事件、数值事件等业务开发者完全可以依照同样的模式[EntitySystem]标记 继承抽象基类扩展自己的事件类型。6.1 补充Invoke函数式调用与 Publish事件广播的区别阅读 EventSystem.cs 的注释可以清晰看到 ET 对两类“分发机制”的刻意区分这对正确使用至关重要Invoke 类似函数必须有被调用方否则抛出异常调用者与被调用者属于同一模块如 MoveComponent 中的 Timer 计时器。之所以不用直接函数调用是因为某些场景不方便直接调用例如 Config 加载在客户端与服务端方式不同、TimerComponent 需要根据 Id 分发。Publish 是事件抛出去可以没人订阅调用者与被调用者属于两个模块如任务系统需要知道道具使用的信息则订阅道具使用事件。注意不要把 Invoke 当函数使用这样会降低代码可读性能用函数就不要用 Invoke。Invoke有严格的“调用必有响应”语义无订阅者抛异常适合框架内部的确定性分发Publish是“广播-订阅”语义无订阅者静默返回适合模块间解耦。二者配合TryInvoke无订阅者时返回默认值而非抛异常一起构成了 ET 完整的分发能力谱系。七、源码级原理一次完整的事件触发链路综合前文梳理一个带参数的组件从创建到销毁的完整事件链路注册阶段框架启动CodeTypes扫描所有打[EntitySystem]特性的类如PlayerAwakeSystemEntitySystemSingleton.Awake()将它们实例化并按“组件类型 → 系统类型列表”登记到TypeSystems中见 EntitySystemSingleton.cs创建阶段业务代码调用parent.AddComponentPlayer(p1)内部CreateComponent创建实例后调用EntitySystemSingleton.Instance.Awake(component, p1)框架查表取出Player的所有IAwakeSystemP1依次执行Run见 EntitySystemSingleton.cs运行阶段每帧驱动UpdateSystem数据变化时开发者手动Change()触发ChangeSystem销毁阶段player.Dispose()时框架检查IDestroy标记并调用EntitySystemSingleton.Instance.Destroy(this)执行所有DestroySystem见 Entity.cs。可以看到ET 将“数据Entity/Component、行为System、通知Event”三者彻底分离数据只负责存储行为以 System 形式挂载到数据上跨模块协作则通过事件完成。这也是整个 ET 框架“一切皆实体、逻辑事件驱动”运行方式的缩影。八、总结ECS 双支柱ET 事件系统实现了数据与逻辑分离并以数据驱动逻辑模块间通过事件通信彻底解耦生命周期系统Awake / Start / Update / LateUpdate / Destroy / Change / Deserialize / Load 八类系统覆盖组件从创建到销毁的完整生命周期其中 Change 需手动触发普通事件[Event]AEvent实现一对多广播最多携带三个参数是模块解耦的主力工具消息事件MessageHandler 服务器类型参数驱动服务端网络消息处理底层原理CodeTypes扫描 反射注册 字典分发开发者只需按约定继承基类并打特性即可自动接入事件总线能力边界Publish可空发解耦Invoke必有响应确定性分发二者语义不同需谨慎区分。掌握这套事件体系就等于掌握了 ET 框架的“逻辑编排方式”——无论是客户端 UI 与模型的联动、服务端模块间的协作还是客户端与服务端的消息往来最终都统一在这套事件驱动模型之下。相关源码与文档指引事件发布/调用核心EventSystem.cs组件系统注册与分发EntitySystemSingleton.csAwake 系列系统定义IAwakeSystem.csDestroy 系统定义IDestroySystem.csUpdate/LateUpdate 系统定义IUpdateSystem.cs、ILateUpdateSystem.cs组件创建与销毁触发点Entity.cs本文原始依据文档Book/3.4EventSystem.md【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表