ECS框架文档翻译十四 Using IJobChunk

    xiaoxiao2025-09-01  12

    以下文档均来源于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的步骤包括:

    通过创建EntityQuery来标识要处理的实体。定义Job结构体,包括ArchetypeChunkComponentType对象的字段,以标识Job直接访问的组件类型,并指定Job是读取还是写入这些组件。实例化Job结构体并在系统OnUpdate()函数中调度此Job。在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)