ECS框架文档翻译十四 Using IJobChunk

以下文档均来源于ECS官网:

https://docs.unity3d.com/Packages/com.unity.entities@0.0/manual/ecs_entities.html

使用IJobChunk

您可以在JobComponentSystem中实现IJobChunk,这种Job可以按块(chunk)来遍历您的数据。JobComponentSystem将为每个块调用一次Execute()函数,由于块中包含了所有您所希望系统处理的实体,此时您可以对块中所有实体逐个处理数据。

使用IJobChunk来遍历实体,比起使用IJobForEach来需要进行更多的代码设置,但其访问过程中可以对数据进行最直接的访问,可以直接修改其实际存储数据。

使用块遍历的另一个好处是,您可以检查某个可选组件在块中是否存在(使用Archetype.Has方法),并相应地处理块中的所有实体。

实现IJobChunk Job的步骤包括:

  1. 通过创建EntityQuery来标识要处理的实体。
  2. 定义Job结构体,包括ArchetypeChunkComponentType对象的字段,以标识Job直接访问的组件类型,并指定Job是读取还是写入这些组件。
  3. 实例化Job结构体并在系统OnUpdate()函数中调度此Job。
  4. 在Execute()函数中,获取Job读取或写入的组件的NativeArray实例,最后,遍历当前块以执行你所需的工作。

ECS samples repository 示例包含一个简单的HelloCube示例,演示如何使用IJobChunk。

使用EntityQuery来查询数据

EntityQuery定义了实体原型必须包含的一系列的组件类型,以便系统来过滤与其相关联的块和实体{译者注:通过实体原型上的组件来过滤实体原型,根据过滤后的原型找出所有实体}。 实体原型也可以有其他组件,但它必须至少包含EntityQuery中定义的组件。 你还可以排除包含特定类型组件的原型。

对于简单的查询来说,可以使用JobComponentSystem.GetEntityQuery()函数,传入组件类型,如下所示:

public class RotationSpeedSystem : JobComponentSystem
{
   private EntityQuery m_Group;
   protected override void OnCreate()
   {
       m_Group = GetEntityQuery(typeof(RotationQuaternion), ComponentType.ReadOnly<RotationSpeed>());
   }
   //…
}

对于更复杂的情况,你可以使用EntityQueryDesc。提供了一个灵活的查询机制来指定组件类型。

  • All = 原型中存必须包含该数组中指定的所有的组件类型
  • Any = 原型中至少包含一个该数组指定的组件类型
  • None = 原型中存不能包含该数组中指定的任意的组件类型

例如,以下的查询结果包含这样的原型-它们的组件中包含RotationQuaternion和RotationSpeed组件,但是不包含Frozen组件

protected override void OnCreate()
{
   var query = new EntityQueryDesc
   {
       None = new ComponentType[]{ typeof(Frozen) },
       All = new ComponentType[]{ typeof(RotationQuaternion), ComponentType.ReadOnly<RotationSpeed>() }
   };
   m_Group = GetEntityQuery(query);
}

这里的请求使用ComponentType.ReadOnly<T>使用而不是简单的typeof,标志了该系统不会写入RotationSpeed。

您还可以通过传递EntityQueryDesc对象数组,而不是单个实例来进行组合查询。 每个查询间使用逻辑或运算来进行组合。 以下示例筛选出包含RotationQuaternion组件或RotationSpeed组件(或包含两者)的原型:

protected override void OnCreate()
{
   var query0 = new EntityQueryDesc
   {
       All = new ComponentType[] {typeof(RotationQuaternion)}
   };

   var query1 = new EntityQueryDesc
   {
       All = new ComponentType[] {typeof(RotationSpeed)}
   };

   m_Group = GetEntityQuery(new EntityQueryDesc[] {query0, query1});
}

注意: 不要在EntityQueryDesc中包含完全可选的组件{译者注:完全可选组件指那些未出现在查询条件中,筛选后的实体却会包含的组件}。 要处理可选组件,请使用IJobChunk.Execute()中的chunk.Has<T>() 方法来确定当前ArchetypeChunk是否具有可选组件。 由于同一块中的所有实体具有相同的组件,因此您只需要检查每个块是否存在一个可选组件 - 而不需要每个实体检查一次。

为了提高效率,同时避免不必要地创建引用类型(会造成垃圾收集),您应该在系统的OnCreate()函数中为系统创建EntityQuerie,并将结果存储在成员变量中(在上面的示例中,m_Group变量用于此目的)。

## 定义 IJobChunk 结构

IJobChunk结构定义了Job运行时所需数据的字段,以及Job的Execute()方法。

为了访问那些由系统传递给Execute()方法的,块内部的组件数组,你需要为Job读取或写入的每种类型的组件创建一个ArchetypeChunkComponentType对象。 这些对象允许您获取那些可以访问实体组件的NativeArrays实例,其中包含了Job的EntityQuery中筛选的,将被Execute方法读取或写入的所有组件。 您还可以为EntityQuery中未包含的可选组件类型提供ArchetypeChunkComponentType变量。 (在尝试访问之前,必须检查以确保当前块存在可选组件)

例如,HelloCube IJobChunk 示例声明了一个Job结构,它为RotationQuaternion和RotationSpeed两个组件定义了ArchetypeChunkComponentType变量:

[BurstCompile]
struct RotationSpeedJob : IJobChunk
{
   public float DeltaTime;
   public ArchetypeChunkComponentType<RotationQuaternion> RotationType;
   [ReadOnly] public ArchetypeChunkComponentType<RotationSpeed> RotationSpeedType;

   public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
   {
       //...
   }
}

在System的OnUpdate() 函数中为这些变量赋值。 当ECS框架运行Job时,在Execute()方法内部可以使用这些变量。此Job使用了Unity的delta time来操作3D对象的旋转运动。本例也是通过一个结构体的成员变量来将此值转递给Execute方法。
{译者注:这里指ArchetypeChunkComponentType对象,在系统中使用诸如:GetArchetypeChunkComponentType<Rotation>()的方法来获取,并传递给Job结构内的成员变量}。

## Execute函数的写法

IJobChunk 的Execute方法参数的声明如下:

 public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)

chunk 参数是一整块内存的句柄,其中包含了将会在Job中被遍历的实体和组件。由于块只包含一个对象原型,所以块中的所有的实体都拥有相同的组件集。

使用chunk参数来获取你指定的组件数组NativeArray:

var chunkRotations = chunk.GetNativeArray(RotationType);
var chunkRotationSpeeds = chunk.GetNativeArray(RotationSpeedType);

这些组件数组是按实体对齐的,使得任意一个实体在所有的组件数组中都具有相同的索引。 然后,您可以使用正常的for循环遍历组件数组,使用 chunk.Countt获取当前块中存储的实体数:

for (var i = 0; i < chunk.Count; i++)
{
   var rotation = chunkRotations[i];
   var rotationSpeed = chunkRotationSpeeds[i];

   // Rotate something about its up vector at the speed given by RotationSpeed.
   chunkRotations[i] = new RotationQuaternion
   {
       Value = math.mul(math.normalize(rotation.Value),
           quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * DeltaTime))
   };
}

如果你的EntityQueryDesc 中存在Any筛选器,或存在没有出现在查询条件中的完全可选组件,在使用它们之前,你可以使用ArchetypeChunk.Has<T>函数来测试当前chunk是否包含他们:

if (chunk.Has<OptionalComp>(OptionalCompType))
{//...}

注意: 如果你使用的是并发的实体命令缓存,你需要将chunkIndex参数作为jobIndex参数传入到命令缓存函数中{译者注:从这里也说明了,IJobChunk本身是并行运行的,当一个EntityQueryDesc查询存在多个结果(每个结果只能包含一个chunk以及其内部的一种实体原型)时,这些结果将会被并行运行,此时的并发实体命令缓存需要依赖chunkIndex作为jobIndex,来保证先后执行顺序的唯一性}。

跳过那些实体没有发生过变化的块

如果你只是需要在某类组件的值发生变化时才去更新实体,你可以将该组件类型添加到EntityQuery的更改筛选器(change filter)中,该筛选器将为Job来筛选实体和块。 例如,如果您有一个系统,它读取两个组件,并且只需要在这两个组件中任意一个发生更改时,才会去更新第三个组件,则可以以下的方式来使用EntityQuery:

EntityQuery m_Group;
protected override void OnCreate()
{
   m_Group = GetEntityQuery(typeof(Output), 
                               ComponentType.ReadOnly<InputA>(), 
                               ComponentType.ReadOnly<InputB>());
   m_Group.SetFilterChanged(new ComponentType{ typeof(InputA), typeof(InputB)});
}

EntityQuery更改筛选器最多支持两个组件。 如果需要支持更多,或你未使用EntityQuery,则可以手动进行检查。 方法是,使用ArchetypeChunk.DidChange() 函数将组件所在块的更改版本与系统的LastSystemVersion 进行比较,如果此函数返回false,则可以完全跳过当前块,因为自上次系统运行以来,该类型的所有组件都没有更改。

来自System的LastSystemVersion需要传入到Job结构体中的成员变量:

[BurstCompile]
struct UpdateJob : IJobChunk
{
   public ArchetypeChunkComponentType<InputA> InputAType;
   public ArchetypeChunkComponentType<InputB> InputBType;
   [ReadOnly] public ArchetypeChunkComponentType<Output> OutputType;
   public uint LastSystemVersion;

   public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
   {
       var inputAChanged = chunk.DidChange(InputAType, LastSystemVersion);
       var inputBChanged = chunk.DidChange(InputBType, LastSystemVersion);
       if (!(inputAChanged || inputBChanged))
           return;
      //...
}

与所有Job结构字段一样,您必须在调度Job之前为其赋值:

   var job = new UpdateJob()
   {
        LastSystemVersion = this.LastSystemVersion,
        //… initialize other fields
   }

请注意,为了提高效率,更改版本被应用于整个块而不是单个实体。 如果某个块已被另一个能够写入该类型组件的Job访问,则该组件的更改版本将递增,并且DidChange()函数返回true。{译者注:是不是所有Chunk的组件分组对应的更改版本都会在帧Tick的初始被统一赋值为该系统的LastSystemVersion【?】}

初始化和调度Job

要运行IJobChunk作业,您需要创建该Job结构的实例,设置结构字段,然后调度该作业。 在JobComponentSystem的OnUpdate()函数中执行此操作时,该Job将在每帧中被系统调度运行。

// OnUpdate runs on the main thread.
protected override JobHandle OnUpdate(JobHandle inputDependencies)
{           
   var job = new RotationSpeedJob()
   {
       RotationType = GetArchetypeChunkComponentType<RotationQuaternion>(false),
       RotationSpeedType = GetArchetypeChunkComponentType<RotationSpeed>(true),
       DeltaTime = Time.deltaTime
   };

   return job.Schedule(m_Group, inputDependencies);
}

当您调用GetArchetypeChunkComponentType函数来设置组件类型变量时,请确保为作业读取但不写入的那些组件设置isReadOnly参数为true。 正确设置这些参数将会对ECS框架调度作业的效率产生重大影响。 这些访问模式的设置必须与结构中泛型参数的定义,以及EntityQuery中的设置相匹配。

不要将GetArchetypeChunkComponentType的返回值缓存在System类变量中。 每次系统运行时都必须调用该函数,并将更新后的值传递给Job。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值