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了。

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信息的数组。

 

 

  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Unity ECSEntity Component System)是一种用于高性能游戏开发的系统,通过使用 ECS 可以优化游戏的性能并提高游戏开发的效率。下面是一个简单入门教程: 1. 创建一个 ECS 项目 首先,在 Unity 中创建一个新项目。在创建项目时,选择“High Definition RP(Preview)”模板,这将为你创建一个使用 ECS 的项目。 2. 创建实体(Entity) 在 ECS 中,实体是一个简单的标识符,用于标识游戏中的对象。在 Unity 中,可以通过使用实体组件(Entity Component)来创建实体。在 Unity 中,可以使用以下代码创建一个实体: ```csharp Entity entity = EntityManager.CreateEntity(); ``` 3. 创建组件(Component) 组件是实体的属性,它们包含实体的数据和行为。在 Unity ECS 中,组件是 C# 类,它们必须继承自 IComponentData 接口。以下是一个示例组件: ```csharp public struct Position : IComponentData { public float3 Value; } ``` 在这个示例中,Position 是一个包含 float3 类型变量 Value 的组件。 4. 添加组件到实体 要将组件添加到实体中,可以使用 EntityManager 的 AddComponent 方法。以下是一个示例代码,将 Position 组件添加到 entity 实体中: ```csharp EntityManager.AddComponentData(entity, new Position { Value = new float3(0, 0, 0) }); ``` 5. 创建系统(System) 系统是用于处理实体和组件的逻辑的代码。在 ECS 中,系统是 C# 类,它们必须继承自 ComponentSystem 类。以下是一个示例系统代码: ```csharp public class MoveSystem : ComponentSystem { protected override void OnUpdate() { Entities.ForEach((ref Position position) => { position.Value += new float3(0, 1, 0); }); } } ``` 在这个示例中,MoveSystem 是一个系统,它会处理所有带有 Position 组件的实体,并将它们的位置向上移动一个单位。 6. 将系统添加到 ECS 要将系统添加到 ECS 中,需要在 Unity 中创建一个 GameObject,并将其命名为“ECS Manager”。然后,将 ECS Manager 组件添加到 GameObject 中,并将 MoveSystem 添加到 ECS Manager 中。 7. 运行游戏 现在,可以启动游戏并查看实体和组件的效果。在这个示例中,应该会看到所有带有 Position 组件的实体都向上移动一个单位。 这里只是一个简单入门教程,Unity ECS 还有很多高级功能和概念需要学习。但是通过上面的步骤,你可以了解到 ECS 的基本概念,并开始使用 ECS 来开发高性能游戏。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值