您的位置:首页 > 移动开发 > Unity3D

Unity ECS+Jobs System笔记 访问数据1(五)

2019-08-17 17:31 2911 查看
原文链接:https://docs.unity3d.com/Packages/com.unity.entities@0.0/manual/index.html

来源:https://docs.unity3d.com/Packages/com.unity.entities@0.0/manual/index.html
我会对官方文档内容略作整理,有需要可以查看官方文档

访问实体数据

迭代访问数据对ECS而言是最常见的任务之一,ECS系统通常处理一组实体,从一个或多个组件读取数据,执行计算,然后将结果写入另一个组件
一般而言,最有效率的迭代实体和组件的方法就是在可并行化的Job中按顺序处理组件,这利用了所有数据和可用内核的处理能力,可以避免未命中CPU缓存
ECS的API提供了许多迭代方法,每种方法都有自己的性能影响和限制,可以通过以下方式迭代ECS数据:

  • IJobForEach——最简单有效的按实体处理组件数据的方法
  • IJobForEachWithEntity——比IJobForEach稍微复杂一些,使你可以访问正在处理的实体的实体句柄和数组索引
  • IJobChunk——迭代包含匹配条件的实体的内存块(称为Chunk),Job Execute()函数可以使用for循环遍历每个块内的元素,IJobChunk相较于IJobForEach支持更复杂的情况,并保持最高效率
  • ComponentSystem——ComponentSystem提供Entities.ForEach委托函数,以帮助迭代实体。但是,ForEach在主线程上运行,因此应该仅将ComponentSystem实现用于必须在主线程上执行的任务
  • Manual iteration——如果上述方法都不够,那就可以手动迭代实体或区块,例如:可以使用Job(比如IJobParallelFor)获取包含实体或要处理的实体块的NativeArray并对其进行迭代

EntityQuery类提供一种方法来构建你的一个只包含你需要的一个给定的算法或过程中的特定数据的视图,上面列表中的许多迭代方法,无论是显式还是内部,都是使用EntityQuery

1、IJobForEach

你可以定义一个IJobForEach的Job在JobComponentSystem中去写入/读取组件中的数据
当Job运行时,ECS框架会查找所有具有所需组件的实体,并为每个实体调用Job的Execute()函数,数据按照在内存中布局的顺序进行处理,并且作业并行运行,因此IJobForEach既简单又有效率
以下示例简单说明了如何使用IJobForEach,Job读取RotationSpeed组件中的数据并写入RotationQuaternion组件

public class RotationSpeedSystem : JobComponentSystem
{
//使用[BurstCompile]属性进行Burst加速.
[BurstCompile]
struct RotationSpeedJob : IJobForEach<RotationQuaternion, RotationSpeed>{
public float DeltaTime;
// [ReadOnly]告诉Job系统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运行在主线程中
//任何先于Rotation进行读写或RotationSpeed进行写的Jobs都会自动被包括在inputDependencies
protected override JobHandle OnUpdate(JobHandle inputDependencies)
{
var job = new RotationSpeedJob()
{
DeltaTime = Time.deltaTime
};
return job.Schedule(this, inputDependencies);
}
}

注意:上述代码可以从ECS Samples案例中的Hello Cube中可以找到

定义IJobForEach识别类别

IJobForEach可以定义需要识别具有什么样组件的实体:

struct RotationSpeedJob : IJobForEach<RotationQuaternion, RotationSpeed>

您还可以使用以下属性来修改Job选择的实体:

  • [ExcludeComponent(typeof(T))]—— 排除Archetype包含类型T的组件的实体
  • [RequireComponentTag(typeof(T))]——仅包括其Archetype包含类型T的组件的实体,当系统不读取或写入但仍必须与实体关联的组件时,请使用此属性

例如,以下Job定义选择具有包含Gravity,RotationQuaternion和RotationSpeed组件的,但不包含Frozen组件的实体:

[ExcludeComponent(typeof(Frozen))]
[RequireComponentTag(typeof(Gravity))]
[BurstCompile]
struct RotationSpeedJob : IJobForEach<RotationQuaternion, RotationSpeed>

如果需要更复杂的查询来选择要操作的实体,则可以使用IJobChunk而不是IJobForEach

编写Execute()方法

JobComponentSystem为每个符合条件的实体调用Execute()方法,传入到IJobForEach所标识的实体中去,因此,Execute()的参数必须与为结构体所定义的泛型参数匹配
例如,以下Execute()方法读取RotationSpeed组件的数据并读写入RotationQuaternion组件S(读/写是默认值,因此不需要属性)

public void Execute(ref RotationQuaternion rotationQuaternion, [ReadOnly] ref RotationSpeed rotSpeed){}

可以向函数参数添加属性来优化系统:

  • [ReadOnly]——用于只读不写的组件
  • [WriteOnly]——用于只写不读的组件
  • [ChangeFilter]——用于那些自从上一次系统运行后该组件的值可能已经更改了的实体

识别组件的读写性可以帮助Job scheduler更有效地分配Jobs,例如,scheduler不会同时调度对组件的读/写Job,但如果只是读取组件内容,它就会并行运行两个作业
请注意,为了效率,[ChangeFilter]会作用在整个实体的区块上,它不会追踪单个实体。如果某个区块已被另一个能够对该类型组件进行写入操作的Job所访问,那么ECS框架就会认为该区块已被更改并包含Job中的所有实体;否则ECS框架完全排除该块中的实体

2、IJobForEachWithEntity

实现IJobForEachWithEntity接口的Jobs与实现IJobForEach的Jobs非常相似。不同之处在于IJobForEachWithEntity中的Execute()函数中的类别(signature)提供了当前实体的Entity对象以及扩展的并行组件的数组的索引

使用Entity参数

你可以使用一个实体对一个EntityCommandBuffer添加指令,比如,你可以对一个实体添加一个增删组件或销毁的指令,但为了避免冲突,这些指令都不会在作业内直接完成,命令缓冲区允许您在工作线程上执行任何可能代价高昂的计算——在主线程上执行的实际操作之后再执行
以下系统基于ECS Samples案例中的SpawnFromEntity示例,在计算Job中的位置后,使用命令缓冲区实例化实体

public class SpawnerSystem : JobComponentSystem
{
// EndFrameBarrier provides the CommandBuffer
EndFrameBarrier m_EndFrameBarrier;

protected override void OnCreate()
{
// Cache the EndFrameBarrier in a field, so we don't have to get it every frame
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)
{
// Schedule the job that will add Instantiate commands to the EntityCommandBuffer.
var job = new SpawnJob
{
CommandBuffer = m_EndFrameBarrier.CreateCommandBuffer()
}.ScheduleSingle(this, inputDeps);

// We need to tell the barrier system which job it needs to complete before it can play back the commands.
m_EndFrameBarrier.AddJobHandleForProducer(job);

return job;
}
}

此示例使用IJobForEach.ScheduleSingle(),它在单个线程上执行作业,如果使用Schedule(()方法,系统将使用并行作业来处理实体,在并行的情况下,必须使用并发实体命令缓冲区(EntityCommandBuffer.Concurrent)

使用索引参数

你可以使用索引将命令添加到并发命令缓冲区,在运行并行处理实体的作业时,可以使用并发命令缓冲区,在IJobForEachWithEntity作业中,当使用Schedule()方法而不是上面示例中使用的ScheduleSingle()方法时,Job System会并行处理实体,并发命令缓冲区应始终用于并行作业,以保证线程安全和缓冲区命令执行的确定性
你还可以使用索引在同一系统中引用跨Job的相同实体,例如,如果需要在多个过程中处理一组实体并收集临时数据,则可以使用索引将临时数据插入到一个Job中的NativeArray中,然后使用索引在随后的共最终访问该数据(当然,必须将相同的NativeArray传递给两个Job)

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: