使用实体批处理作业
使用实体批处理作业
在系统内实现IJobEntityBatch或IJobEntityBatchWithIndex以批量迭代您的数据。
当您在系统的OnUpdate函数中调度IJobEntityBatch作业时,系统会使用您传递给调度函数的实体查询来识别应传递给作业的块。该作业为这些块中的每一批实体调用一次您的函数。默认情况下,批处理大小是一个完整的块,但您可以在调度作业时将批处理大小设置为块的一部分。无论批大小如何,给定批中的实体始终存储在同一块中。在您的作业函数中,您可以逐个实体地迭代每个批次中的数据。Execute
Execute
当您需要一组批次中所有实体的索引值时,请使用 IJobEntityBatchWithIndex。否则,IJobEntityBatch效率更高,因为它不需要计算这些索引。
要实现批处理作业:
-
使用 EntityQuery 查询数据以标识要处理的实体。
-
声明您的作业访问的数据。在作业结构上,包括用于标识作业必须直接访问的组件类型的 ComponentTypeHandle 对象的字段。此外,指定作业是读取还是写入这些组件。您还可以包括标识要查找的数据的字段,这些数据不属于查询的一部分,以及用于非实体数据的字段。
-
编写作业结构的 Execute 函数来转换您的数据。获取作业读取或写入的组件的 NativeArray 实例,然后迭代当前批处理以执行所需的工作。
-
在系统 OnUpdate 函数中调度作业,将标识要处理的实体的实体查询传递给调度函数。
笔记
使用IJobEntityBatch 或IJobEntityBatchWithIndex 进行迭代比使用 Entities.ForEach 更复杂,需要更多的代码设置,并且应该只在必要或更高效时使用。
有关更多信息,ECS 示例存储库包含一个简单的 HelloCube 示例,该示例演示如何使用 IJobEntityBatch。
笔记
IJobEntityBatch取代IJobChunk。主要区别在于,您可以安排IJobEntityBatch来迭代比完整块更小的实体批次,并且如果您需要每个批次中的实体的作业范围索引,则可以使用变体IJobEntityBatchWithIndex。
使用 EntityQuery 查询数据
一个EntityQuery定义了一组部件类型的一个EntityArchetype必须包含系统处理其相关联的组块和实体。原型可以有额外的组件,但它必须至少具有查询定义的组件。您还可以排除包含特定类型组件的原型。
将选择您的作业应处理的实体的查询传递给您用于调度作业的调度方法。
有关定义查询的信息,请参阅使用 EntityQuery 查询数据。
笔记
不要在EntityQuery 中包含完全可选的组件。要处理可选组件,请使用IJobEntityBatch.Execute 中的ArchetypeChunk.Has方法来确定当前ArchetypeChunk是否具有可选组件。因为同一批中的所有实体都有相同的组件,所以你只需要每批检查一次可选组件是否存在,而不是每个实体检查一次。
定义作业结构
作业结构由执行要执行的工作的Execute函数和声明Execute
函数使用的数据的字段组成。
典型的IJobEntityBatch作业结构如下所示:
public struct UpdateTranslationFromVelocityJob : IJobEntityBatch
{
public ComponentTypeHandle<VelocityVector> velocityTypeHandle;
public ComponentTypeHandle<Translation> translationTypeHandle;
public float DeltaTime;
[BurstCompile]
public void Execute(ArchetypeChunk batchInChunk, int batchIndex)
{
NativeArray<VelocityVector> velocityVectors =
batchInChunk.GetNativeArray(velocityTypeHandle);
NativeArray<Translation> translations =
batchInChunk.GetNativeArray(translationTypeHandle);
for(int i = 0; i < batchInChunk.Count; i++)
{
float3 translation = translations[i].Value;
float3 velocity = velocityVectors[i].Value;
float3 newTranslation = translation + velocity * DeltaTime;
translations[i] = new Translation() { Value = newTranslation };
}
}
}
此示例访问实体的两个组件 VelocityVector 和Translation 的数据,并根据自上次更新以来经过的时间计算新的翻译。
IJobEntityBatch 与 IJobEntityBatchWithIndex
IJobEntityBatch和IJobEntityBatchWithIndex之间的唯一区别是IJobEntityBatchWithIndexindexOfFirstEntityInQuery
在对批处理调用 Execute 函数时传递一个参数。该参数是实体查询选择的所有实体列表中当前批次的第一个实体的索引。
当您需要每个实体的单独索引时,请使用 IJobEntityBatchWithIndex。例如,如果您计算每个实体的唯一结果,您可以使用此索引将每个结果写入本机数组的不同元素。如果不使用该indexOfFirstEntityInQuery
值,请改用IJobEntityBatch,以避免计算索引值的开销。
笔记
当您向 [EntityCommandBuffer.ParallelWriter] 添加命令时,您可以使用该batchIndex
参数作为sortKey
命令缓冲区函数的参数。您不需要使用IJobEntityBatchWithIndex来获取每个实体的唯一排序键。该batchIndex
可从两个作业类型参数适用于这一目的。
声明您的工作访问的数据
作业结构中的字段声明可用于执行函数的数据。这些字段分为四大类:
-
ComponentTypeHandle字段——组件句柄字段允许您的 Execute 函数访问存储在当前块中的实体组件和缓冲区。请参阅访问实体组件和缓冲区数据。
-
ComponentDataFromEntity、BufferFromEntity字段——这些“来自实体的数据”字段允许您的 Execute 函数查找任何实体的数据,无论它存储在哪里。(这种类型的随机访问是访问数据效率最低的方式,应仅在必要时使用。)请参阅查找其他实体的数据。
-
其他字段——您可以根据需要为您的结构声明其他字段。您可以在每次安排作业时设置此类字段的值。请参阅访问其他数据。
-
输出字段——除了更新作业中的可写实体组件或缓冲区外,您还可以写入为作业结构声明的本机容器字段。此类字段必须是本机容器,例如NativeArray;您不能使用其他数据类型。
访问实体组件和缓冲区数据
访问存储在查询中实体之一的组件中的数据是三步过程:
首先,您必须在作业结构上定义一个ComponentTypeHandle字段,将 T 设置为组件的数据类型。例如:
public ComponentTypeHandle<Translation> translationTypeHandle;
接下来,您在作业的Execute
方法中使用此句柄字段来访问包含该类型组件数据的数组(作为NativeArray)。此数组包含批处理中每个实体的一个元素:
NativeArray<Translation> translations =
batchInChunk.GetNativeArray(translationTypeHandle);
最后,当您安排作业时(在系统的OnUpdate方法中,您使用ComponentSystemBase.GetComponentTypeHandle函数为类型句柄字段赋值:
// "this" is your SystemBase subclass
updateFromVelocityJob.translationTypeHandle
= this.GetComponentTypeHandle<Translation>(false);
每次安排作业时,始终设置作业的组件句柄字段。不要缓存类型句柄并在以后使用它。
批处理中的每个组件数据数组都对齐,以便给定索引对应于所有数组中的相同实体。换句话说,如果您的作业使用一个实体的两个组件,请在两个数据数组中使用相同的数组索引来访问同一实体的数据。
您可以使用ComponentTypeHandle变量来访问未包含在EntityQuery中的组件类型。但是,您必须先检查以确保当前批次包含该组件,然后再尝试访问它。使用Has函数检查当前批次是否包含特定组件类型:
该ComponentTypeHandle字段是读书和就业数据写入时防止竞争条件ECS工作安全系统的一部分。始终设置GetComponentTypeHandle函数的isReadOnly
参数以准确反映在作业中访问组件的方式。
查找其他实体的数据
通过EntityQuery和IJobEntityBatch作业(或Entities.ForEach)访问组件数据几乎总是访问数据的最有效方式。但是,经常存在需要以随机访问方式查找数据的情况,例如,当一个实体依赖于另一个实体中的数据时。要执行这种类型的数据查找,您必须通过作业结构将不同类型的句柄传递给您的作业:
ComponentDataFromEntity -- 访问具有该组件类型的任何实体的组件
BufferFromEntity -- 访问具有该缓冲区类型的任何实体的缓冲区
这些类型为组件和缓冲区提供了一个类似数组的接口,由Entity对象索引。除了由于随机数据访问而相对低效之外,以这种方式查找数据还会增加您遇到工作安全系统建立的保护措施的机会。例如,如果您尝试根据另一个实体的转换设置一个实体的转换,则工作安全系统无法判断这是否安全,因为您可以通过ComponentDataFromEntity对象访问所有转换。您可能正在写入您正在阅读的相同数据,从而造成竞争条件。
要使用ComponentDataFromEntity和BufferFromEntity,请在作业结构上声明一个ComponentDataFromEntity或BufferFromEntity类型的字段,并在调度作业之前设置该字段的值。
有关详细信息,请参阅查找数据。
访问其他数据
如果在执行作业时需要其他信息,可以在作业结构体上定义一个字段,然后访问Execute
方法内部的字段。您只能在安排作业时设置该值,并且该值对于所有批次都保持不变。
例如,如果您正在更新移动对象,您很可能需要传入自上次更新以来经过的时间。为此,您可以定义一个名为 的字段DeltaTime
,设置其值OnUpdate
并在工作Execute
职能中使用该值。在每一帧中,DeltaTime
在为新帧安排作业之前,您将计算并为您的字段分配一个新值。
编写执行函数
编写Execute
作业结构的函数,将数据从其输入状态转换为所需的输出状态。
IJobEntityBatch.Execute方法的签名是:
void Execute(ArchetypeChunk batchInChunk, int batchIndex)
对于IJobEntityBatchWithIndex.Execute,签名是:
void Execute(ArchetypeChunk batchInChunk, int batchIndex, int indexOfFirstEntityInQuery)
batchInChunk 参数
该batchInChunk
参数提供ArchetypeChunk实例,该实例包含作业的此迭代的实体和组件。因为块只能包含一个原型,所以块中的所有实体都具有相同的组件集。默认情况下,该对象包含单个块中的所有实体;但是,如果您使用ScheduleParallel调度作业,则可以指定批处理仅包含块中实体数量的一小部分。
使用该batchInChunk
参数获取访问组件数据所需的NativeArray实例。(您还必须声明一个具有相应组件类型句柄的字段 - 并在安排作业时设置该字段。)
batchIndex 参数
该batchIndex
参数是为当前作业创建的所有批次列表中当前批次的索引。作业中的批次不一定按索引顺序处理。
batchIndex
如果您有一个本机容器,每个批次有一个元素,您想将在Execute
函数中计算出的值写入其中,则可以使用该值。将batchIndex
用作此容器的数组索引。
如果您使用并行写入实体命令缓冲区,请将batchIndex
参数作为sortKey
参数传递给命令缓冲区函数。
indexOfFirstEntityInQuery 参数
一个IJobEntityBatchWithIndex Execute
功能有一个名为附加参数indexofFirstEntityInQuery
。如果您将查询选择的实体描绘为单个列表,indexOfFirstEntityInQuery
则将是当前批次中第一个实体的该列表的索引。作业中的批次不一定按索引顺序处理。
可选组件
如果您的实体查询中有Any过滤器或完全可选的组件根本没有出现在查询中,您可以在使用它之前使用ArchetypeChunk.Has函数来测试当前块是否包含这些组件之一:
// If entity has Rotation and LocalToWorld components,
// slerp to align to the velocity vector
if (batchInChunk.Has<Rotation>(rotationTypeHandle) &&
batchInChunk.Has<LocalToWorld>(l2wTypeHandle))
{
NativeArray<Rotation> rotations
= batchInChunk.GetNativeArray(rotationTypeHandle);
NativeArray<LocalToWorld> transforms
= batchInChunk.GetNativeArray(l2wTypeHandle);
// By putting the loop inside the check for the
// optional components, we can check once per batch
// rather than once per entity.
for (int i = 0; i < batchInChunk.Count; i++)
{
float3 direction = math.normalize(velocityVectors[i].Value);
float3 up = transforms[i].Up;
quaternion rotation = rotations[i].Value;
quaternion look = quaternion.LookRotation(direction, up);
quaternion newRotation = math.slerp(rotation, look, DeltaTime);
rotations[i] = new Rotation() { Value = newRotation };
}
}
安排作业
要运行IJobEntityBatch作业,您必须创建作业结构的实例,设置结构字段,然后安排作业。当您在SystemBase实现的OnUpdate函数中执行此操作时,系统会将作业调度为每帧运行。
public class UpdateTranslationFromVelocitySystem : SystemBase
{
EntityQuery query;
protected override void OnCreate()
{
// Set up the query
var description = new EntityQueryDesc()
{
All = new ComponentType[]
{ComponentType.ReadWrite<Translation>(),
ComponentType.ReadOnly<VelocityVector>()}
};
query = this.GetEntityQuery(description);
}
protected override void OnUpdate()
{
// Instantiate the job struct
var updateFromVelocityJob
= new UpdateTranslationFromVelocityJob();
// Set the job component type handles
// "this" is your SystemBase subclass
updateFromVelocityJob.translationTypeHandle
= this.GetComponentTypeHandle<Translation>(false);
updateFromVelocityJob.velocityTypeHandle
= this.GetComponentTypeHandle<VelocityVector>(true);
// Set other data need in job, such as time
updateFromVelocityJob.DeltaTime = World.Time.DeltaTime;
// Schedule the job
this.Dependency
= updateFromVelocityJob.ScheduleParallel(query, 1, this.Dependency);
}
当您调用GetComponentTypeHandle函数来设置组件类型变量时,请确保isReadOnly
将作业读取但不写入的组件的参数设置为 true。正确设置这些参数会对 ECS 框架调度作业的效率产生重大影响。这些访问模式设置必须与 struct 定义和EntityQuery中的等效项匹配。
不要在系统类变量中缓存GetComponentTypeHandle的返回值。每次系统运行时都必须调用该函数,并将更新后的值传递给作业。
调度选项
您可以通过在安排作业时选择适当的功能来控制作业的执行方式:
-
运行——立即在当前(主)线程上执行作业。运行还会完成当前作业所依赖的任何计划作业。批处理大小始终为 1(整个块)。
-
调度——在当前作业所依赖的任何调度作业之后调度作业在工作线程上运行。为实体查询选择的每个块调用一次作业执行函数。块按顺序处理。批次大小始终为 1。
-
ScheduleParallel —— 与 Schedule 类似,不同之处在于您可以指定批处理大小并且批处理是并行处理的(假设工作线程可用)而不是顺序处理。
设置批量大小
要设置批量大小,请使用ScheduleParallel方法来安排作业并将batchesPerChunk
参数设置为正整数。使用值 1 将批处理大小设置为完整块。
用于调度作业的查询选择的每个块都分为由 指定的批次数batchesPerChunk
。来自同一块的每个批次包含大约相同数量的实体;然而,来自不同块的批次可能包含非常不同数量的实体。最大的批处理大小为 1,这意味着每个块中的所有实体都在对您的Execute
函数的一次调用中一起处理。来自不同块的实体永远不能包含在同一个批次中。
笔记
通常,使用batchesPerChunk
设置 1 在一次调用中处理块中的所有实体是最有效的Execute
。然而,情况并非总是如此。例如,如果您的Execute
函数执行的实体数量较少且算法开销很大,则可以通过使用较小批量的实体从并行处理中获得额外的好处。
跳过实体不变的块
如果您只需要在组件值更改时更新实体,您可以将该组件类型添加到为作业选择实体和块的EntityQuery的更改过滤器中。例如,如果您有一个系统读取两个组件,并且只需要在前两个组件之一发生更改时更新第三个,则可以使用EntityQuery如下:
EntityQuery query;
protected override void OnCreate()
{
query = GetEntityQuery(
new ComponentType[]
{
ComponentType.ReadOnly<InputA>(),
ComponentType.ReadOnly<InputB>(),
ComponentType.ReadWrite<Output>()
}
);
query.SetChangedVersionFilter(
new ComponentType[]
{
typeof(InputA),
typeof(InputB)
}
);
}
该EntityQuery改变滤光器最多可支持两个组成部分。如果您想检查更多或不使用EntityQuery,您可以手动进行检查。要进行此检查,请使用ArchetypeChunk.DidChange函数将组件的块更改版本与系统的LastSystemVersion 进行比较。如果此函数返回 false,则您可以完全跳过当前块,因为自上次系统运行以来,该类型的组件均未发生更改。
您必须使用结构字段将LastSystemVersion从系统传递到作业中,如下所示:
struct UpdateOnChangeJob : IJobEntityBatch
{
public ComponentTypeHandle<InputA> InputATypeHandle;
public ComponentTypeHandle<InputB> InputBTypeHandle;
[ReadOnly] public ComponentTypeHandle<Output> OutputTypeHandle;
public uint LastSystemVersion;
[BurstCompile]
public void Execute(ArchetypeChunk batchInChunk, int batchIndex)
{
var inputAChanged = batchInChunk.DidChange(InputATypeHandle, LastSystemVersion);
var inputBChanged = batchInChunk.DidChange(InputBTypeHandle, LastSystemVersion);
// If neither component changed, skip the current batch
if (!(inputAChanged || inputBChanged))
return;
var inputAs = batchInChunk.GetNativeArray(InputATypeHandle);
var inputBs = batchInChunk.GetNativeArray(InputBTypeHandle);
var outputs = batchInChunk.GetNativeArray(OutputTypeHandle);
for (var i = 0; i < outputs.Length; i++)
{
outputs[i] = new Output { Value = inputAs[i].Value + inputBs[i].Value };
}
}
}
与所有作业结构字段一样,您必须在安排作业之前分配其值:
public class UpdateDataOnChangeSystem : SystemBase {
EntityQuery query;
protected override void OnUpdate()
{
var job = new UpdateOnChangeJob();
job.LastSystemVersion = this.LastSystemVersion;
job.InputATypeHandle = GetComponentTypeHandle<InputA>(true);
job.InputBTypeHandle = GetComponentTypeHandle<InputB>(true);
job.OutputTypeHandle = GetComponentTypeHandle<Output>(false);
this.Dependency = job.ScheduleParallel(query, 1, this.Dependency);
}
protected override void OnCreate()
{
query = GetEntityQuery(
new ComponentType[]
{
ComponentType.ReadOnly<InputA>(),
ComponentType.ReadOnly<InputB>(),
ComponentType.ReadWrite<Output>()
}
);
}
}
笔记
为了效率,更改版本适用于整个块而不是单个实体。如果另一个能够写入该类型组件的作业访问了一个块,则 ECS 会增加该组件的更改版本,并且 DidChange 函数返回 true。即使声明对组件的写访问权限的作业实际上并未更改组件值,ECS 也会增加更改版本。(这是您在读取组件数据而不更新它时应该始终只读的原因之一。)