创建系统 Entity.Each

使用 Entity.ForEach

使用SystemBase类提供的Entities.ForEach构造作为在实体及其组件上定义和执行算法的简洁方法。Entities.ForEach执行您在实体查询选择的所有实体上定义的 lambda 函数。

要执行作业 lambda 函数,您可以使用Schedule()和安排作业ScheduleParallel(),或者使用立即(在主线程上)执行它Run()。您可以使用Entities.ForEach上定义的其他方法来设置实体查询以及各种作业选项。

以下示例说明了一个简单的SystemBase实现,该实现使用Entities.ForEach读取一个组件(在本例中为 Velocity)并写入另一个组件(翻译):


partial class ApplyVelocitySystem : SystemBase
{
    protected override void OnUpdate()
    {
        Entities
            .ForEach((ref Translation translation,
            in Velocity velocity) =>
            {
                translation.Value += velocity.Value;
            })
            .Schedule();
    }
}

请注意ForEach lambda 函数的关键字refin参数的使用。使用ref的组件,你写不出来,并in为组件,您只读。将组件标记为只读有助于作业调度程序更有效地执行您的作业。

选择实体

Entities.ForEach提供了自己的机制来定义用于选择要处理的实体的实体查询。该查询会自动包含您用作 lambda 函数参数的任何组件。你也可以使用WithAllWithAnyWithNone条款,以进一步细化哪些实体被选中。有关查询选项的完整列表,请参阅SystemBase.Entities

以下示例选择具有组件 Destination、Source 和 LocalToWorld 的实体;并且至少具有旋转、平移或缩放其中之一;但没有 LocalToParent 组件。


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

在此示例中,只有 Destination 和 Source 组件可以在 lambda 函数内部访问,因为它们是参数列表中的唯一组件。

访问 EntityQuery 对象

要访问Entities.ForEach创建的EntityQuery对象,请将[WithStoreEntityQueryInField(ref query)] 与 ref 参数修饰符一起使用。此函数为您提供的字段分配对查询的引用。

笔记

EntityQuery 在 OnCreate 中创建。此方法提供该查询的副本,可随时使用(甚至在调用 Entities.ForEach 之前)。此外,此 EntityQuery 没有 Entities.ForEach 调用设置的任何过滤器。

以下示例说明了如何访问为Entities.ForEach构造隐式创建的 EntityQuery 对象。在这种情况下,该示例使用 EntityQuery 对象来调用CalculateEntityCount()方法。该示例使用此计数创建一个具有足够空间的本机数组,可以为查询选择的每个实体存储一个值:


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

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

可选组件

您不能创建指定可选组件的查询(使用 WithAny<T,U>),也不能在 lambda 函数中访问这些组件。如果您需要读取或写入可选组件,您可以将 Entities.ForEach 构造拆分为多个作业,每个作业用于可选组件的组合。例如,如果您有两个可选组件,则需要三个 ForEach 结构:一个包含第一个可选组件,一个包含第二个组件,一个包含两个组件。另一种选择是使用 IJobChunk 按块进行迭代。

更改过滤

如果您只想在自上次运行当前SystemBase实例后该组件的另一个实体发生更改时处理该实体组件,您可以使用 WithChangeFilter<T> 启用更改过滤。更改过滤器中使用的组件类型必须位于 lambda 函数参数列表中或 WithAll<T> 语句的一部分。


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

实体查询最多支持两种组件类型的更改过滤。

请注意,更改过滤是在块级别应用的。如果任何代码通过写访问访问块中的组件,则该块中的组件类型被标记为已更改——即使代码实际上没有更改任何数据。

共享组件过滤

具有共享组件的实体被分组为块,其他实体对其共享组件具有相同的值。您可以使用 WithSharedComponentFilter() 函数选择具有特定共享组件值的实体组。

以下示例选择按 Cohort ISharedComponentData 分组的实体。此示例中的 lambda 函数根据实体的同类群组设置 DisplayColor IComponentData 组件:


public partial class ColorCycleJob : SystemBase
{
    protected override void OnUpdate()
    {
        List<Cohort> cohorts = new List<Cohort>();
        EntityManager.GetAllUniqueSharedComponentData<Cohort>(cohorts);
        foreach (Cohort cohort in cohorts)
        {
            DisplayColor newColor = ColorTable.GetNextColor(cohort.Value);
            Entities.WithSharedComponentFilter(cohort)
                .ForEach((ref DisplayColor color) => { color = newColor; })
                .ScheduleParallel();
        }
    }
}

该示例使用 EntityManager 来获取所有唯一的同类群组值。然后它为每个群组安排一个 lambda 作业,将新颜色作为捕获变量传递给 lambda 函数。

定义 ForEach 函数

当您定义要与Entities.ForEach一起使用的 lambda 函数时,您可以声明SystemBase类在执行函数时用于传递有关当前实体的信息的参数。

典型的 lambda 函数如下所示:


Entities.ForEach(
    (Entity entity,
        int entityInQueryIndex,
        ref Translation translation,
        in Movement move) => { /* .. */})

默认情况下,您最多可以将八个参数传递给 Entities.ForEach lambda 函数。(如果需要传递更多参数,可以定义自定义委托。)使用标准委托时,必须按以下顺序对参数进行分组:

1. Parameters passed-by-value first (no parameter modifiers)
2. Writable parameters second (`ref` parameter modifier)
3. Read-only parameters last (`in` parameter modifier)

所有组件都应使用 therefinparameter 修饰符关键字。否则,传递给函数的组件结构是副本而不是引用。这意味着只读参数的额外内存副本,并且意味着当函数返回后复制的结构超出范围时,您打算更新的组件的任何更改都会被静默抛出。

如果您的函数不遵守这些规则并且您没有创建合适的委托,编译器会提供类似于以下内容的错误:

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

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

自定义委托

您可以在 ForEach lambda 函数中使用 8 个以上的参数。通过声明您自己的委托类型和 ForEach 重载。这允许您根据需要使用尽可能多的参数,并按您想要的任何顺序放置 ref/in/value 参数。

你可以声明三个特殊,命名参数 entityentityInQueryIndexnativeThreadIndex任何地方你的参数列表。不要对这些参数使用refin修饰符。


static class BringYourOwnDelegate
{
    // Declare the delegate that takes 12 parameters. T0 is used for the Entity argument
    [Unity.Entities.CodeGeneratedJobForEach.EntitiesForEachCompatible]
    public delegate void CustomForEachDelegate<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>
        (T0 t0, in T1 t1, in T2 t2, in T3 t3, in T4 t4, in T5 t5,
         in T6 t6, in T7 t7, in T8 t8, in T9 t9, in T10 t10, in T11 t11);

    // Declare the function overload
    public static TDescription ForEach<TDescription, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>
        (this TDescription description, CustomForEachDelegate<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11> codeToRun)
        where TDescription : struct, Unity.Entities.CodeGeneratedJobForEach.ISupportForEachWithUniversalDelegate =>
        LambdaForEachDescriptionConstructionMethods.ThrowCodeGenException<TDescription>();
}

// A system that uses the custom delegate and overload
public partial class MayParamsSystem : SystemBase
{
    protected override void OnUpdate()
    {
        Entities.ForEach(
                (Entity entity0,
                    in Data1 d1,
                    in Data2 d2,
                    in Data3 d3,
                    in Data4 d4,
                    in Data5 d5,
                    in Data6 d6,
                    in Data7 d7,
                    in Data8 d8,
                    in Data9 d9,
                    in Data10 d10,
                    in Data11 d11
                    ) => {/* .. */})
            .Run();
    }
}

笔记

选择 ForEach lambda 函数的默认限制为 8 个参数,因为声明过多的委托和重载会对 IDE 性能产生负面影响。ref/in/value 和参数数量的每个组合都需要唯一的委托类型和 ForEach 重载。

组件参数

要访问与实体关联的组件,您必须将该组件类型的参数传递给 lambda 函数。编译器会自动将传递给函数的所有组件作为必需组件添加到实体查询中。

要更新组件值,您必须使用ref参数列表中的关键字通过引用将其传递给 lambda 函数。(如果没有ref关键字,将对组件的临时副本进行任何修改,因为它将按值传递。)

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

笔记

使用ref意味着当前块中的组件被标记为已更改,即使 lambda 函数实际上并未修改它们。为了提高效率,请始终使用in关键字将 lambda 函数不会修改的组件指定为只读。

以下示例将 Source 组件参数作为只读传递给作业,并将 Destination 组件参数作为可写传递给作业:


Entities.ForEach(
    (ref Destination outputData,
        in Source inputData) =>
    {
        outputData.Value = inputData.Value;
    })
    .ScheduleParallel();
笔记

目前,您无法将块组件传递给 Entities.ForEach lambda 函数。

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


public partial class BufferSum : SystemBase
{
    private EntityQuery query;

    //Schedules the two jobs with a dependency between them
    protected override void OnUpdate()
    {
        //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
        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")
            .ScheduleParallel(); // Execute in parallel for each chunk of entities

        //Schedule the second job, which depends on the first
        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);
        })
            .WithDisposeOnCompletion(intermediateSums)
            .WithoutBurst()
            .WithName("FinalSum")
            .Schedule(); // Execute on a single, background thread
    }
}

特殊的命名参数

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

  • Entity entity— 当前实体的实体实例。(只要类型为实体,参数可以命名为任何名称。)
  • int entityInQueryIndex— 查询选择的所有实体列表中实体的索引。当您拥有需要为每个实体填充唯一值的本机数组时,请使用实体索引值。您可以使用 entityInQueryIndex 作为该数组中的索引。entityInQueryIndex 还应用作sortKey将命令添加到并发EntityCommandBuffer 的
  • int nativeThreadIndex— 执行 lambda 函数当前迭代的线程的唯一索引。当您使用 Run() 执行 lambda 函数时,nativeThreadIndex 始终为零。(不要nativeThreadIndex用作sortKey并发EntityCommandBuffer 的entityInQueryIndex而是使用。)

捕获变量

您可以为 Entities.ForEach lambda 函数捕获局部变量。当您使用作业执行该函数时(通过调用其中一个 Schedule 函数而不是 Run),对捕获的变量及其使用方式有一些限制:

  • 只能捕获本机容器和 blittable 类型。
  • 作业只能写入作为本机容器的捕获变量。(要“返回”单个值,请创建一个包含一个元素的本机数组。)

如果您读取 [本机容器],但不写入它,请始终使用WithReadOnly(variable). 有关为捕获的变量设置属性的更多信息,请参阅SystemBase.Entities。您可以指定的属性包括NativeDisableParallelForRestriction和其他。Entities.ForEach将这些作为函数提供,因为 C# 语言不允许对局部变量进行属性。

您还可以使用 指示您希望在Entities.ForEach运行后处理捕获的 NativeContainers 或包含 NativeContainers 的类型WithDisposeOnCompletion(variable)。这将在 lambda 运行后立即处理类型(在 情况下Run())或安排它们稍后使用 Job 处理并返回 JobHandle(在Schedule()/的情况下ScheduleParallel())。

笔记

执行该函数时,Run()您可以写入非本机容器的捕获变量。但是,您仍应尽可能使用 blittable 类型,以便可以使用Burst编译函数。

支持的功能

您可以使用Run()、作为单个作业使用Schedule()或作为并行作业使用 来在主线程上执行 lambda 函数ScheduleParallel()。这些不同的执行方法对您访问数据的方式有不同的限制。此外,Burst使用 C# 语言的受限子集,因此您需要指定WithoutBurst()何时使用该子集之外的 C# 功能(包括访问托管类型)。

下表显示了Entities.ForEach当前支持哪些功能,用于SystemBase 中可用的不同调度方法:

支持的功能 日程 调度并行
捕获本地值类型 X X X
捕获本地引用类型 x(仅限无突发)    
写入捕获的变量 X    
在系统类上使用字段 x(仅限无突发)    
引用类型的方法 x(仅限无突发)    
共享组件 x(仅限无突发)    
托管组件 x(仅限无突发)    
结构变化 x(仅WithoutBurst 和WithStructuralChanges)    
SystemBase.GetComponent X X X
SystemBase.SetComponent X X  
从实体获取组件数据 X X x(仅作为只读)
有组件 X X X
完成处置 X X X

一个Entities.ForEach建设使用专门的中间语言(IL)编译后处理翻译,你写的建设纳入正确的ECS的代码。这种转换允许您表达算法的意图,而无需包含复杂的样板代码。但是,这可能意味着不允许使用某些常见的代码编写方式。

目前不支持以下功能:

不支持的功能
.With 调用中的动态代码
通过 ref 共享组件参数
嵌套 Entities.ForEach lambda 表达式
系统中标有 [ExecuteAlways] 的 Entities.ForEach(目前正在修复)
使用存储在变量、字段或方法中的委托进行调用
带有 lambda 参数类型的 SetComponent
带有可写 lambda 参数的 GetComponent
lambda 中的通用参数
在具有通用参数的系统中

依赖关系

默认情况下,系统使用其Dependency属性管理其与 ECS 相关的依赖项。默认情况下,系统会将使用Entities.ForEach和 [Job.WithCode]创建的每个作业按照它们在OnUpdate()函数中出现的顺序添加到依赖作业句柄。您还可以通过将 [JobHandle] 传递给您的函数来手动管理作业依赖项,然后返回生成的依赖项。有关更多信息,请参阅依赖项Schedule

有关作业依赖项的更多一般信息,请参阅作业依赖项。

上一篇:empty()方法


下一篇:HttpUtils 地址调用工具类