要读取或写入数据,首先必须找到要更改的数据。ECS 中的数据存储在组件中,ECS 根据它们所属实体的原型在内存中将这些组件组合在一起。您可以使用EntityQuery获取 ECS 数据的视图,其中仅包含给定算法或流程所需的特定数据。
您可以使用EntityQuery执行以下操作:
- 运行作业以处理选定的实体和组件
- 获取包含所有选定实体的 NativeArray
- 获取所选组件的 NativeArrays(按组件类型)
EntityQuery返回的实体和组件数组保证是“并行的”,即相同的索引值始终适用于任何数组中的相同实体。
笔记
SystemBase Entities.ForEach构造根据您为这些 API 指定的组件类型和属性创建内部EntityQuery实例。您不能将不同的EntityQuery对象与Entities.ForEach一起使用(尽管您可以获得Entities.ForEach实例构造的查询对象并在其他地方使用它)。
定义查询
一个EntityQuery查询定义了一组部件类型的原型必须包含ECS到包括在视图中其组块和实体。您还可以排除包含特定类型组件的原型。
对于简单查询,您可以基于组件类型数组创建EntityQuery。以下示例定义了一个EntityQuery,用于查找具有 RotationQuaternion 和 RotationSpeed 组件的所有实体。
EntityQuery query
= GetEntityQuery(typeof(RotationQuaternion),
ComponentType.ReadOnly<RotationSpeed>());
该查询使用ComponentType.ReadOnly<T>而不是更简单的typeof表达式来指定系统不写入 RotationSpeed。在可能的情况下始终指定只读,因为对数据的读取访问限制较少,这可以帮助作业调度程序更有效地执行作业。
实体查询描述
对于更复杂的查询,您可以使用EntityQueryDesc对象来创建EntityQuery。一个EntityQueryDesc提供了一种灵活的查询机制,以指定根据以下部件组中选择哪个原型:
All
: 此数组中的所有组件类型都必须存在于原型中Any
: 该数组中的至少一种组件类型必须存在于原型中None
:这个数组中的任何组件类型都不能存在于原型中
例如,以下查询包括包含 RotationQuaternion 和 RotationSpeed 组件的原型,但排除包含 Frozen 组件的任何原型:
var queryDescription = new EntityQueryDesc
{
None = new ComponentType[] { typeof(Static) },
All = new ComponentType[]{ typeof(RotationQuaternion),
ComponentType.ReadOnly<RotationSpeed>() }
};
EntityQuery query = GetEntityQuery(queryDescription);
笔记
不要在EntityQueryDesc 中包含可选组件。要处理可选组件,请使用ArchetypeChunk.Has确定块是否包含可选组件的方法。因为同一块内的所有实体都具有相同的组件,所以您只需检查每个块是否存在一次可选组件:而不是每个实体一次。
查询选项
创建EntityQueryDesc 时,可以设置其Options
变量。这些选项允许专门的查询(通常您不需要设置它们):
- 默认值:未设置选项;查询行为正常。
IncludePrefab
:包括包含特殊预制标记组件的原型。IncludeDisabled
:包括包含特殊禁用标记组件的原型。FilterWriteGroup
:考虑查询中任何组件的 WriteGroup。
当您设置该FilterWriteGroup
选项时,只有在写入组中明确包含在查询中的那些组件的实体才会包含在视图中。ECS 不包括具有来自同一 WriteGroup 的任何附加组件的任何实体。
在以下示例中,C2 和 C3 是基于 C1 的同一写入组中的组件,此查询使用需要 C1 和 C3 的 FilterWriteGroup 选项:
public struct C1 : IComponentData { }
[WriteGroup(typeof(C1))]
public struct C2 : IComponentData { }
[WriteGroup(typeof(C1))]
public struct C3 : IComponentData { }
public class ECSSystem : SystemBase
{
protected override void OnCreate()
{
var queryDescription = new EntityQueryDesc
{
All = new ComponentType[] { ComponentType.ReadWrite<C1>(),
ComponentType.ReadOnly<C3>() },
Options = EntityQueryOptions.FilterWriteGroup
};
var query = GetEntityQuery(queryDescription);
}
protected override void OnUpdate()
{
throw new NotImplementedException();
}
}
此查询排除了同时具有 C2 和 C3 的任何实体,因为 C2 未明确包含在查询中。虽然您可以使用None
将其设计到查询中,但通过写入组执行此操作提供了一个重要的好处:您不需要更改其他系统使用的查询(只要这些系统也使用写入组)。
写入组是一种可用于扩展现有系统的机制。例如,如果在另一个系统中定义了 C1 和 C2(可能是您无法控制的库的一部分),您可以将 C3 放入与 C2 相同的写入组中以更改 C1 的更新方式。对于您添加到 C3 组件的任何实体,系统会更新 C1,而原始系统不会。对于没有 C3 的其他实体,原始系统像以前一样更新 C1。
有关更多信息,请参阅写入组。
组合查询
要组合多个查询,您可以传递EntityQueryDesc对象数组而不是单个实例。您必须使用逻辑 OR 操作来组合每个查询。以下示例选择包含 RotationQuaternion 组件或 RotationSpeed 组件(或两者)的任何原型:
var desc1 = new EntityQueryDesc
{
All = new ComponentType[] { typeof(RotationQuaternion) }
};
var desc2 = new EntityQueryDesc
{
All = new ComponentType[] { typeof(RotationSpeed) }
};
EntityQuery query
= GetEntityQuery(new EntityQueryDesc[] { desc1, desc2 });
创建实体查询
在系统类之外,您可以使用EntityManager.CreateEntityQuery函数创建一个EntityQuery,如下所示:
EntityQuery query =
entityManager.CreateEntityQuery(typeof(RotationQuaternion),
ComponentType.ReadOnly<RotationSpeed>());
但是,在系统类中,您可以从系统获取查询,而不是从头开始创建它。系统缓存您的实现创建的任何查询并返回缓存的实例,而不是在可能的情况下创建一个新的实例。
当您的系统使用Entities.ForEach 时,请使用WithStoreEntityQueryInField获取Entities.ForEach构造所使用的查询实例:
public partial class RotationSpeedSys : SystemBase
{
private EntityQuery query;
protected override void OnUpdate()
{
float deltaTime = Time.DeltaTime;
Entities
.WithStoreEntityQueryInField(ref query)
.ForEach(
(ref RotationQuaternion rotation, in RotationSpeed speed) => {
rotation.Value
= math.mul(
math.normalize(rotation.Value),
quaternion.AxisAngle(math.up(),
speed.RadiansPerSecond * deltaTime)
);
})
.Schedule();
}
}
在其他情况下,例如当您需要查询实例来安排 IJobChunk 作业时,请使用GetEntityQuery函数:
public class RotationSystem : SystemBase
{
private EntityQuery query;
protected override void OnCreate()
{
query = GetEntityQuery(typeof(RotationQuaternion),
ComponentType.ReadOnly<RotationSpeed>());
}
protected override void OnUpdate()
{
throw new NotImplementedException();
}
}
请注意,缓存查询时不考虑过滤器设置。此外,如果您在查询上设置过滤器,则下次您使用GetEntityQuery访问同一查询时会设置相同的过滤器。使用ResetFilter清除任何现有的过滤器。
定义过滤器
过滤器根据以下条件排除那些会被包含在查询返回的实体中的实体:
- 共享组件过滤器:根据共享组件的特定值过滤实体集。
- 更改过滤器:根据特定组件类型的值是否已更改来过滤实体集。
您设置的过滤器一直有效,直到您对查询对象调用ResetFilter。
笔记
写入组使用不同的机制。请参阅查询选项。
共享组件过滤器
要使用共享组件过滤器,请在EntityQuery 中包含共享组件 ——以及其他需要的组件——并调用SetSharedComponentFilter函数。然后传入包含要选择的值的相同 ISharedComponent 类型的结构。所有值必须匹配。您最多可以向过滤器添加两个不同的共享组件。
您可以随时更改过滤器,但如果您更改过滤器,它不会更改您从ToComponentDataArray <T> 或ToEntityArray函数组收到的任何现有实体或组件数组。您必须重新创建这些数组。
以下示例定义了一个名为 SharedGrouping 的共享组件和一个仅处理 Group 字段设置为 的实体的系统1
。
struct SharedGrouping : ISharedComponentData
{
public int Group;
}
class ImpulseSystem : SystemBase
{
EntityQuery query;
protected override void OnCreate()
{
query = GetEntityQuery(typeof(Position),
typeof(Displacement),
typeof(SharedGrouping));
}
protected override void OnUpdate()
{
// Only iterate over entities that have the SharedGrouping data set to 1
query.SetSharedComponentFilter(new SharedGrouping { Group = 1 });
var positions = query.ToComponentDataArray<Position>(Allocator.Temp);
var displacements = query.ToComponentDataArray<Displacement>(Allocator.Temp);
for (int i = 0; i < positions.Length; i++)
positions[i] =
new Position
{
Value = positions[i].Value + displacements[i].Value
};
}
}
更改过滤器
如果您只需要在组件值更改时更新实体,您可以使用SetChangedVersionFilter函数将该组件添加到EntityQuery过滤器。例如,以下EntityQuery仅包含来自另一个系统已写入 Translation 组件的块的实体:
EntityQuery query;
protected override void OnCreate()
{
query = GetEntityQuery(typeof(LocalToWorld),
ComponentType.ReadOnly<Translation>());
query.SetChangedVersionFilter(typeof(Translation));
}
笔记
为了提高效率,更改过滤器适用于整个块,而不是单个实体。更改过滤器还只检查系统是否运行了声明对组件的写访问权限,而不检查它是否实际更改了任何数据。换句话说,如果另一个能够写入该类型组件的作业访问该块,则更改过滤器将包含该块中的所有实体。这就是为什么您应该始终声明对不需要修改的组件的只读访问权限。
执行查询
通常,您在安排使用它的作业时“执行”查询。您还可以调用返回实体、组件或块数组的EntityQuery方法之一:
- ToEntityArray返回所选实体的数组。
- ToComponentDataArray返回
T
所选实体类型组件的数组。 - CreateArchetypeChunkArray返回包含所选实体的所有块。因为查询对原型、共享组件值和更改过滤器进行操作,这些对于块中的所有实体都是相同的,所以存储在返回的块集合中的实体集与ToEntityArray返回的实体集完全相同。