ARTICLE DETAIL

资讯详情

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

ET 框架“一切皆实体”设计解析:树状实体架构与组件生命周期

ET 框架“一切皆实体”设计解析:树状实体架构与组件生命周期 ET 框架“一切皆实体”设计解析树状实体架构与组件生命周期【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ETETUnity3D Client And C# Server Framework在吸收 ECS 思想后并未照搬传统 ECS而是提出了“一切皆实体Everything is Entity”的树状数据架构Entity 既是数据单元、又可作为组件挂载、还可拥有孩子节点。本文以 Book/3.3一切皆实体.md 为主线结合 cn.etetet.core 包中的实体源码完整讲解 ET 实体树的组织方式、组件创建与释放流程以及 InstanceId 在对象池与异步场景中的关键作用帮助你真正理解 ET 数据层的设计与使用。ECS 热潮之下ET 的取舍近几年 ECSEntity-Component-System设计非常流行主要得益于《守望先锋》的成功引爆了这项技术。守望先锋采用状态帧网络技术客户端会进行预测预测不准时需要回滚由于组件式的设计回滚时可以只回滚某些组件这正是组件化的巨大收益之一。ECS 最重要的设计是逻辑与数据的完全分离EC 是纯数据System 实际就是逻辑由数据驱动逻辑。所谓“数据驱动逻辑”很简单——通过 Update 检测数据变化通过事件机制订阅数据变化这就是数据驱动。但 ET 作者认为ECS 的其他卖点如缓存命中在编写逻辑上并不太重要现代游戏都用脚本连脚本的性能都能容忍怎么会在乎缓存命中那点性能提升因此 ET 在设计时吸收了 ECS 的思想但并不完全照搬最终形成了自己的特色。传统 ECS 的缺陷与 ET 的树状实体设计传统扁平式 ECS 的问题传统 ECS 写逻辑存在不少缺陷组件过度拆分为了复用数据必然要拆成非常小的颗粒导致组件数量爆炸协作成本高游戏是多人合作开发的每个人基本上只熟悉自己的模块扁平式结构最后可能造成组件大量冗余难以定位组件常见的 ECS 是扁平式的Entity 与 Component 只有一层。组件一多开发功能时不知道该使用哪些 Component。作者用一个非常形象的公司管理类比来解释这个问题最大的是老板老板手下带几百个人老板不可能认识所有的人完成一项任务老板没法挑出自己需要的人。合理的做法是老板手下有几个经理每个经理手下有几个主管每个主管管理几个工人形成树状的管理结构才容易管理。ET 的树状 Entity 结构这类似 ET 的做法Entity 可以管理 ComponentComponent 可以管理 Entity甚至 Component 还可以挂载 Component。例如人由头、身体、手、脚组成而头又由眼睛、耳朵、鼻子、嘴巴组成Head head human.AddComponentHead(); head.AddComponentEye(); head.AddComponentMouth(); head.AddComponentNose(); head.AddComponentEar(); human.AddComponentBody(); human.AddComponentHand(); human.AddComponentLeg();注原文档示例中的Mouse应指 Mouth嘴此处按语义更正AddComponent 的详细实现见后文源码分析。从源码看这套树状结构由 Entity.cs 中的两套挂载机制支撑组件挂载ComponentParent设置父节点并把自身放入父节点的Components集合同时把IsComponent置为 true孩子挂载Parent设置父节点并把自身放入父节点的Children集合IsComponent置为 false。两者的核心约束一致父节点不能为 null、不能是自己、父节点必须已经在数据树上value.IScene null会直接抛异常。这保证了整个实体树始终挂在某个 Scene 之下。一切皆 Entity数据放置的哲学ET 中所有数据都是 Entity包括 Entity 本身。Entity 既可以当成组件使用也可以当作其它 Entity 的孩子。这就引出数据放置的判断标准通用数据放在 Entity 身上作为成员不太通用的数据作为组件挂在 Entity 身上。比如物品的设计所有物品都有配置 Id、数量、等级的字段这些字段没有必要做成组件放在 Entity 身上使用会更加方便class Item : Entity { // 道具的配置Id public int ConfigId { get; set; } // 道具的数量 public int Count { get; set; } // 道具的等级 public int Level { get; set; } }这种设计使 ET 的数据呈现树状结构非常有层次能非常轻松地理解整个游戏的架构顶层是Game.Scene不同模块的数据都挂载在Game.Scene上面每个模块自身下面又可以挂载很多数据每开发一个新功能不用过多思考类该怎么设计、数据放在什么地方、挂载在这里会不会导致冗余。例如玩家需要做道具系统设计一个ItemsComponent挂在Player身上即可需要技能开发一个SpellComponent挂在Player身上全服需要做活动搞一个活动组件挂在Game.Scene上面。这种设计使任务分派变得很简单十分模块化。源码印证Scene 与 Fiber 的层级在 Scene.cs 中Scene继承自Entity并实现IScene接口持有Fiber纤程与SceneType。每个 Scene 构造时就会通过fiber.NewInstanceId()获得自己的 InstanceId 并注册进实体系统public Scene(Fiber fiber, long id, int sceneType, string name) { this.Id id; this.Name name; this.InstanceId fiber.NewInstanceId(); this.SceneType sceneType; this.IsNoDeserializeSystem true; this.Fiber fiber; this.IScene this; this.IsRegister true; }配合 EntityHelper.cs 提供的扩展方法Zone()、Scene()、Root()、Fiber()等任何挂在树上的 Entity 都能一路回溯到自己的 Scene 与 Fiber这正是“挂在树上”之后才能被事件系统驱动的前提。组件的一些细节1. 组件的创建原文档指出组件的创建不要自己去new应该统一使用ComponentFactory创建。ComponentFactory提供了三组方法Create、CreateWithParent、CreateWithId。Create是最简单的创建方式做了几个处理a. 根据组件类型构造一个组件b. 将组件加入事件系统并且抛出一个 AwakeSystemc. 决定是否启用对象池。CreateWithParent在Create的基础上提供了一个 Parent 对象设置到Component.Parent字段上。CreateWithId用来创建ComponentWithId或其子类在Create的基础上可以自己设置一个 Id。三类工厂方法都带有一个fromPool参数用于控制是否使用对象池。与当前源码的对应在 ET 最新源码中这套能力已整合进Entity的方法族见 Entity.csAddComponentT(bool isFromPool false)及带参版本AddComponentT, P1, P2...创建组件、设置 Id 与父节点ComponentParent、然后调用EntitySystemSingleton.Instance.Awake(component)触发 AwakeSystemAddComponentWithIdK(long id, ...)对应原CreateWithId语义显式指定组件 IdAddChildT(...)/AddChildWithIdT(...)创建孩子实体并触发 AwakeSystem。底层创建统一走ObjectPool.Fetch(type, isFromPool)见 Entity.cs 中的Create私有方法随后设置IsFromPool、IsNoDeserializeSystem标志并挂到父节点上。因此“组件不要自己 new统一走工厂创建”的原则在当前版本依然成立只是入口从ComponentFactory迁移到了Entity.AddComponent/AddChild系列方法。需要说明的是当前源码中AddComponent的isFromPool默认值为false与原文档所述“默认 true”略有差异以当前仓库实现为准。挂载时若父节点已有同类型组件Entity.CreateComponent会直接抛出异常entity already has component从源码层面杜绝了重复挂载。2. 组件的释放组件都继承了IDisposable接口。需要注意组件拥有非托管资源删除一个组件必须调用该接口。该接口做了如下操作a. 抛出 Destroy Systemb. 如果组件是使用对象池创建的在这里会放回对象池c. 从全局事件系统EventSystem中删除该组件并且将 InstanceId 设为 0。如果组件挂在 Entity 身上那么 Entity 调用Dispose时会自动调用身上所有 Component 的Dispose方法。源码印证Entity.cs 中的Entity.Dispose()完整流程为通过InstanceId 0判断是否已释放IsDisposed已释放则直接返回置IsRegister false、InstanceId 0递归清理children集合中所有孩子并逐个Dispose()递归清理components集合中所有组件并逐个Dispose()若自身实现了IDestroy接口触发EntitySystemSingleton.Instance.Destroy(this)抛出 Destroy System从父节点的Children或Components集合中移除自己保存IsFromPool标志、重置 status最后调用基类Dispose()。基类 DisposeObject.cs 的Dispose最终执行ObjectPool.Recycle(this)把对象放回对象池。也就是说释放一个父 Entity 会沿着实体树级联释放整棵子树这正是树状结构带来的“一键清理”能力。3. InstanceId 的作用任何组件都带有一个InstanceId字段这个字段会在组件构造、或组件从对象池取出的时候重新设置它标识这个组件的身份。在 Entity.cs 中InstanceId在设置IScene时若为 0 则通过iScene.Fiber.NewInstanceId()生成而 Fiber.cs 中NewInstanceId()是一个从 1 开始递增的长整型生成器保证了同一 Fiber 内 InstanceId 的全局唯一性。为什么需要这样一个字段主要有两个原因原因一对象池下的异步安全对象池的存在意味着组件未必会被真正释放而是回到对象池中复用。在异步调用中很可能这个组件已经被释放了然后又从对象池中被重新利用起来此时需要一种方式区分“之前的组件对象”是否已经被释放。例如下面这段ActorLocationSender的异步更新逻辑public static async ETVoid UpdateAsync(this ActorLocationSender self) { try { long instanceId self.InstanceId; while (true) { if (self.InstanceId ! instanceId) { return; } ActorTask actorTask await self.GetAsync(); if (self.InstanceId ! instanceId) { return; } if (actorTask.ActorRequest null) { return; } await self.RunTask(actorTask); } } catch (Exception e) { Log.Error(e); } }while (true)中是一段异步方法await self.GetAsync()之后ActorLocationSender对象很可能已经被释放了甚至有可能这个对象又被其它逻辑从对象池中再次利用了起来。此时就可以通过 InstanceId 的变化来判断这个对象是否已被释放只要self.InstanceId ! instanceId就说明对象已经经历了一次“释放→重新分配”后续逻辑立即返回避免对已失效对象进行操作。原因二全局唯一且携带位置信息InstanceId 是全局唯一的并且带有位置信息可以通过 InstanceId 找到对象的位置、将消息发给对象。这个设计会在 Actor 消息机制中利用到如 ActorLocation 定位属于 ET 分布式通信的基础设施之一。总结ET 的“一切皆实体”不是对 ECS 的生搬硬套而是一套面向游戏开发协作的务实设计用树状实体结构替代扁平式 ECS解决组件冗余与“不知道该用哪些组件”的协作难题用“通用数据放成员、专用数据放组件”的准则简化数据放置决策以Game.Scene为顶层组织整棵数据树通过统一的组件创建入口当前源码中的Entity.AddComponent/AddChild与EntitySystemSingleton.Instance.Awake确保 AwakeSystem 正确抛出、对象池正确启用通过IDisposable级联释放保证实体树可以被整体回收Destroy System、对象池回收到位通过InstanceId为对象池复用场景下的异步逻辑提供失效检测也为 Actor 消息定位打下基础。对于想要深入源码的读者建议重点阅读 Entity.cs树结构、AddComponent/AddChild 与 Dispose 实现、Scene.csScene 与 Fiber 的关系以及 EntitySystemSingleton.csAwake/Destroy 等 System 的调度入口再配合 IAwakeSystem.cs 与 IDestroySystem.cs 理解系统回调的挂接方式。【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表