ECS 系统 Entity-Component-System

  参考链接 : 

  https://connect.unity.com/p/part-1-unity-ecs-briefly-about-ecs

  http://esprog.hatenablog.com/entry/2018/05/19/150313

  已经推出了很久了, 貌似也有一些人开始使用, 我是在看守望先锋的程序设计相关文章的时候看到 ECS 的, 从它的设计逻辑上看, 核心就是 Composition over inheritance (or composite reuse principle) 组合重用, 也就是对特定的数据组合, 使用特定的处理过程来处理, 跟我们一般的 OOP 有点差别, 它是以数据组合为对象的.

  很多人都把它描述成高效运行的框架, 而我看守望先锋它也是这样定义的, 那么这个框架到底高效在哪里呢? 从我的主观来看, 它只不过是换了一种程序设计思路, 就跟我们写 UI 逻辑的同学之前把逻辑啊数据啊网络啊都写在 UI 脚本中然后突然有一天他用了 MVC 的模式把数据和逻辑分开了一样, 区别只在复用性和维护性上, 并不构成高效的保证. (2020.03.02 重新看了一遍, 守望先锋那篇文章, 他们使用ECS的目的是为了解耦, 解耦, 解耦! 而不是为了提高框架效率, 这个应该可以简单理解为一个强制MVC框架).

  再看看它的处理数据的过程, 当数据都进行正确的分组之后, 可以经过处理逻辑一次性地处理批量数据, 这个叫 Batch Processing 的过程就有点渲染管线的味道了, 姑且不说这个批处理能高效多少, 至少在各种需要 Update 计算的逻辑上能物理上限制人为错误导致性能问题, 因为很多同学直接添加 Update 函数然后写更新逻辑, 数量巨大的话调用开销让人受不了的.

  来看看它现在提供的 ECS 的可能的扩展, 看出 ECS 大致有三种模式 : 

  1. Pure ECS

  2. Hybird ECS

  3. ECS on Job

  真正能提供性能提升的应该就是 Job 系统了, 包含多线程, 内存 layout 之类的提升, 其实按照数量级来看, UI底层框架 / 动画 / 粒子特效 这些才是量级较大并且跟实际开发没有太大关联的东西, 我们真正开发中写的上层逻辑比如战斗, 除掉动画和特效也就只有人物位移之类的少量控制了, 即使用上 ECS + Job 也应该没有很明显的提升, 最近出了一个叫 UI framework for DOTS 的基于 ECS 的 UI 框架系统, 也有些 ECS 的动画系统, 应该大方向还是在修改这些上, 明天下载一些例子来看看.

  { 这几天去考科目三, 教练简直牛人, 考试前一天晚上18点去考场练习, 跑到第二天早上4点, 简直了, 旅馆睡3小时爬起来就去考, 加上回家坐车3个多小时, 一次来回让人几天都没精神了, 然后科目四还挂了... (2020.01.02) }

 PS : ECS系统没有包含在Unity安装包里, 是个插件, 要自己通过Package Manager下载或者修改Packages/manifest.json文件让编辑器去安装, 有点像安卓库.

==================== 继续测试(2020.01.08) ====================

  按照常理来说, ECS 的框架设计应该是脱离引擎的, 它只需要对它要处理的数据负责, 所以就有了 Pure ECS / Hybird ECS 的区别, 我下载来两个例子很有代表性, 先看最简单的 Hybird ECS( 混合式 ECS) : 

我们在场景中创建10000个自动旋转的正方体, 用 ECS 的方式进行旋转逻辑:

using UnityEngine;

public class EntryScript : MonoBehaviour
{
    public GameObject Cube;

    private void Awake()
    {
        if(Cube)
        {
            for(int i = 0; i < 10000; i++)
            {
                var copy = GameObject.Instantiate(Cube);
                copy.transform.position = UnityEngine.Random.insideUnitSphere * UnityEngine.Random.Range(1, 1000);
            }
        }
    }
}

我们在 Cube 上进行添加 ECS 的组件 ( RotationComponent ) 和指定它为 Entity ( GameObjectEntity ) : 

GameObjectEntity 是系统自带的, RotationBehaviour 是我们自己添加的, 它充当了一个数据类型 : 

using UnityEngine;

public class RotationBehaviour : MonoBehaviour
{
    public float speed = 135.0f;
}

RotationComponent 就是 ECS 的数据集合 Component, 当 Entity 符合有这个数据集合的时候, 会有相应的 System 被调用 :

using UnityEngine;

struct RotationComponent
{
    public RotationBehaviour rotation;
    public Transform transform;
}

这样 Entity 和 Component 都有了, 我们要添加一个 System 来操作这个数据 : 

using UnityEngine;
using Unity.Entities;

public class RotationSystem : ComponentSystem
{
    protected override void OnUpdate()
    {
        foreach(RotationComponent component in GetEntities<RotationComponent>())
        {
            component.transform.Rotate(0f, component.rotation.speed * Time.deltaTime, 0f);
        }
    }
}

可以运行, 里面的正方体在旋转.

  不过感觉跟效率什么的没什么关系, 这是混合ECS(Hybird)的特点: 使用方便, 符合用户习惯,  对于理解上来说也很方便, 一个游戏实体挂上GameObjectEntity, 它就是一个ECS实体了, 当它挂上RotationBehaviour它就是有旋转行为的实体了, 我们要实现这个旋转逻辑, 在RotationSystem里面写就行了, 很清晰.

  不过凡事还是要分两面看, 一个效率又高, 开发又快的框架, 它的 开发/维护 难度在哪呢? 我觉得是在需求变更上, 如果一个系统它需求经常变更, 就不适用ECS. 反之比如UI系统, 它的坐标计算, 组件功能都是确定的, 没有太多的变化, 就很合适使用ECS, 然后ECS的组件冲突在实际开发中是否会大量出现, 很依赖于程序设计, 初期设计不好, 后期需求变更后牵一发动全身. 如下在上面的基础上B同学写了另一个旋转逻辑:

   

  运行断点, 发现两个System都进来了, 因为旋转相反, 正方体都不动了. 这种全局的系统最先想到的就是程序设计冲突, 本来相同的数据(Transform + RotationBehaviour)在不同的地方有不同的意义, 这就是面向对象的思维, 而这个系统是全局的, 它只要有这两个组件在, 它就运行, 如果一个同学想用它旋转特效(主要Y轴), 一个人想做个UI旋转(主要Z轴), 那就特效和UI都在Y轴+X轴旋转了. 为了解决这个冲突, 有那么几种修改方案:

  1. 给RotationBehavuour添加绕哪个轴旋转的功能, 然后在RotationSystem里面去判断, 也就是说B同学在已有的基础上进行修改.

  

  

  这样修改的话, 就可以符合两个人不同的需求了, 并且在原代码的基础上做修改, 比较简单. 可是放在项目中, 就没那么简单了, 这个依赖于序列化的功能, 数量可能很大, 在后期有变更的时候维护起来能烦死人, 特别是一些开发到一半的项目.

  并且, 在使用ECS系统的时候, 它并不按照组件的生命周期进行ECS的生命周期管理, 比如我在某个物体上运行时删除RotationBehaviour这个脚本, 也不影响它被ECS引用的事实, 并且导致"null"对象

    这里运行时删掉了脚本

    断点能够看到, 它被删除了, 可是引用仍然被ECS引用过来, 成了"null"对象

  所以可以看到ECS如果作为上层逻辑它是有很多缺陷和不足的, 特别是需求无限变更的项目组, 打个比方就像拿 Compute Shader 来做所有的计算一样不一定合适.

  PS : 有个很可怕的问题, 在修改了哪个轴向旋转之后, 我想到按照这个逻辑我动态添加Y旋转和X旋转的话我要怎么写代码才能实现?  

  

  果然, 如果同一个Entity使用两个相同的组件, 就报错了 : ArgumentException: It is not allowed to have two components of the same type on the same entity. (RotationBehaviour and RotationBehaviour)

  

  这是一个问题, 改一下看看:

  

  结果就是覆盖式的了, 如果我们不执行Delete操作, 直接改变轴向, 只是产生了新的Entity, 老的Entity没有执行Exit操作的话, 是否会导致一些内存泄漏就不知道了, 这个还需要测试. 这种情况下绕轴旋转的功能就无法叠加了, 只能绕某个轴旋转, 所谓的扩展性受限. 当然解决的办法很多, 可是单从这个例子就看出问题了.

  这样看来它也能做类似决策树这样的结构, 比如A, B, C三种debuff, AB同时存在时角色被减速, AC同时存在时角色被定身, BC同时存在时角色死亡, 这样每次加debuff的时候就AddToEntityManager, 关联数据就自动被相应系统处理了哈哈.

  2. 另外添加一套旋转系统, 就叫RotationBehaviour_UI, 系统就叫RotationSystem_UI之类的, 这样不会影响到已有的系统, 减少耦合性. 可是一个系统就变成两个了, 如果随着开发越叠越多, 两个变四个, 四个变八个, 求解2的N次幂.

  

  好了上面就是通过混合型ECS的例子能想到的对于开发的影响, 东西是好, 可是用在刀刃上才是好. 因为ECS有学习成本并且跟我们的开发习惯有差别, 如果是一个封闭的系统比如寻路, 蒙皮, 粒子等肯定很好, 如果是上层逻辑, 这就要小心使用, 因为对某些功能是些理论上无解的.

  下来是Pure ECS的例子, 它已经使用了Job系统了, 感觉比较混乱, 估计也是对ECS on Job不是太明白.

  PS : 比较老的版本(2018.3)例子, 新版本有变更, 比如 [ComponentDataWrapper and SharedComponentDataWrapper have been renamed to ComponentDataProxy and SharedComponentDataProxy] 这些.

   先来看看运行时的情况:

    

  它在一个圆圈范围上生成了一些正方体, 正方体的Prefab如下:

  

  可以看到, 正方体它是没有MeshRenderer组件的, 只是有个MeshInstanceRendererComponent组件对相关资源有引用. 也就是说Prue ECS它是脱离引擎组件来运行了, 下面还有各种PositionComponent, RotationComponent等代替了引擎原有的组件系统, 这样做有什么好处呢? 先看它运行时Entity Debugger的信息 : 

  

  最终这些Entity没有GameObject实体, 它们是通过EntityManager进行实例化的, 所以只在Entity Debugger面板中能查看到, 并且显示了它被多个System控制的情况, 那么它脱离引擎组件的好处在哪呢? 下面通过代码来分析:

  看看RotationSpeedComponent / RotationComponent组件的组合作用, RotationComponent代替Transform的旋转, 和渲染系统一起作用来渲染物体的旋转, 而RotationSpeedComponent组件跟它组合的时候, 受到RotationSpeedSystem这个系统的控制, 随时间改变物体的旋转.

  

  

  至于它的继承关系的说明, IComponentData指示了更高效的存储结构, 所以IJobProcessComponentData系统的输入输出直接使用 IComponentData, 必须继承它, 可以看到Rotatoin类是ECS自带的, 它的旋转变量quaternion使用了 Unity.Mathematics 库, 我对它比较疑惑, 虽然说为了BurstCompile能提高NativeCode的效率, 因为看代码没有什么高效性的东西除了一堆 [MethodImpl(MethodImplOptions.AggressiveInlining)] 属性.

  

   然后是自旋系统RotationSpeedSystem已经继承于JobComponentSystem了, 这是专门提供给ECS用的Job扩展, 它的OnUpdate函数跟ComponentSystem有点不同, 返回的是一个JobHandle, 看文档它应该就是一个Task之类的, 因为我还没看Job系统的介绍, 估计是多线程的吧, 因为它脱离了引擎的组件嘛. 这里看到了高大上的 [BurstCompile] 属性, 提高编译后代码质量的......

  这里的旋转逻辑写到了RotationSpeedRotation里面去了, 它继承于 IJobProcessComponentData 也是Job系统提供的, 应该在计算上提升最大的点就在这里了, 所以配上了 [BurstCompile] 属性吧... 接下来就是旋转系统怎样使用它了, System通过OnUpdate返回它的JobHandle来完成计算, 经过测试System的OnUpdate在主线程中运行, 而 RotationSpeedRotation的Execute是在多线程中运行的.

  这样就明白了Pure ECS的工作原理和性能暴力的点了, 首先通过脱离引擎自带的一些组件, 提供了在多线程上运行能力, 解放了主线程, 然后是它Job系统和BurstCompile, PHC之类的提高运行效率的能力. 总之很好很强大, 希望Unity能把引擎自带的性能大户改成ECS的吧, 这样开发者就可以不那么头大了. 

  说回来ECS因为没有实体, 并且在Debugger面板上看到的实体列表没办法跟渲染出来的东西一一对应啊, 哪个是哪个根本不懂, 就跟Shader一样, 哪个像素我不知道...... 打断点的话也和跟踪行为树一样有时候找不到头脑的样子.

  然后是有点让人疑惑的地方, 从Hybird ECS那里就一直有的: 系统是怎样知道这个系统在处理哪些数据的呢? 或者反过来哪些数据被哪个系统处理的? 看下图:

  

  这个系统就跟普通MonoBehaviour一样, OnUpdate是每帧调用的, foreach之中的函数才是处理数据的, 然而在Entity Debugger中能显示出来Entity是被这个系统控制的, 其实挺有意思的:

  

  

  (2020.03.04)

  漏了另外一种直接使用ECS的方式了, 这个在工程中也有使用, SpawnRandomCircleSystem它被用来创建那些正方体. 看看代码上它是怎样使用的, 这个用法在Unity Connent上有文章说 : 

  https://connect.unity.com/p/part-1-unity-ecs-briefly-about-ecs

  

  这个Group是当前系统的数据集合, 说明当前系统处理这些数据组合, 数据都要继承于IComponentData或者ISharedComponentData接口, 这里的 [Inject] 属性就是我上面说的, 你怎么知道当前系统处理的是哪些数据的问题, 有了Inject标签才能解释的通啊......

  然后是它的数组类型, SharedComponentDataArray<T>, ComponentDataArray<T>, 数组也要符合继承于IComponentData或者ISharedComponentData接口, 所以需要这两个容器.

  然后是 [ReadOnly] 属性, 说明是 : The ReadOnly attribute lets you mark a member of a struct used in a job as read-only. 是一些多线程的问题, 为了高效性的话, 使用SharedComponentDataArray<T>容器.

  

  SharedComponentDataArray不是 [NativeContainer], Job系统需要NativeContainer才能操作内存, 可能只读形式的内存不需要吧..

  コピーするデータはBlittableデータ型 (マネージコードとネイティブコードの間で渡されたときに変換を必要としないデータ型)のみで、JobSystemではこれに該当する型にしかアクセス出来ないという制約があります。

  PS : 这样看来, 单是数据类型的确定就已经比较糟糕了, 如果你希望系统在处理某些数据能高速并行访问, 就要使用 SharedComponentDataArray 容器, 然后它要求的数据要继承于ISharedComponentData接口, 这样就需要在使用中根据需要反过来修改数据类型了, 而数据有多个系统共用的话, 就麻烦了......

   PS : 这个例子里看出来, Unity把原组件拆分出来了, 可以减少不必要的组件, 比如Rotation, Position这些从Transform分离出来了, 你可以用可以不用... 喂喂喂, 难道Unity觉得我们都是做3A级游戏的吗? 这种简直像是重做一遍引擎框架的事情学渣们能做出来吗? 或者说这个是为了以后走向电影市场做的准备吗?

  不管怎样, 有人用在寻路上, 有人用在蒙皮上, 潜力很大, 使用设计上难度很高.

  从这两天看的官方文档以及各个博客来看, 跨版本间的ECS系统差别很大, 至少API上差别很大, 从2018到2019至少有三个API版本, 估计还在开发中. (2020.03.04)

  

  

原文地址:https://www.cnblogs.com/tiancaiwrk/p/12121063.html