以下文档均来源于ECS官网:
https://docs.unity3d.com/Packages/com.unity.entities@0.0/manual/ecs_entities.html
你可以在JobComponentSystem中定义IJobForEach
作业以读写组件数据。当此Job运行时,ECS框架会找出具有所需组件的所有实体,并为每个实体调用Job的Execute()方法。数据将按照其在内存中布局的顺序进行处理,并且Job会并行运行,因此IJobForEach既简单又高效。
以下示例介绍了一个简单的使用IJobForEach的系统。 Job读取RotationSpeed
component 组件数据并向RotationQuaternion组件写入数据。
public class RotationSpeedSystem : JobComponentSystem
{
// Use the [BurstCompile] attribute to compile a job with Burst.
[BurstCompile]
struct RotationSpeedJob : IJobForEach<RotationQuaternion, RotationSpeed>
{
public float DeltaTime;
// The [ReadOnly] attribute tells the job scheduler that this job will not write to rotSpeed
public void Execute(ref RotationQuaternion rotationQuaternion, [ReadOnly] ref RotationSpeed rotSpeed)
{
// Rotate something about its up vector at the speed given by RotationSpeed.
rotationQuaternion.Value = math.mul(math.normalize(rotationQuaternion.Value), quaternion.AxisAngle(math.up(), rotSpeed.RadiansPerSecond * DeltaTime));
}
}
// OnUpdate runs on the main thread.
// Any previously scheduled jobs reading/writing from Rotation or writing to RotationSpeed
// will automatically be included in the inputDependencies.
protected override JobHandle OnUpdate(JobHandle inputDependencies)
{
var job = new RotationSpeedJob()
{
DeltaTime = Time.deltaTime
};
return job.Schedule(this, inputDependencies);
}
}
注意: 以上的系统是基于 ECS Samples repository中的例子HelloCube IJobForEach.
定义 IJobForEach 的泛型参数
IJobForEach
结构体的泛型参数标识了你的系统将操作哪些组件:
struct RotationSpeedJob : IJobForEach<RotationQuaternion, RotationSpeed>
通过使用以下属性,你可以修改此Job对实体的筛选:
- [ExcludeComponent(typeof(T)] — 排除那些原型中包含了T类型组件的实体
- [RequireComponentTag(typeof(T)] — 只包含那些原型中含有T类型组件的实体。当需要保持某组件与实体的关联,却又需要避免读写它时,可以使用这个属性
例如,以下的Job定义选择了那些原型中包含Gravity、RotationQuaternion、RotationSpeed 组件,但是不含有Frozen component组件的实体:
[ExcludeComponent(typeof(Frozen))]
[RequireComponentTag(typeof(Gravity))]
[BurstCompile]
struct RotationSpeedJob : IJobForEach<RotationQuaternion, RotationSpeed>
如果你需要更复杂的请求来选择操作实体,你可以使用IJobChunk作业而不是IJobForEach。
Execute() 方法的书写
JobComponentSystem 将会对连续的实体逐个调用 Execute()方法,传入你在泛型参数中定义的那些组件。也就是说,你的Execute()方法的参数应该与本Job结构体中定义的泛型参数相匹配。
例如,写作以下的 Execute()方法,它保持了与结构体定义的泛型参数的匹配,并且声明了参数属性:读取RotationSpeed组件,读写 RotationQuaternion组件。(Read/write是默认的,所以不需要声明属性)
public void Execute(ref RotationQuaternion rotationQuaternion, [ReadOnly] ref RotationSpeed rotSpeed){}
你可以增加以下的属性,来帮助ECS优化你的系统:
- [ReadOnly] — 此组件数据在本方法中只读不写。
- [WriteOnly] — 此组件数据在本方法中只写不读。
- [ChangeFilter] — 只有当此组件数据发生变化时(从上一次本系统运行以来),才会执行此方法
声明为只读或者只写组件,将让Job更加高效地被调度执行。例如,当一个Job正在读取某个组件时,调度器不会安排另一个Job去写这个组件,但是当两个Job都只是在读取某个组件时,它们可以并行执行。
注意,为了考虑性能,ChangeFilter只在整个实体块(Chunk)上运行,它不会追踪单个实体。如果一个实体块被一个可以针对某类组件写入的Job访问,那么ECS框架将会认为整个的Chunk,包含在整个块总的所有实体,都已经发生了变化,否则,ECS框架将会把整个块中的实体都排除在外{译者注:即认为整个实体块都未发生变化}。
使用IJobForEachWithEntity
实现了IJobForEachWithEntity接口的Job,与实现了IJobForEach接口的Job的行为非常相似。 不同之处在于,IJobForEachWithEntity中的Execute()函数参数为您提供了当前处理的实体对象,以及位于展开的、并行的组件数组中的索引。
使用Entity参数
你可以使用此实体对象来向EntityCommandBuffer增加实体操作命令。例如,你可以增加命令,来向该实体增加或者删除组件,甚至删除该实体— 为了避免竞争条件,所有的命令都不会在该Job内立刻执行。命令缓存将允许您在作业线程上执行那些代价可能很高昂的计算,它们将实际插入和删除操作用队列缓存起来,而后在主线程上执行。
以下的系统,源于例子 HelloCube SpawnFromEntity,在Job中,在计算实体的位置之后,它使用一个命令缓存来将结果应用到实体:
public class SpawnerSystem : JobComponentSystem
{
// EndFrameBarrier 提供了 CommandBuffer
EndFrameBarrier m_EndFrameBarrier;
protected override void OnCreate()
{
// 将EndFrameBarrier引用存储在内部, 这样我们不需要每帧再去获取
m_EndFrameBarrier = World.GetOrCreateSystem<EndFrameBarrier>();
}
struct SpawnJob : IJobForEachWithEntity<Spawner, LocalToWorld>
{
public EntityCommandBuffer CommandBuffer;
public void Execute(Entity entity, int index, [ReadOnly] ref Spawner spawner,
[ReadOnly] ref LocalToWorld location)
{
for (int x = 0; x < spawner.CountX; x++)
{
for (int y = 0; y < spawner.CountY; y++)
{
var __instance __= CommandBuffer.Instantiate(spawner.Prefab);
// Place the instantiated in a grid with some noise
var position = math.transform(location.Value,
new float3(x * 1.3F, noise.cnoise(new float2(x, y) * 0.21F) * 2, y * 1.3F));
CommandBuffer.SetComponent(instance, new Translation {Value = position});
}
}
CommandBuffer.DestroyEntity(entity);
}
}
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
// 分配一个Job,并且初始化Job内部的EntityCommandBuffer
var job = new SpawnJob
{
CommandBuffer = m_EndFrameBarrier.CreateCommandBuffer()
}.ScheduleSingle(this, inputDeps);
//我们需要告诉其它的barrier系统,在命令被执行之前此Job必须被完成。
m_EndFrameBarrier.AddJobHandleForProducer(job);
return job;
}
}
注意: 本例使用了IJobForEach.ScheduleSingle(),它将会让本Job在某个单一的线程上执行。如果你使用Schedule()取代的话,系统将会分配并行的多个Job来处理这些实体。如果是并行的情况,这时你需要使用实体命令缓存的并发形式(EntityCommandBuffer.Concurrent)。
查看 ECS samples repository 来查看整个例子代码。
使用index参数
将命令添加到并发命令缓存时,你可以使用此index参数。 在并行运行Job来处理多个实体时,可以使用并发命令缓存。 在IJobForEachWithEntity作业中,如果使用Schedule()方法,而不是上面示例中使用的ScheduleSingle()方法时,Job System将会并行处理所有的实体。在并行作业中,应该始终使用并发命令缓冲区,以保证线程安全和被缓存命令的执行确定性。
在同一系统中,你还可以使用索引来跨作业地引用相同实体。 例如,如果你需要在多个过程中处理同一组实体,并同时收集临时数据,则可以在某个Job中,使用索引将临时数据插入到一个NativeArray中,再在随后的Job中,使用索引去访问该数据 。(当然,你必须将相同的NativeArray传递给两个Job)