ECS的简单入门(二):Entity

介绍完ECS的大致理念之后,我们接着单独来了解下Entity,Component和System的概念以及使用,首先我们先从Entity开始入手。

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的关系。

World

我们可以将其理解成一种Manager,每一个World都拥有一个EntityManager和一系列的System,我们可以自由的创建World:

World newWorld = new World("NewWorld");

ECS中会有一个默认的World(DefaultWorld),可以通过 World.DefaultGameObjectInjectionWorld 来获取它。系统自带的System和我们自定义的System默认都会被添加进这个World。

可以在Entity Debugger(Window -> Analysis 中打开)中查看World

创建Entity

创建一个Entity

前面我们说到Entity是由EntityManager来管理的,而EntityManager又存放与World之中,因此要创建Entity,我们先要获得EntityManager,然后就可以利用其CreateEntity方法来创建我们的Entity了。

扫描二维码关注公众号,回复: 11632078 查看本文章
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);

创建多个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

entityManager.DestroyEntity(entity);

对于生成的Entity,我们可以在Entity Debugger中查看

EntityQuery

注:建议先看了Component和System相关内容后再来看。

在ECS中,Entity关联着Component,Component存储着所有的数据,若要读取或者修改这些数据,自然首先要找到哪些数据是我们所需要的。而 EntityQuery 就包含了这些我们想要的数据,其主要功能如下

  • 运行一个Job来处理选定的Entities和Components
  • 获取一个NativeArray 包含所有选定的Entity
  • 根据Component类型获取多个包含选定Component的NativeArray

EntityQuery在返回Entity或Component 的NativeArray时保证是并行的,也就是相同的下标每次获取到值都是一样的。

定义一个Query

想要得到包含我们想要的数据的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

我们可以使用EntityQueryDesc 来更详细的制定查询规则,带有三个关键字分别为 AllAnyNone(和 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});

除了前面提到的 AllAnyNone 三种属性外,我们还可以设置 Options 属性,其值是一个 EntityQueryOptions 枚举,如下:

  • Default: 不设置options的值默认即为Default
  • IncludePrefab: 含有特殊的 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就会被剔除。

创建EntityQuery

在前面我们利用 SystemBase 提供的 GetEntityQuery 方法来获取一个EntityQuery,若在System外,我们可以利用 EntityManager.CreateEntityQuery() 方法来创建一个EntityQuery

EntityQuery query = CreateEntityQuery(typeof(Translation), ComponentType.ReadOnly<LocalToWorld>());

filters

我们还可以对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信息的数组。

猜你喜欢

转载自blog.csdn.net/wangjiangrong/article/details/107064021