接下来的内容将对ECS的三个组成部分由浅至深的进行阐述,并为接下来的ECS系统流程抛砖引玉,没有弹射起步,请以放松的姿态阅览。
通过之前的HelloWorld程序,相信大家已经对如何编写一个ECS程序有了一个大致的概念,如果仍然不够清晰,也不用太在意,只需要牢记:System根据Component去更新Entity引用的数据这一点。我再进行特别讲解。
除此之外:把编写ECS程序想象成“我是在管理成块成块的数据(Data)”,而不是“管理对象(Object)”,这样的思维方式转变是非常重要的。
一、Entity(实体)
在代码层面,你就可以了解到,实体非常非常的简单。它的核心部分:
public struct Entity
{
public int Index;
public int Version;
}
Index表示这个实体的ID,和身份证一样用于实体与实体之间区分。Version用于描述这个实体的生命周期,由于实体是可以重用的,那么就需要用Version来区分这个Entity是新生实体,还是即将销毁的实体。前文说过,实体是对数据的索引,实体并不包含任何数据或者是函数逻辑,数据存储在Component中,函数逻辑则由System驱动,实体仅仅扮演一名引路人,引领你前往存储数据的地方(Component)
二、Component(组件)
IComponentData
IcomponentData是表示组件最基本的接口,大部分种类的组件都实现此接口。此接口非常非常之简单,它是空的,仅仅是让系统知道,继承了此接口的部分将会被识别成组件,并来用来存储数据。组件必须是结构体,而且不能包含引用类型
public struct MyComponent:IComponentData
{
public float Value;
}
ISharedComponentData
ISharedComponentData表示多个实体共享的数据的接口。此接口多用于存储材质,网格数据,AI数据等通用的数据,比如大量一模一样的丧尸,但是大量模型一样,颜色不一样的丧尸却不行,因为材质数据不一样。ISharedComponentData的强大优势在于用最小的内存访问开销获得大批量的数据,同时能使Burst Complier的效率最大化,用在描述状态机的情况下非常合适。
[System.Serializable]
public struct MyRenderMesh:ISharedComponentData,IEquatable<RenderMesh>
{
public Mesh mesh;
public Material mat;
}
被ISharedComponentData实现的组件将会为每一个字段属性都分配一个单独的内存块(存引用信息),并在内存块外存储一个值(不会多也不会少,只有一个值)这样的话,当具有相同原型(MyRenderMesh)的实体大量出现在场景内时,内存并不会成倍增加,而是仍仅仅占用一开始分配好的内存块。此外,如果每个实体的组件的值不同,每一个不同的值都要分配一个内存块,这样的存储效果将十分差。实现的组件的值存储在块之外,块内仅存引用信息,所以它可以包含引用类型,由于要有能够识别要共享的数据部分的能力,就需要继承IEquatable<T>并实现GetHashCode覆盖。
最后ISharedComponent还必须可序列化以允许保存和还原数据。
除了这两种Component以外还有
-
IBufferElementData
-
ISharedComponentData
-
ISystemStateComponentData
-
ISystemStateBufferElementData
-
ISystemStateSharedComponentData
几种类型的Component,但这里先不用考虑这些,先学会熟练使用并理解IComponentData与ISharedComponentData这两种最为常用的Component。
三、System(系统)
在UnityECS中可以通过继承ComponentSystem来定义系统(继承JobComponentSystem则是定义使用JobSystem的系统)
public class MySystem:ComponentSystem
{
protected override void OnUpdate()
{...}
}
必须实现OnUpdate()方法,在OnUpdate()里编写自己想要执行的逻辑。和Monobehaviour非常类似,OnUpdate()是每帧进行,如果想在OnUpdate()之前或者之后进行操作,可以实现OnCreate()和OnDestory()方法。
JobSystemComponent则有些区别,你必须声明一个Job,并在三种流程中对Job进行操作,例如每帧将逻辑分发给不同的核心(Job.Schedule())大致的过程是这样的:
public class MyJobSystem:JobComponentSystem
{
struct MyJob:IJobForEach<T...>
{
...
}
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
...
var job=new MyJob().Schedule(this,inputDeps);
}
}
除了这三个基本的组成以外,UnityECS还有一个重要的部分:世界
四、(一)世界(World)
World包含了所有的Entity,Component,System 是一个ECS的顶层管理者。严格意义上来说一个ECS程序应当只有一个World,但是存在多个World也是可以的,你可以模拟多个不同的World,或者多个运行规律相同的World,但是不同的World是完全独立的,不能互相干涉,也没有办法将实体或是组件发送到另一个世界。
默认情况下,程序会自动生成一个包含所有已定义System的世界--->World.Active并会被设为初始World,你可以通过接口:ICustomBootstrap或者宏定义:UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP、UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP_RUNTIME_WORLD、UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP_EDITOR_WORLD来禁用这个流程,但不推荐你这么做。
四、(二)实体管理者(EntityManager)
World中可以获得重要的类:EntityManager。World包含了所有的Entity,Component,System。但是World仅仅直接控制System,Entity与Component则由EntityManager进行管理。你可以注意到,在前文中,Entity、Component的产生都是由EntityManager进行的:
你可以手动调用World生成System,但是不能生成Entity或是Component,同样World也不能访问Entity或是Component。