Using Entity Batch jobs(使用实体批处理作业)
在系统中实现IJobEntityBatch或IJobEntityBatchWithIndex,以分批迭代实体的数据。
当你在System的OnUpdate函数中调度一个IJobEntityBatch作业时,System会使用你传递给调度函数的EntityQuery来识别应该传递给作业的块。该作业对这些块中的每一批实体调用你的Execute函数一次。默认情况下,批次大小是一个完整的块,但你可以在调度作业时将批次大小设置为一个块的某个分数。无论批次大小如何,给定批次中的实体总是存储在同一个块中。在作业的Execute函数中,你可以逐个实体遍历每个批次中的数据。
当你需要一个跨批次集合的所有实体的索引值时,使用IJobEntityBatchWithIndex。否则,IJobEntityBatch会更有效率,因为它不需要计算这些索引。
要实现一个批处理Job:
-
用EntityQuery查询数据,以确定你要处理的实体。
-
使用 IJobEntityBatch 或 IJobEntityBatchWithIndex 定义Job结构。
-
声明你的Job所访问的数据。在作业结构中,包括ComponentTypeHandle对象的字段,以确定Job必须直接访问的组件类型。同时,指定Job是否读取或写入这些组件。你也可以包括一些字段,这些字段标识了你想为不属于查询的实体查找的数据,以及非实体数据的字段。
-
编写Job结构的Execute函数来转换你的数据。获取Job读取或写入的组件的NativeArray实例,然后在当前批次上迭代,执行所需的工作。
-
在System的OnUpdate函数中安排工作,将识别要处理的实体的实体查询传递给schedule函数。
NOTE
使用IJobEntityBatch或IJobEntityBatchWithIndex进行迭代比使用Entities.ForEach更复杂,需要更多的代码设置,只应在必要时或更有效时使用。
欲了解更多信息,ECS Sample 库包含一个简单的HelloCube例子,演示了如何使用IJobEntityBatch。
NOTE
IJobEntityBatch取代了IJobChunk。主要区别在于,你可以安排一个IJobEntityBatch来迭代比一个完整的大块更小的实体批次,如果你需要为每个批次中的实体建立一个工作范围的索引,你可以使用变体IJobEntityBatchWithIndex。
Query for data with an EntityQuery(用EntityQuery来查询数据)
EntityQuery定义了EntityArchetype必须包含的组件类型集,以便系统处理其相关的块和实体。一个原型可以有额外的组件,但它必须至少有查询所定义的那些组件。你也可以排除那些包含特定类型组件的原型。
将选择你的Job应该处理的实体的查询传递给你用来安排Job的schedule方法。
关于定义查询的信息,请参见使用EntityQuery来查询数据。
NOTE
不要在EntityQuery中包含完全可选的组件。为了处理可选组件,使用IJobEntityBatch.Execute里面的ArchetypeChunk.Has方法来确定当前ArchetypeChunk是否有可选组件。因为同一批次中的所有实体都有相同的组件,所以你只需要在每个批次中检查一次可选组件是否存在,而不是每个实体一次。
Define the job struct(定义工作结构)
一个Job结构由执行函数和字段组成,执行函数完成要执行的工作,而字段则声明执行函数所使用的数据。
一个典型的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的数据,并根据上次更新后的时间计算出一个新的Translation。
IJobEntityBatch与IJobEntityBatchWithIndex的比较
IJobEntityBatch和IJobEntityBatchWithIndex的唯一区别是,IJobEntityBatchWithIndex在对一个批次调用Execute函数时,会传递一个indexOfFirstEntityInQuery参数。这个参数是实体查询选择的所有实体列表中当前批次中第一个实体的索引。
当你需要每个实体的单独索引时,请使用IJobEntityBatchWithIndex。例如,如果你为每个实体计算一个唯一的结果,你可以使用这个索引将每个结果写入一个本地数组的不同元素中。如果你不使用indexOfFirstEntityInQuery的值,请使用IJobEntityBatch代替,以避免计算索引值的开销。
NOTE
当你向[EntityCommandBuffer.ParallelWriter]添加命令时,你可以使用 batchIndex 参数作为命令缓冲区函数的 sortKey 参数。你不需要使用IJobEntityBatchWithIndex只是为了获得每个实体的唯一排序键。两种Job类型中的batchIndex参数都可用于此目的。
Declare the data your job accesses(声明你的Job所访问的数据)
Job结构中的字段声明了执行函数可用的数据。这些字段一般分为四类。
-
ComponentTypeHandle字段 – 组件处理字段允许你的执行函数访问存储在当前块中的实体组件和缓冲区。参见访问实体组件和缓冲区数据。
-
ComponentDataFromEntity, BufferFromEntity字段 – 这些 "来自实体的数据 "字段允许你的执行函数查找任何实体的数据,不管它存储在哪里。(这种类型的随机访问是访问数据效率最低的方式,应该只在必要时使用)。参见为其他实体查询数据。
-
Other fields(其他字段) – 您可以根据需要为您的结构声明其他字段。您可以在每次安排Job时设置此类字段的值。参见访问其他数据。
-
Output fields – 除了更新作业中的可写实体组件或缓冲区外,你还可以写到为Job结构声明的本地容器字段。这些字段必须是Native container,例如NativeArray;你不能使用其他数据类型。
访问实体组件和缓冲区数据
访问存储在查询中的一个实体的组件中的数据是一个三步的过程:
首先,你必须在Job结构上定义一个ComponentTypeHandle字段,将T设置为组件的数据类型。比如说。
public ComponentTypeHandle<Translation> translationTypeHandle;
接下来,你在Job的执行方法中使用这个句柄字段来访问包含该类型组件数据的数组(作为一个NativeArray)。这个数组包含了一个批处理中每个实体的元素。
NativeArray<Translation> translations =
batchInChunk.GetNativeArray(translationTypeHandle);
最后,当你安排Job时,在System的OnUpdate方法中,你使用ComponentSystemBase.GetComponentTypeHandle函数为类型处理字段赋值:
// "this" is your SystemBase subclass
updateFromVelocityJob.translationTypeHandle
= this.GetComponentTypeHandle<Translation>(false);
每次安排Job时,总是设置Job的组件句柄字段。不要缓存一个类型的句柄并在以后使用它。
批次中的每个组件数据数组都是对齐的,这样一个给定的索引在所有数组中对应于同一个实体。换句话说,如果你的Job使用一个实体的两个组件,在两个数据数组中使用相同的数组索引来访问同一实体的数据。
你可以使用ComponentTypeHandle变量来访问你不包括在EntityQuery中的组件类型。然而,在你试图访问它之前,你必须检查以确保当前批次包含该组件。使用Has函数来检查当前批次是否包含一个特定的组件类型。
ComponentTypeHandle字段是ECS作业安全系统的一部分,可以防止在Job中读写数据时出现竞争条件。始终设置GetComponentTypeHandle函数的isReadOnly参数,以准确反映Job中对组件的访问方式。
Looking up data for other entities(查阅其他实体的数据)
通过EntityQuery和IJobEntityBatch作业(或Entities.ForEach)访问组件数据几乎总是访问数据的最有效方式。然而,经常会有这样的情况,你需要以随机访问的方式查询数据,例如,当一个实体依赖于另一个实体的数据时。为了执行这种类型的数据查询,你必须通过Job结构向你的Job传递不同类型的句柄。
ComponentDataFromEntity – 访问具有该组件类型的任何实体的组件
BufferFromEntity – 访问具有该缓冲区类型的任何实体的缓冲区
这些类型为组件和缓冲区提供了一个类似数组的接口,按实体对象进行索引。除了因为随机数据访问而相对低效外,这样查找数据也会增加你遇到Job安全系统所建立的保障措施的机会。例如,如果你试图根据另一个实体的变换来设置一个实体的变换,Job安全系统无法判断这是否安全,因为你可以通过ComponentDataFromEntity对象访问所有变换。你可能会对你正在读取的数据进行写入,因此会产生一个竞争条件。
要使用ComponentDataFromEntity和BufferFromEntity,请在Job结构上声明一个ComponentDataFromEntity或BufferFromEntity类型的字段,并在调度作业之前设置该字段的值。
欲了解更多信息,请参见查询数据。
Accessing other data(访问其他数据)
如果你在执行Job时需要其他信息,你可以在Job结构中定义一个字段,然后在执行方法中访问该字段。你只能在调度Job时设置该值,并且该值对所有batches都保持不变。
例如,如果你正在更新移动的对象,你很可能需要传入自上一次更新以来所经过的时间。要做到这一点,你可以定义一个名为DeltaTime的字段,在OnUpdate中设置其值,并在Job执行函数中使用该值。在每一帧,你将计算并分配一个新的值给DeltaTime字段,然后为新的一帧调度Job。
Write the Execute function(编写 "Execute"函数)
编写Job结构的Execute函数,将数据从输入状态转化为期望的输出状态。
IJobEntityBatch.Execute方法的签名是。
void Execute(ArchetypeChunk batchInChunk, int batchIndex)
而对于IJobEntityBatchWithIndex.Execute,签名是:
void Execute(ArchetypeChunk batchInChunk, int batchIndex, int indexOfFirstEntityInQuery)
batchInChunk参数
batchInChunk参数提供了ArchetypeChunk实例,该实例包含Job的这次迭代的实体和组件。因为一个chunk只能包含一个原型,所以一个chunk中的所有实体都有相同的组件集。默认情况下,这个对象包括单个chunk中的所有实体;但是,如果你用ScheduleParallel调度作业,你可以指定一个批次只包含chunk中实体数量的一小部分。
使用batchInChunk参数来获取你需要的NativeArray实例来访问组件数据。(你还必须用相应的组件类型句柄声明一个字段–并在调度作业时设置该字段)。
batchIndex参数
batchIndex参数是当前批次在为当前Job创建的所有批次列表中的索引。工作中的批次不一定按照索引顺序处理。
你可以在这样的情况下使用 batchIndex 值:你有一个本地容器,每个批次有一个元素,你想把执行函数中计算的值写到这个容器中。使用 batchIndex 作为这个容器的数组索引。
如果你使用parallel writing实体的命令缓冲区,把batchIndex参数作为sortKey参数传给命令缓冲区函数。
indexOfFirstEntityInQuery参数
IJobEntityBatchWithIndex Execute函数有一个额外的参数,名为indexofFirstEntityInQuery。如果你把你的查询所选择的实体作为一个单一的列表,indexOfFirstEntityInQuery将是当前批次中第一个实体在该列表中的索引。Job中的批次不一定按照索引的顺序来处理。
Optional components(可选组件)
如果你的实体查询中有Any过滤器,或者有完全可选的组件,根本不会出现在查询中,你可以使用ArchetypeChunk.Has函数来测试当前的chunk是否包含这些组件之一,然后再使用它:
// 如果实体有Rotation和LocalToWorld组件,则滑动以对准速度矢量。
if (batchInChunk.Has<Rotation>(rotationTypeHandle) &&
batchInChunk.Has<LocalToWorld>(l2wTypeHandle))
{
NativeArray<Rotation> rotations
= batchInChunk.GetNativeArray(rotationTypeHandle);
NativeArray<LocalToWorld> transforms
= batchInChunk.GetNativeArray(l2wTypeHandle);
// 通过把循环放在可选组件的检查中,
// 我们可以每批检查一次,而不是每个实体检查一次。
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 };
}
}
Schedule the job(安排Job)
要运行 IJobEntityBatch Job,您必须创建Job结构的实例,设置结构字段,然后安排该Job。当您在SystemBase实现的OnUpdate函数中这样做时,系统会将Job安排在每一帧运行。
public class UpdateTranslationFromVelocitySystem : SystemBase
{
EntityQuery query;
protected override void OnCreate()
{
// 设置查询
var description = new EntityQueryDesc()
{
All = new ComponentType[]
{ComponentType.ReadWrite<Translation>(),
ComponentType.ReadOnly<VelocityVector>()}
};
query = this.GetEntityQuery(description);
}
protected override void OnUpdate()
{
// 实例化Job结构
var updateFromVelocityJob
= new UpdateTranslationFromVelocityJob();
// 设置Job组件类型句柄
updateFromVelocityJob.translationTypeHandle
= this.GetComponentTypeHandle<Translation>(false);
updateFromVelocityJob.velocityTypeHandle
= this.GetComponentTypeHandle<VelocityVector>(true);
// 设置Job中需要的其他数据,如时间
updateFromVelocityJob.DeltaTime = World.Time.DeltaTime;
// 安排Job
this.Dependency
= updateFromVelocityJob.ScheduleParallel(query, 1, this.Dependency);
}
当你调用GetComponentTypeHandle函数来设置你的组件类型变量时,确保你将Job读取但不写入的组件的isReadOnly参数设置为true。正确设置这些参数会对ECS框架如何有效地安排你的作业产生重大影响。这些访问模式的设置必须与结构定义和EntityQuery中的等价物相匹配。
不要将GetComponentTypeHandle的返回值缓存在系统类变量中。你必须在每次系统运行时调用该函数,并将更新的值传递给Job。
Scheduling options
你可以在安排Job时选择适当的功能来控制Job的执行方式:
-
Run – 在当前(主)线程上立即执行该作业。运行也会完成当前Job所依赖的任何预定作业。批量大小总是1(一整块)。
-
Schedule – 将Job安排在一个工作线程上,在当前作业所依赖的任何预定作业之后运行。对于实体查询所选择的每个块,作业执行函数被调用一次。块是按顺序处理的。批量大小始终为1。
-
ScheduleParallel – 和Schedule一样,只是你可以指定一个批处理的大小,并且批处理是并行的(假设有工作线程),而不是顺序的。
Setting the batch size(设置批大小)
要设置批大小,使用ScheduleParallel方法来安排Job,并将batchesPerChunk参数设置为一个正整数。使用1的值来设置批次大小为一个完整的块。
用于安排Job的查询所选择的每个块都被划分为由batchesPerChunk指定的批次数量。来自同一大块的每个批次包含的实体数量大致相同;但是,来自不同大块的批次可能包含非常不同的实体数量。最大的批处理量是1,这意味着每个块中的所有实体在一次调用你的Execute函数时被一起处理。来自不同块的实体永远不会被包含在同一个批次中。
NOTE
通常情况下,使用batchesPerChunk设置为1,在一次调用Execute时处理一个块中的所有实体,是最有效的。然而,情况并非总是如此。例如,如果你有少量的实体和一个由你的Execute函数执行的昂贵的算法,你可以通过使用较小的实体批次从并行处理中获得额外的好处。
Skipping chunks with unchanged entities(跳过没有变化的实体的块状物)
如果你只需要在某个组件的值发生变化时更新实体,你可以将该组件类型添加到EntityQuery的变化过滤器中,该过滤器为该Job选择实体和块。例如,如果你有一个读取两个组件的系统,并且只需要在前两个组件中的一个发生变化时更新第三个组件,你可以使用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函数来比较该组件的chunk的变化版本和系统的LastSystemVersion。如果这个函数返回false,你可以完全跳过当前的块,因为自从上次系统运行以来,该类型的组件都没有改变。
你必须使用一个结构字段,将LastSystemVersion从系统中传入Job中,如下所示:
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 };
}
}
}
与所有Job结构字段一样,您必须在安排Job前指定其值。
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>()
}
);
}
}
NOTE
为了提高效率,更改版本适用于整个块而不是单个实体。如果另一个有能力写入该类型组件的工作访问了一个块,那么ECS就会增加该组件的变化版本,并且DidChange函数返回true。即使声明对某一组件进行写入访问的工作实际上并没有改变该组件的值,ECS也会增加改变的版本。这也是你在读取组件数据而不是更新它时,应该始终只读的原因之一)。