来源:https://docs.unity3d.com/Packages/com.unity.entities@0.0/manual/index.html
我会对官方文档内容略作整理,有需要可以查看官方文档
3、IJobChunk
你可以通过在JobComponentSystem中实现IJobChunk来对区块中的数据进行迭代,JobComponentSystem会一个个的对每一个你希望系统去执行的实体所在的区块执行Execute()函数,然后,你可以逐个实体处理每个区块内的数据
使用IJobChunk进行迭代需要更多(相较于IJobForEach而言)的设置,但同时也更加明确,并且是对数据最直接的访问(因为访问的就是内存块)
使用IJobChunk的另一个好处是,您可以检查每个区块中是否存在可选组件(是原型所具有的组件),然后相应处理区块中所有的实体
实现IJobChunk作业需要的步骤包括:
- 通过创建EntityQuery来标识要处理的实体
- 定义Job结构,包括ArchetypeChunkComponentType对象的字段——以标识Job需要直接访问的组件类型并指定Job是读取还是写入这些组件
- 实例化Job结构并在系统OnUpdate()函数中调度Job
- 在Execute()函数中,获取Job读取或写入的组件的NativeArray,最后迭代当前块以执行所需的工作
可以参考ECS Sample中的HelloCube例子
通过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数组而不是单个来组合查询,使用or运算组合每个查询,以下代码查询了包含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是否具有可选组件,由于同一区块中的所有实体具有相同的组件,因此只需要检查每个块是否存在可选组件——而不是检查每个实体
为了提高效率并避免不必要GC,应该在OnCreate()函数中为系统创建EntityQueries,并将结果存储在变量中
定义IJobChunk结构
IJobChunk结构定义了Job运行时所需数据的字段,以及Job的Execute()方法
要访问系统传递给Execute()方法的块内部的组件数组,必须对需要读取或写入的每种类型组件的对象创建ArchetypeChunkComponentType,这些对象允许您获取NativeArrays的实例,从而提供对实体组件的访问,这包含了Execute函数所读写的在Job的实体队列中的所有组件。还可以为EntityQuery中未包含可选组件类型提供ArchetypeChunkComponentType变量。(在尝试访问之前,必须检查以确保当前块具有可选组件)
例如,在HelloCube的IJobChunk示例声明了一个定义ArchetypeChunkComponentType的两个组件变量——RotationQuaternion和RotationSpeed:
[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)
{
//...
}
}
系统在OnUpdate()函数中为这些变量赋值,当ECS框架运行Job时,变量在Execute()方法内部使用
Job还使用Unity delta时间为3D对象的旋转设置动画,该示例还使用struct字段将此值传递给Execute方法
编写Execute方法
IJobChunk Execute()方法的写法是:
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
该chunk参数是包含要在此Job迭代中处理的实体和组件的内存块的句柄,由于chunk只能包含单个原型,因此块中的所有实体都具有相同的组件
使用chunk参数获取组件的NativeArray实例:
var chunkRotations = chunk.GetNativeArray(RotationType);
var chunkRotationSpeeds = chunk.GetNativeArray(RotationSpeedType);
这些数组都是对齐的,因此每个实体在不同数组中有着相同的索引,也就可以正常使用for循环遍历组件数组,使用chunk.Count得到存储在当前块的实体的数量:
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 >函数测试当前块是否包含其中一个组件
if (chunk.Has<OptionalComp>(OptionalCompType))
{
//...
}
注意: 如果使用并发实体命令缓冲区,请将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,则可以跳过当前块,因为自上次系统运行以来,该类型的所有组件都没有更改
系统中的LastSystemVersion必须使用struct传递给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
实例化并安排Job
要运行IJobChunk的Job,必须创建Job结构体,设置结构体的字段,然后安排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函数来设置组件类型变量时,确保作业所读取的组件是只读的,正确设置这些参数会对ECS框架调度作业的效率产生重大影响,这些访问模式设置必须使结构体定义和EntityQuery中的设置相匹配
不要缓存GetArchetypeChunkComponentType在系统类变量中的返回值,因为系统每次运行时都必须调用该函数,并将更新后的值传递给Job