介绍完ECS的大致理念之后,我们接着单独来了解下Entity,Component和System的概念以及使用,首先我们先从Entity开始入手。
我们来看下Entity的组成,代码如下,只有Index和Version两个字段。
public struct Entity { /// <summary> /// The ID of an entity. /// </summary> public int Index; public int Version; }我们可以理解为Entity就是游戏中不同的物体,其中只有一个ID,也就是Index用来进行区分。Entity中不存在任何数据和行为,这些交给了Component和System去办了。
在每个World中,会有一个EntityManager来管理该World下所有的Entity,EntityManager利用NativeArray来管理Entities并且纪录了每个Entity和Component的关系。
我们可以将其理解成一种Manager,每一个World都拥有一个EntityManager和一系列的System,我们可以自由的创建World:
World newWorld = new World("NewWorld");ECS中会有一个默认的World(DefaultWorld),可以通过 World.DefaultGameObjectInjectionWorld 来获取它。系统自带的System和我们自定义的System默认都会被添加进这个World。
可以在Entity Debugger(Window -> Analysis 中打开)中查看World
前面我们说到Entity是由EntityManager来管理的,而EntityManager又存放与World之中,因此要创建Entity,我们先要获得EntityManager,然后就可以利用其CreateEntity方法来创建我们的Entity了。
EntityManager entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; Entity entity = mEntityManager.CreateEntity();若要为Entity添加组件,也需要使用EntityManager来完成,例如添加一个用于存放坐标信息的Translation Component并且赋值:
entityManager.AddComponentData(entity, new Translation(){Value = new float3(0, 0, 0)});若要删除Entity上的Component,自然是要用RemoveComponent方法了:
entityManager.RemoveComponent<Translation>(entity);除此之外,我们也可以在创建的时候就指定好需要添加的组件,然后再利用 SetComponentData 赋值,例如:
Entity entity = entityManager.CreateEntity(typeof(Translation), typeof(OtherComponent), ...); entityManager.SetComponentData(entity, new Translation(){Value = new float3(0, 0, 0)});前面我们提到Component的不同组合被称为Archetype,因此我们也可实现声明好Archetype,然后用其创建Entity,这样生成的Entity就会带有Archetype所代表的所有Component(其实和上面那种方法是等价的)。
EntityArchetype entityArchetype = entityManager.CreateArchetype(typeof(Translation), typeof(LocalToWorld), typeof(RenderMesh)); Entity entity = entityManager.CreateEntity(entityArchetype); entityManager.SetComponentData(entity, new Translation(){Value = new float3(0, 0, 0)});对于已存在的Entity,我们可以使用EntityManager.Instantiate方法来进行copy
Entity entity2 = entityManager.Instantiate(entity);
CreateEntity不仅可以创建一个Entity,也可以同时创建多个Entity,前提是要事先声明好EntityArchetype,例如
NativeArray<Entity> entityArray = new NativeArray<Entity>(10, Allocator.Temp); entityManager.CreateEntity(entityArchetype, entityArray); entityArray.Dispose();等价于
entityManager.CreateEntity(entityArchetype, 10, Allocator.Temp).Dispose();;对于已存在的Entities我们可以进行整体copy,Entities中的数据都会被copy到新的Entities中。
NativeArray<Entity> copyEntityArray = new NativeArray<Entity>(10, Allocator.Temp); entityManager.Instantiate(entityArray, copyEntityArray); copyEntityArray.Dispose();Instantiate也可将单个Entity进行多份copy
entityManager.Instantiate(entity, 10, Allocator.Temp);
对于生成的Entity,我们可以在Entity Debugger中查看
注:建议先看了Component和System相关内容后再来看。
在ECS中,Entity关联着Component,Component存储着所有的数据,若要读取或者修改这些数据,自然首先要找到哪些数据是我们所需要的。而 EntityQuery 就包含了这些我们想要的数据,其主要功能如下
运行一个Job来处理选定的Entities和Components获取一个NativeArray 包含所有选定的Entity根据Component类型获取多个包含选定Component的NativeArrayEntityQuery在返回Entity或Component 的NativeArray时保证是并行的,也就是相同的下标每次获取到值都是一样的。
想要得到包含我们想要的数据的EntityQuery,那就需要一个查询规则来查询。SystemBase为我们提供了GetEntityQuery的方法来获取EntityQuery,我们可以通过Component Type来进行查询,也就是作为参数传递进去,例如下列代码,可以帮我们找到所有带有Translation和LocalToWorld组件的Entity。
EntityQuery m_Query = GetEntityQuery(typeof(Translation), ComponentType.ReadOnly<LocalToWorld>());注:对于一些只需要读取的组件,我们使用 ComponentType.ReadOnly<T> 可以提高执行效率
在Entity Debugger中选中我们的System,即可看见该System下我们设置的EntityQuery:
我们可以使用EntityQueryDesc 来更详细的制定查询规则,带有三个关键字分别为 All,Any,None(和 Entities.ForEach 几乎一样,就不详细介绍了),例如
var queryDesc = new EntityQueryDesc { All = new ComponentType[] { typeof(Translation), ComponentType.ReadOnly<LocalToWorld>() }, Any = new ComponentType[] { typeof(Scale), typeof(NonUniformScale) } None = new ComponentType[] {typeof(LocalToParent)}, }; EntityQuery entityQuery = GetEntityQuery(queryDesc);会为我们找到包含Translation和LocalToWorld,且至少包含Scale和NoUniformScale中一个的,同时不包含LocalToParent的Entity。
同时还可以使用多个EntityQueryDesc进行查询,例如
var query0 = new EntityQueryDesc { All = new ComponentType[] {typeof(RotationQuaternion)} }; var query1 = new EntityQueryDesc { All = new ComponentType[] {typeof(RotationSpeed)} }; EntityQuery m_Query = GetEntityQuery(new EntityQueryDesc[] {query0, query1});除了前面提到的 All,Any,None 三种属性外,我们还可以设置 Options 属性,其值是一个 EntityQueryOptions 枚举,如下:
Default: 不设置options的值默认即为DefaultIncludePrefab: 含有特殊的 Prefab tag component。IncludeDisabled: 含有特殊的 Disabled tag component。FilterWriteGroup: 查询是会检查Component的WriteGroup 标签FilterWriteGroup:
在定义Component的时候,我们可以利用 WriteGroup 标签来将Component写入一个组中,不同的Component可以设置相同的组,如下,C2和C3都关联在C1的组中
public struct C1: IComponentData{} [WriteGroup(typeof(C1))] public struct C2: IComponentData{} [WriteGroup(typeof(C1))] public struct C3: IComponentData{}我们定义几个EntityQueryDesc,并加上FilterWriteGroup属性,如下:
var query1 = new EntityQueryDesc{ All = new ComponentType[]{typeof(C1), typeof(C2), typeof(C3)}, Options = EntityQueryOptions.FilterWriteGroup }; var query2 = new EntityQueryDesc{ All = new ComponentType[]{typeof(C1), typeof(C2)}, Options = EntityQueryOptions.FilterWriteGroup }; var query3 = new EntityQueryDesc{ All = new ComponentType[]{typeof(C1)}, Options = EntityQueryOptions.FilterWriteGroup }; var query4 = new EntityQueryDesc{ All = new ComponentType[]{typeof(C2)}, Options = EntityQueryOptions.FilterWriteGroup };结果是
query1:找到所有带有C1,C2,C3的Entity
query2:找到所有带有C1,C2,并且不带有C3的Entity
query3:找到所有带有C1,并且不带有C2,C3的Entity
query3:找到所有带有C2的Entity
因此添加了FilterWriteGroup属性后,如果一个组件(如C2,C3)写入另外一个组件(如C1)的组中时,当组拥有者的组件(C1)被查询时,ECS会为我们剔除(类似 None 属性)改组下没有明确声明要被查找的组件。如query2,C1,C2被声明的要查找,所以C3就会被剔除。
在前面我们利用 SystemBase 提供的 GetEntityQuery 方法来获取一个EntityQuery,若在System外,我们可以利用 EntityManager.CreateEntityQuery() 方法来创建一个EntityQuery
EntityQuery query = CreateEntityQuery(typeof(Translation), ComponentType.ReadOnly<LocalToWorld>());
我们还可以对EntityQuery对象设置filter来细分查询的对象
SharedComponentFilter:
基于shared component的值为指定的值做查询,例如下面代码,只会查询到带有SharedGrouping共享组件且 SharedGrouping.Group 的值等于1的数据。
struct SharedGrouping : ISharedComponentData { public int Group; } class ImpulseSystem : SystemBase { EntityQuery query; protected override void OnStartRunning() { base.OnStartRunning(); query = GetEntityQuery(typeof(SharedGrouping)); query.SetSharedComponentFilter(new SharedGrouping { Group = 1 }); } }ChangedVersionFilter:
只查询Component发生了改变的数据,如下面代码,只有当别的System修改了Translation时(此修改只需要给予到Translation写的权限即可),该System才能查询到这些被修改了的数据
query = GetEntityQuery(ComponentType.ReadWrite<Translation>()); query.SetChangedVersionFilter(typeof(Translation));为了效率基于change的过滤应用于整个Chunk而不是单个Entity。并且是否改变的判断只检测Component是否获得了写的权限,而不是其值是否变化,这也印证了是应用于整个Chunk的理念。
可能描述的不太好,举个例子:例如我们有Entity1带有C1组件,Entity2和Entity3都带有C1,C2两个组件,那么Entity1肯定存在Chunk1中,Entity2和3存在Chunk2中。若我们通过C1,C2的Query来查询可以找到Entity2和Entity3,若我们要修改Entity2的C1的值,那么必须给予C1写的权限。那么Chunk2将被标记为改变,即使只是改变了其中Entity2的C1值。此时若有个通过C1的Query来查询,正常会找到Enity1,2,3三个,但是若我们设置了ChangedVersionFilter(typeof(C1)),那么就会找到Entity2和3而不是仅仅只找到值改变了的Entity2。
我们可以在任何时候进行Filter的设置,同时使用 ResetFilter 方法可以清除这些设置。
我们可以通过下列方法来获取到我们需要的数据
ToEntityArray() :将查询到的数据转换为Entity数组ToComponentDataArray<T>() :将查询到的数据转换为类型为T的Component数组CreateArchetypeChunkArray():返回查询到的数据的Chunk信息,例如我们查询到的Entity分别存放在了三个Chunk中,那么就会返回带有这三个Chunk信息的数组。