ECS Accessing entity data 一

Accessing entity data:访问实体数据

在实现ECS系统时,对数据进行遍历是最常见的任务之一. ECS systems通常处理一组实体,从一个或多个组件读取数据,执行计算,然后将结果写入另一个组件。

通常,遍历实体和组件最有效的方式时在一个并行化的Job中按顺序处理组件 . 这利用了CPU的多核性能,并避免CPU的缓存丢失

ECS API提供了多种遍历的方式,每个都有自己的性能影响和限制. 下面是方法:

  • IJobForEach —按实体处理组件数据的最简单有效方法.

  • JobComponentSystem Entities.ForEach —使用一个job结构来有效的遍历实体. (IJobForEach和在JobComponentSystemEntities.ForEach 一样,但是需要更多手工编写的设置代码.)

  • IJobForEachWithEntity — 比 IJobForEach稍微复杂一些, 允许你访问实体 handle和你正在访问的实体数组索引

  • IJobChunk —遍历所有符合条件的内存块(called a Chunk),其中包含的是符合条件的实体 . Job Execute() 方法可以用for循环遍历所有的块中的元素,你可以使用  IJobChunk 来执行比 IJobForEach更复杂的操作,同时保持最高效率.

  • ComponentSystem —  ComponentSystem 提供 Entities.ForEach 委托,允许你遍历所有的实体entities. 然而, ForEach 在主线程中执行,所有一般来说,你应该只用 ComponentSystem来实现必须在主线程上执行的任务。

  • Manual iteration — 如果以前的方法不足, 您可以手动遍历实体或块.例如,您可以获得一个包含实体的NativeArray,或者您想要处理的实体的块,使用Job(比如IJobParallelFor)对它们进行遍历。

 EntityQuery 类提供了一个构造一个你数据的视图方法,这个视图仅仅包含你算法或者程序中需要的特定的数据. 上面列表中的许多遍历方法都使用EntityQuery,无论是显式的还是内部的。就是可以通过 EntityQuery 来只遍历符合条件的实体或组件

Using IJobForEach

您可以在JobComponentSystem中定义IJobForEach Job来读写组件数据。

 当job运行的时候, ECS framework 会查找所有符合条件的实体(含有特定组件),并且为每个实体执行Execute() 方法. 数据是按照在内存中放置的顺序处理的,job是并行运行的,所以IJobForEach结合了简单和效率。

下面的示例演示了一个使用IJobForEach的简单系统. Job读取一个RotationSpeed组件,并写入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);
   }
}

Note: the above system is based on the HelloCube IJobForEach sample in the ECS Samples repository.

IJobForEach以批处理的方式处理存储在同一块中的所有实体. 当实体集跨越多个块时,Job并行处理每批实体.按块遍历一组实体通常是最有效的方法,因为它可以防止多个线程试图访问同一块内存.但是,如果要在少量实体上运行非常昂贵的进程, IJobForEach 可能无法提供最佳性能,因为它不能在每个进程上并行运行流程.在这种情况下,您可以使用 IJobParallelFor, 让你来控制批处理的大小. See Manual Iteration for an example.

 

Defining the IJobForEach signature

只有继承 IJobForEach 接口,才能让系统知道这个是需要操作的组件

struct RotationSpeedJob : IJobForEach<RotationQuaternion, RotationSpeed>

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

  • [ExcludeComponent(typeof(T)] —排除原型包含类型T的组件的实体
  • [RequireComponentTag(typeof(T)] —只包含原型包含类型T的组件的实体.当系统不需要对组件进行读写但人需要这个组件时使用这个属性

例如,下面job定义选择的实体包含Gravity、RotationQuaternion和RotationSpeed组件的原型,但不包含Frozen组件:

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

如果需要更复杂的查询来选择要操作的实体,使用 IJobChunk Job 代替 IJobForEach.

Writing the Execute() method

JobComponentSystem为每个合格的实体调用Execute()方法,传递由IJobForEach标识的组件。 因此,Execute()函数的参数必须与为IJobForEach定义的泛型参数匹配。

例如,下面的Execute()方法读取一个RotationSpeed组件,读写一个RotationQuaternion组件。(读/写是默认设置,所以不需要属性。)

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

您可以为方法的参数添加属性,以帮助ECS优化您的系统:

  • [ReadOnly] — 只读
  • [WriteOnly] — 只写
  • [ChangedFilter] — 只检索组件数据更改了的实体

标记只读或者只写,能够使job运行更有效. 例如,调度器不会将写入组件的job与读取组件的解job安排在同一时间,但是可以并行运行两个只读取相同组件的job

请注意,为了效率,  change filter 检索的使整个块中的所有实体,他不会检索单个实体,如果一个块已经被另一个job访问过了,且这个job更改了块中组件的数据,则  ECS framework 认为这个块已经被更改了,包括其中的实体也被更改了, 否则,ECS框架将完全排除该块中的实体。

Using IJobForEachWithEntity

 Jobs 通过继承 IJobForEachWithEntity 接口和继承 IJobForEach的功能差不多,区别就是. Execute() 方法的参数是当前实体的实体对象和扩展的平行数组中组件的索引。signature in IJobForEachWithEntity provides you with the Entity object for the current entity and the index into the extended, parallel arrays of components.

Using the Entity parameter

你可以使用实体对象给一个 EntityCommandBuffer添加命令. 比如, 你可以给那个实体添加移除或添加组件的命令,或者销毁实体  — 为了避免race conditions,所有的操作不能直接在job中执行.命令缓冲区允许您在工作线程上执行任何可能开销很大的计算, 同时在主线程上进行排队while queuing up the actual insertions and deletions to be performed later on the main thread.

下面的例子,使用在job执行完计算实体的位置之后使用command buffer去生成一个实体:

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;
   }
}

Note: this example uses IJobForEach.ScheduleSingle(), 让job在单个线程上执行任务. 如果使用Schedule() 方法,  system使用并行 jobs 访问实体 ,在并行线程中, 你必须使用 concurrent entity command buffer (EntityCommandBuffer.Concurrent).

See the ECS samples repository for the full example source code.

Using the index parameter

你可以使用索引 index,当给  concurrent command buffer添加一个命令的时候. 当运行访问实体的并行jobs的时候使用 concurrent command buffers.在一个 IJobForEachWithEntity Job中,  Job System 当使用 Schedule() 方法的时候会并行的访问实体,ScheduleSingle() 方法不会. Concurrent command buffers应该始终用于并行job,以确保线程安全和缓冲区命令的确定性执行。

  你还可以通过jobs使用index来同一个系统下应用同一个实体,比如,如果你想在多个通道内访问一组实体,并临时收集数据,您可以使用索引在一个job中将临时数据插入到NativeArray中,然后在后续job中使用索引访问该数据。(Naturally, you have to pass the same NativeArray to both Jobs.)

JobComponentSystem lambda functions

JobComponentSystem lambda 方法提供了一个简单的方法在实体,their components, over native containers执行自己的逻辑

The JobComponentSystem 支持两种类型的 lambda functions:

为了执行一个job lambda 函数, 使用 ForEach() or WithCode()来定义lambda , 然后通过Schedule()安排job或者使用 Run()函数在主线程中立即调用. 无论你是用 Entities.ForEach 还是Job.WthCode, 你都可以使用这些对象上的其它方法来设置不同的job选项和参数

Entities.ForEach 例子:

下面的示例演示了一个使用JobComponentSystem. Entities.ForEach。读取一个组件(本例中为Velocity)并写入另一个组件(Translation):

class ApplyVelocitySystem : JobComponentSystem
{
    protected override JobHandle OnUpdate(JobHandle inputDependencies)
    {
        var jobHandle = Entities
            .ForEach((ref Translation translation,
                      in Velocity velocity) =>
            {
                translation.Value += velocity.Value;
            })
            .Schedule(inputDependencies);

        return jobHandle;
    }
}

 使用 refin 关键字在 ForEach的参数中. 给要写入的组件使用 ref 关键字,  in 关键字给要读取的组件. 将组件标记为只读有助于job调度器更有效地执行作业。

Job.WithCode 例子

下面的例子演示了一个简单的例子,它使用一个Job.WithCode() lambda函数来用随机数填充一个本机数组,然后用另一个函数将这些随机数相加:

public class RandomSumJob : JobComponentSystem
{
    private uint seed = 1;

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        Random randomGen = new Random(seed++);
        NativeArray<float> randomNumbers
            = new NativeArray<float>(500, Allocator.TempJob);

        JobHandle generateNumbers = Job.WithCode(() =>
        {
            for (int i = 0; i < randomNumbers.Length; i++)
            {
                randomNumbers[i] = randomGen.NextFloat();
            }
        }).Schedule(inputDeps);


        NativeArray<float> result
            = new NativeArray<float>(1, Allocator.TempJob);

        JobHandle sumNumbers = Job.WithCode(() =>
        {
            for (int i = 0; i < randomNumbers.Length; i++)
            {
                result[0] += randomNumbers[i];
            }
        }).Schedule(generateNumbers);

        sumNumbers.Complete();
        UnityEngine.Debug.Log("The sum of "
                              + randomNumbers.Length + " numbers is " + result[0]);

        randomNumbers.Dispose();
        result.Dispose();

        return sumNumbers;
    }
}

Entities.ForEach entity query

  通过 entity query 来查找选择的实体或者块,用作 Entities.ForEach表达式处理的对象,这个entity query当创建JobComponentSystem的时候就已经隐式的创建了 (使用 WithStoreEntityQueryInField(ref EntityQuery) 来访问这个隐式的EntityQuery对象.)

这个 query 是显示的把你lambda表达式中的组件参数,添加到查询表达式里面,查询表达式包括 WithAll<T>, WithAny<T>, and WithNone<T>方法,你还可以设置特定的查询选项,通过使用除此之外的实体方法. 下面是和查询有关的实体方法:

  • WithAll<T> — 一个实体必须具有所有这些组件类型 (除了在lambda参数列表中找到所有组件类型之外)
  • WithAny<T,U> — 一个实体必须有一个或多个这样的组件类型.注意,允许使用WithAny指定单个组件类型;但是,由于实体必须有一个或多个这样的“可选”组件类型供查询选择,因此使用带有单个类型的WithAny等同于将该类型放在WithAll语句中。
  • WithNone<T> — 实体不能具有任何这些组件类型
  • WithChangeFilter<T> — 只选择子上次 JobComponentSystem 更新以来,特性组件发生变化的实体
  • WithSharedComponentFilter — 只选择拥有特定值的share component的块
  • WithStoreEntityQueryInField — 把Entities.ForEach生成的 EntityQuery 对象存储在一个 EntityQuery 字段里. 您可以使用这个EntityQuery对象用于获取符合条件的实体的数量.注意,这个函数在创建JobComponentSystem时将EntityQuery实例分配给您的字段. 这意味着您可以在第一次执行lambda函数之前使用查询。

Important: 不要使用WithAny<T、U>或WithNone<T>向查询添加参数列表中的组件。所有添加到lambda函数参数列表中的组件都会自动添加到实体查询的WithAll列表中;向WithAll列表和WithAny或WithNone列表添加组件会创建一个不合逻辑的查询。

Entity query example:

下面的示例选择具有 Destination, Source, and LocalToWorld组件的实体, 并且至少有一个 Rotation, Translation, or Scale组件,但是没有LocalToParent组件。

return Entities.WithAll<LocalToWorld>()
    .WithAny<Rotation, Translation, Scale>()
    .WithNone<LocalToParent>()
    .ForEach((ref Destination outputData, in Source inputData) =>
    {
        /* do some work */
    })
    .Schedule(inputDeps);

在本例中,值用Destination 和 Source 组件可以在lambda表达式内访问,因为他们俩是表达式的参数

Access to EntityQuery object example:

如何隐式的访问Entities.ForEach创建的 EntityQuery对象. 本例中,使用 EntityQuery 对象执行 CalculateEntityCount() 方法. 然后使用这个数字创建一个拥有足够空间存储每一个选择的实体的值得 native array :

private EntityQuery query;
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
    int dataCount = query.CalculateEntityCount();
    NativeArray<float> dataSquared
        = new NativeArray<float>(dataCount, Allocator.Temp);
    JobHandle GetSquaredValues = Entities
        .WithStoreEntityQueryInField(ref query)
        .ForEach((int entityInQueryIndex, in Data data) =>
            {
                dataSquared[entityInQueryIndex] = data.Value * data.Value;
            })
        .Schedule(inputDeps);

    return Job
        .WithCode(() =>
        {
            //Use dataSquared array...
            var v = dataSquared[dataSquared.Length -1];
        })
        .WithDeallocateOnJobCompletion(dataSquared)
        .Schedule(GetSquaredValues);
}

 

Change filtering

如果您只想处理一个实体组件,而另一个实体上的该组件自上次运行JobComponentSystem以来发生了更改,你可以使用 WithChangeFilter<T>来选择已经更改的组件得实体,但是这个T必须也在lambda表达式得参数列表中. 或者在 WithAll<T> 的声明中.

return Entities
    .WithChangeFilter<Source>()
    .ForEach((ref Destination outputData,
        in Source inputData) =>
    {
        /* Do work */
    })
    .Schedule(inputDeps);

实体查询最多支持对两个组件类型进行更改筛选,也就是说可以最多有两个参数.

Note 注意change filtering 是应用在块级别的 chunk level.如果任何代码使正在写入块中的组件,然后该块中的组件类型被标记为已更改 -- 即使代码实际上没有改变任何数据.

Shared component filtering

拥有shared components的相同值的实体被分到一组 . 你可以使用 WithSharedComponentFilter() 方法来选择具有特定值的实体

The following example selects entities grouped by a Cohort ISharedComponentData. The lambda function in this example sets a DisplayColor IComponentData component based on the entity’s cohort:

public class ColorCycleJob : JobComponentSystem
{
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        List<Cohort> cohorts = new List<Cohort>();
        EntityManager.GetAllUniqueSharedComponentData<Cohort>(cohorts);
        JobHandle sequentialDeps = inputDeps; // Chain job dependencies
        foreach (Cohort cohort in cohorts)
        {
            DisplayColor newColor = ColorTable.GetNextColor(cohort.Value);
            JobHandle thisJobHandle =
                Entities.WithSharedComponentFilter(cohort)
                    .ForEach((ref DisplayColor color) => { color = newColor; })
                    .Schedule(sequentialDeps);
            sequentialDeps = thisJobHandle;
        }
        return sequentialDeps;
    }
}

这个例子使用EntityManager来获得所有唯一的队列值 cohort values. 然后为每个队列安排一个lambda job,将新颜色作为捕获的变量传递给lambda函数

Lambda parameters

(A Job.WithCode lambda函数不接受任何参数)

可以将最多八个参数传递给 Entities.ForEach lambda 方法. 参数必须按以下顺序分组:

1. Parameters passed-by-value first (no parameter modifiers):按值传递的参数放在最前面
2. Writable parameters second (`ref` parameter modifier):可写的不是只写的用ref关键字,放在第二位
3. Read-only parameters last (`in` parameter modifier):只读的用in关键字,放在最后面

所有组件都应该使用ref或in参数修饰词关键字。

如果函数不遵守这些规则,编译器将提供类似于的错误:

error CS1593: Delegate 'Invalid_ForEach_Signature_See_ForEach_Documentation_For_Rules_And_Restrictions' does not take N arguments

(注意,即使问题是参数顺序,错误消息也会引用参数的数量作为问题。)

Component parameters

要访问与实体关联的组件,必须将该组件类型的参数传递给Entities.ForEach lambda 方法(除非您遍历的是块而不是实体). 编译器自动将传递给函数的所有组件作为所需组件添加到实体查询中。

要更新组件的值,需要在前面加上ref关键字,  (如果没有 ref 关键字, 所有的更改只是在其复制品上的更改,因为它是值传递,并不是引用传递,原来的值并不会更改.) 注意:使用 ref 因为这个组件在该块中已经被更改了 ,即使lambda里面并没有真正的修改它. 为了提高效率,始终将lambda函数不修改的组件指定为只读

若要将传递给lambda函数的组件指定为只读,请在参数列表中使用in关键字。

 下面是只读的例子:

return Entities.ForEach(
        (ref Destination outputData,
            in Source inputData) =>
        {
            outputData.Value = inputData.Value;
        })
    .Schedule(inputDeps);

Note:目前,您不能将chunk components块组件传递给Entities.ForEach函数

对于动态缓冲区,使用DynamicBuffer<T>而不是存储在缓冲区中的组件类型:

public class BufferSum : JobComponentSystem
{
    private EntityQuery query;

    //Schedules the two jobs with a dependency between them
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        //The query variable can be accessed here because we are
        //using WithStoreEntityQueryInField(query) in the entities.ForEach below
        int entitiesInQuery = query.CalculateEntityCount();

        //Create a native array to hold the intermediate sums
        //(one element per entity)
        NativeArray<int> intermediateSums
            = new NativeArray<int>(entitiesInQuery, Allocator.TempJob);

        //Schedule the first job to add all the buffer elements
        JobHandle bufferSumJob = Entities
            .ForEach((int entityInQueryIndex, in DynamicBuffer<IntBufferData> buffer) =>
            {
                for (int i = 0; i < buffer.Length; i++)
                {
                    intermediateSums[entityInQueryIndex] += buffer[i].Value;
                }
            })
            .WithStoreEntityQueryInField(ref query)
            .WithName("IntermediateSums")
            .Schedule(inputDeps);

        //Schedule the second job, which depends on the first
        JobHandle finalSumJob = Job
            .WithCode(() =>
            {
                int result = 0;
                for (int i = 0; i < intermediateSums.Length; i++)
                {
                    result += intermediateSums[i];
                }
                //Not burst compatible:
                Debug.Log("Final sum is " + result);
            })
            .WithDeallocateOnJobCompletion(intermediateSums)
            .WithoutBurst()
            .WithName("FinalSum")
            .Schedule(bufferSumJob);

        return finalSumJob;
    }
}

Special, named parameters

除了组件之外,还可以将以下特殊的命名参数传递给函数 Entities.ForEach lambda 函数, 据job当前处理的实体分配值:

  • Entity entity — 当前的实体 (这个参数类型只要是实体,它可以随意命名)
  • int entityInQueryIndex — 当前实体在所查询的实体列表中的索引. 当你有一个native array的时候,你可以使用  entity index,来更改每一个实体的值,就相当于for,你可以使用entityInQueryIndex作为数组索引.  entityInQueryIndex也可以被用作 jobIndex来向 concurrent EntityCommandBuffer.添加命令
  • int nativeThreadIndex —一个执行当前遍历的lambda表达式的线程. 当你通过Run()执行Lambda方法时,nativeThreadIndex 总是零.

Capturing variables

您可以为Entities.ForEach and Job.WithCode lambda functions捕获局部变量.当您使用job执行该函数时(条用 Schedule() 而不是Run()) 对于捕获的变量以及如何使用它们有一些限制:

只能捕获本机容器和blittable类型. 如果是native containers, job 只能向其中写入值(若要“返回”单个值,请创建包含一个元素的本机数组。)

可以使用以下函数将修饰符和属性应用于捕获的变量:

  • WithReadOnly(myvar) —将对变量的访问限制为只读
  • WithDeallocateOnJobCompletion(myvar) —在job完成后释放本机容器. See DeallocateOnJobCompletionAttribute.
  • WithNativeDisableParallelForRestriction(myvar) —允许多个线程访问同一个可写的本机容器.只有当每个线程只访问自己的线程时,并行访问才是安全的, 如果有多个线程访问同一个元素,就会创建一个竞争条件,其中访问的时间会改变结果. See NativeDisableParallelForRestriction.
  • WithNativeDisableContainerSafetyRestriction(myvar) — 禁用阻止对本机容器的危险访问的正常安全限制.不明智地禁用安全限制可能会导致竞态条件、细微的bug和应用程序中的崩溃. See NativeDisableContainerSafetyRestrictionAttribute.
  • WithNativeDisableUnsafePtrRestrictionAttribute(myvar) — 允许您使用本机容器提供的不安全指针。不正确的指针使用可能导致应用程序中出现细微的错误、不稳定和崩溃. See NativeDisableUnsafePtrRestrictionAttribute.

Job options

Entities.ForEach 和Job.WithCode lambda 方法都可以使用下列方法:

  • JobHandle Schedule(JobHandle) — 安排lambda方法作为一个job执行
    • Entities.ForEach — 作业在并行后台执行lambda函数, job threads. 每个作业遍历ForEach查询选择的块中的实体. (一个job实例至少处理一个块中的实体)
    • Job.WithCode —作业在后台作业线程上执行lambda函数的单个实例。
  • void Run() —在主线程上同步执行lambda函数:
    • Entities.ForEach — 对于ForEach查询选择的块中的每个实体,lambda函数执行一次. 注意: Run()不接受JobHandle参数,也不返回JobHandle,因为lambda函数不作为job运行。
    • Job.WithCode —lambda函数执行一次
  • WithBurst(FloatMode, FloatPrecision, bool) — 设置 Burst compiler 选项:
    • floatMode — 设置浮点数学优化模. Fast模式执行速度更快,但产生的浮点错误比Strict模式更大 m. See Burst FloatMode.
    • floatPrecision — 设置浮点数学精度. See Burst FloatPrecision.
    • synchronousCompilation — 立即编译该函数,而不是调度该函数以供以后编译。
  • WithoutBurst() — 关闭Burst compilation. 当您的lambda函数包含Burst不支持的代码时,请使用此函数。
  • WithStructuralChanges() — 在主线程上执行lambda函数并禁用Burst,以便可以对函数中的实体数据进行结构更改. 为了获得更好的性能,可以使用一个并发的EntityCommandBuffer。 concurrent EntityCommandBuffer instead.
  • WithName(string) —将指定的字符串作为生成的job类的名字. 这个是可选的,使用可以在debug或者profiling的时候帮助识别函数

Job dependencies

 JobHandle 对象传递给 JobComponentSystem.OnUpdate 方法,它封装了所有到目前为止在帧当中已经更新了的相关联的组件,相关联的可读写job,当你 把input dependence 从之前的系统中传递给你的 Schedule 方法, ECS 确保lambda表达式访问的一样的数组数据,被任意的job写入完成. 当您调用Run()时,lambda函数在主线程上执行,因此由早期系统调度的任何作业都会立即完成。

同样,OnUpdate()函数必须通过返回JobHandle将其依赖项传递给后续系统. 如果您的update函数只构造了单个job,则可以返回Schedule()提供的JobHandle。如果 update 方法构造了多个jobs 您可以通过将其中一个返回的JobHandle传递给下一个的Schedule()方法来链接各个依赖项

 或者, 如果奇job彼此不依赖,可以使用通过JobHandle.CombineDependencies().组合它们的依赖项

注意: JobHandle只包含数组数据的依赖项component data, 不包含native containers. 如果你的 system or job读取一个由另一个 system or job构成的 native container  ,您必须手动管理依赖项.一种方法是提供一个方法或属性,允许producing系统添加一个JobHandle作为consuming系统的依赖项 (See the AddProducerForJob() method of the EntityCommandBufferSystem for an example of this technique.)

Using Entities.ForEach with an EntityCommandBuffer

您不能在job中对实体的结构进行更改, 包括创建entities, 增加或移除组件 components, 或者销毁实体 destroying entities. 相反, 你必须使用entity command buffer在稍后进行结构更改. 默认的 ECS system group设置在系统的开始和结尾提供了entity command buffer . 通常,您应该选择最后一个实体命令缓冲区系统,它在任何其他依赖于您的结构更改的系统之前运行.例如,如果您在simulation system group创建实体,并希望在同一帧中渲染这些实体, 你可以在创建这些实体的时候使用EndSimulationEntityCommandBufferSystem 创建的entity command buffers

   要创建 entity command buffers,你必须存储了一个你想使用的 entity command buffer system的引用,它们在update函数当中,你使用那个引用创建一个 EntityCommandBuffer 的实例. (你必须在每一个update函数里面创建一个 new entity command buffer)

下面的例子演示了如何创建一个entity command buffer,在本例中,从EndSimulationEntityCommandBufferSystem获取它:

public class MyJobSystem : JobComponentSystem
{
    private EndSimulationEntityCommandBufferSystem commandBufferSystem;

    protected override void OnCreate()
    {
        commandBufferSystem = World
            .DefaultGameObjectInjectionWorld
            .GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        EntityCommandBuffer.Concurrent commandBuffer
            = commandBufferSystem.CreateCommandBuffer().ToConcurrent();

        //.. The rest of the job system code
        return inputDeps;
    }
}

因为Entities.ForEach.Schedule() 创建了一个并行job,你必须使用 entity command buffer.的concurrent  并行接口

Entites.ForEach lambda with entity command buffer example

下面的例子说明了如何在JobComponentSystem中使用entity command buffer来实现一个简单的粒子系统:

// ParticleSpawner.cs
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;

public struct Velocity : IComponentData
{
    public float3 Value;
}

public struct TimeToLive : IComponentData
{
    public float LifeLeft;
}

public class ParticleSpawner : JobComponentSystem
{
    private EndSimulationEntityCommandBufferSystem commandBufferSystem;

    protected override void OnCreate()
    {
        commandBufferSystem = World
            .DefaultGameObjectInjectionWorld
            .GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        EntityCommandBuffer.Concurrent commandBufferCreate
            = commandBufferSystem.CreateCommandBuffer().ToConcurrent();
        EntityCommandBuffer.Concurrent commandBufferCull
            = commandBufferSystem.CreateCommandBuffer().ToConcurrent();

        float dt = Time.DeltaTime;
        Random rnd = new Random();
        rnd.InitState((uint) (dt * 100000));


        JobHandle spawnJobHandle = Entities
            .ForEach((int entityInQueryIndex,
                      in SpawnParticles spawn,
                      in LocalToWorld center) =>
            {
                int spawnCount = spawn.Rate;
                for (int i = 0; i < spawnCount; i++)
                {
                    Entity spawnedEntity = commandBufferCreate
                        .Instantiate(entityInQueryIndex,
                                     spawn.ParticlePrefab);

                    LocalToWorld spawnedCenter = center;
                    Translation spawnedOffset = new Translation()
                    {
                        Value = center.Position +
                                rnd.NextFloat3(-spawn.Offset, spawn.Offset)
                    };
                    Velocity spawnedVelocity = new Velocity()
                    {
                        Value = rnd.NextFloat3(-spawn.MaxVelocity, spawn.MaxVelocity)
                    };
                    TimeToLive spawnedLife = new TimeToLive()
                    {
                        LifeLeft = spawn.Lifetime
                    };

                    commandBufferCreate.SetComponent(entityInQueryIndex,
                                                     spawnedEntity,
                                                     spawnedCenter);
                    commandBufferCreate.SetComponent(entityInQueryIndex,
                                                     spawnedEntity,
                                                     spawnedOffset);
                    commandBufferCreate.AddComponent(entityInQueryIndex,
                                                     spawnedEntity,
                                                     spawnedVelocity);
                    commandBufferCreate.AddComponent(entityInQueryIndex,
                                                     spawnedEntity,
                                                     spawnedLife);
                }
            })
            .WithName("ParticleSpawning")
            .Schedule(inputDeps);

        JobHandle MoveJobHandle = Entities
            .ForEach((ref Translation translation, in Velocity velocity) =>
            {
                translation = new Translation()
                {
                    Value = translation.Value + velocity.Value * dt
                };
            })
            .WithName("MoveParticles")
            .Schedule(spawnJobHandle);

        JobHandle cullJobHandle = Entities
            .ForEach((Entity entity, int entityInQueryIndex, ref TimeToLive life) =>
            {
                life.LifeLeft -= dt;
                if (life.LifeLeft < 0)
                    commandBufferCull.DestroyEntity(entityInQueryIndex, entity);
            })
            .WithName("CullOldEntities")
            .Schedule(inputDeps);

        JobHandle finalDependencies
            = JobHandle.CombineDependencies(MoveJobHandle, cullJobHandle);

        commandBufferSystem.AddJobHandleForProducer(spawnJobHandle);
        commandBufferSystem.AddJobHandleForProducer(cullJobHandle);

        return finalDependencies;
    }
}
// SpawnParticles.cs
using Unity.Entities;
using Unity.Mathematics;

[GenerateAuthoringComponent]
public struct SpawnParticles : IComponentData
{
    public Entity ParticlePrefab;
    public int Rate;
    public float3 Offset;
    public float3 MaxVelocity;
    public float Lifetime;
}

Implementation notes

Entities.ForEach and Job.WithCode使用编译器扩展将编写的代码转换成高效的、基于job的c#代码。本质上一般来,当你使用这两种方法时,你就可以描述你想让job做什么,然后编译器扩展就会生成所需的代码,这种转换应该是透明的;但是,请注意下列各点:

  • lambda函数的性能缺陷,如捕获变量时不应用额外的托管内存分配
  • IDE中完成的代码可能不会列出实体和作业对象方法的正确参数。
  • 您可能会在警告、错误消息和IL代码反汇编等位置看到生成的类名
  •  当你使用WithStoreEntityQueryInField(ref query), 编译器扩展在系统的OnCreate()方法之前为query字段分配一个值.这意味着你可以在Entities.ForEach lambda函数运行之前,访问 EntityQuery object 对象

 

发布了80 篇原创文章 · 获赞 7 · 访问量 2674

猜你喜欢

转载自blog.csdn.net/qq_37672438/article/details/104598734