Unity ECS Manual [12]

63 篇文章 11 订阅

Using Entities.ForEach

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

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

  下面的例子说明了一个简单的SystemBase实现,它使用Entities.ForEach来读取一个组件(本例中为Velocity)并写入另一个组件(Translation)。

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

  注意在ForEach lambda函数的参数上使用关键字ref和in。对你写入的组件使用ref,而对你只读的组件使用in。将组件标记为只读有助于Job调度器更有效地执行你的Job。

Selecting entities(选择实体)

  Entities.ForEach提供了它自己的机制来定义实体查询,用来选择要处理的实体。该查询会自动包括你作为你的lambda函数的参数使用的任何组件。你还可以使用WithAll、WithAny和WithNone子句来进一步细化哪些实体被选中。参见SystemBase.Entities获取完整的查询选项列表。

  下面的例子选择了具有组件 “Destination”、" Source "和 “LocalToWorld”,并且至少具有一个组件 “Rotation”、"Translation"或 “Scale”,但没有 "LocalToParent "组件的实体。

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

  在这个例子中,只有Destination和Source组件可以在lambda函数中被访问,因为它们是参数列表中唯一的组件。

Accessing the EntityQuery object(访问EntityQuery对象)

  要访问Entities.ForEach创建的EntityQuery对象,请使用带有ref参数修改器的[WithStoreEntityQueryInField(ref query)] 。这个函数将查询的引用分配给你提供的字段。

NOTE
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();
}

Optional components(可选组件)

  你不能创建一个指定了可选组件的查询(使用WithAny<T,U>),同时在lambda函数中访问这些组件。如果你需要读取或写入一个可选组件,你可以将Entities.ForEach构造分成多个Jobs,每个可选组件的组合都有一个。例如,如果你有两个可选组件,你需要三个ForEach构造:一个包括第一个可选组件,一个包括第二个,还有一个包括两个组件。另一种方法是使用IJobChunk按块进行迭代。

Change filtering(改变过滤方式)

  如果你只想在当前SystemBase实例运行后该组件的另一个实体发生变化时处理该实体组件,你可以使用WithChangeFilter启用变化过滤。在变化过滤器中使用的组件类型必须在lambda函数参数列表中,或者是WithAll语句的一部分。

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

  一个实体查询支持对最多两个组件类型的变化过滤。

  请注意,变化过滤是在块级应用的。如果有任何代码以写的方式访问了一个块中的组件,那么该块中的组件类型就会被标记为已更改 – 即使该代码实际上并没有改变任何数据。

Shared component filtering(共享组件的过滤)

  具有共享组件的实体被分组,与其他具有相同共享组件值的实体分组。你可以使用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函数。

Defining the ForEach function(定义ForEach函数)

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

  一个典型的lambda函数看起来像:


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

  默认情况下,你最多可以向Entities.ForEach lambda函数传递八个参数。(如果你需要传递更多的参数,你可以定义一个自定义的委托。)当使用标准的委托时,你必须按以下顺序分组参数。

  1. 首先是逐值传递的参数(没有参数修改器)
  2. 然后是可写参数(ref参数修改器)
  3. 最后只读参数(in参数修改器)

  所有的组件都应该使用ref或in参数修饰关键字。否则,传递给你的函数的组件结构是一个拷贝而不是一个引用。这意味着对只读参数要进行额外的内存拷贝,也意味着当拷贝的结构在函数返回后超出范围时,你打算更新的组件的任何变化都会被无声地抛出。

  如果你的函数不遵守这些规则,而且你没有创建一个合适的委托,编译器会提供一个类似的错误。

error CS1593: Delegate ‘Invalid_ForEach_Signature_See_ForEach_Documentation_For_Rules_And_Restrictions’ does not take N arguments

(请注意,即使问题出在参数顺序上,错误信息也会把参数数量作为问题来处理)。

Custom delegates(自定义委托)

  你可以在ForEach lambda函数中使用8个以上的参数。通过声明你自己的委托类型和ForEach重载。这使得你可以使用你想要的参数,并将Ref/in/value参数放在你想要的任何顺序中。

  你可以在你的参数列表中的任何地方声明三个特殊的、命名的参数实体、entityInQueryIndex和nativeThreadIndex。不要对这些参数使用ref或in修饰语。

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

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

Component parameters(组件参数)

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

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

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

NOTE
使用ref意味着当前块中的组件被标记为已更改,即使lambda函数没有实际修改它们。为了提高效率,总是使用in关键字将你的lambda函数没有修改的组件指定为只读。

  下面的例子将一个Source组件参数作为只读传递给Job,而将一个Destination组件参数作为可写传递给Job。

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

NOTE
目前,你不能向Entities.ForEach lambda函数传递块状组件。

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

public partial class BufferSum : SystemBase
{
    private EntityQuery query;

    //安排两个Job之间的依赖关系
    protected override void OnUpdate()
    {
        //查询变量可以在这里被访问,
        //因为我们在下面的entities.ForEach中使用WithStoreEntityQueryInField(query)。
        int entitiesInQuery = query.CalculateEntityCount();

        //创建一个本地数组来保存中间值
        //(每个实体有一个元素)
        NativeArray<int> intermediateSums
            = new NativeArray<int>(entitiesInQuery, Allocator.TempJob);

        //安排第一个工作,添加所有的缓冲区元素
        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(); // 对每一块的所有实体并行执行

        //安排第二个Job,这取决于第一个Job。
        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(); // 在一个单一的后台线程上执行
    }
}

Special, named parameters(特殊的、命名的参数)

  除了组件之外,你还可以向Entities.ForEach lambda函数传递以下特殊的、命名的参数,这些参数会根据Job当前处理的实体来分配数值。

  • Entity entity - 当前实体的Entity实例。(只要类型是Entity,该参数可以被命名为任何东西)。

  • int entityInQueryIndex - 实体在被查询选择的所有实体列表中的索引。当你有一个本地数组,需要为每个实体填充一个唯一的值时,可以使用实体索引值。你可以使用entityInQueryIndex作为该数组中的索引。entityInQueryIndex也应该被用作向一个并发的EntityCommandBuffer添加命令的排序键。

  • int nativeThreadIndex - 执行lambda函数当前迭代的线程的唯一索引。当你使用Run()执行lambda函数时,nativeThreadIndex总是为零。(不要使用nativeThreadIndex作为并发的EntityCommandBuffer的sortKey;使用entityInQueryIndex来代替。)

Capturing variables(捕捉变量)

  你可以为Entities.ForEach lambda函数捕获局部变量。当你使用Job执行该函数时(通过调用Schedule函数之一而不是Run),对捕获的变量和你如何使用它们有一些限制。

  • 只有native containers 和 blittable types可以被捕获。

  • 一个Job只能写到Job native containers的捕获变量。要 "返回 "一个单一的值,请创建一个有一个元素的native array)。

  如果你读取一个[native container],但不对其进行写入,请始终使用WithReadOnly(variable)指定只读访问。参见SystemBase.Entities,了解更多关于为捕获的变量设置属性的信息。你可以指定的属性包括NativeDisableParallelForRestriction和其他。Entities.ForEach将这些属性作为函数提供,因为C#语言不允许在局部变量上设置属性。

  你也可以通过使用以下方式来表明你希望在Entities.ForEach运行后将捕获的NativeContainers或包含NativeContainers的类型处理掉。

  WithDisposeOnCompletion(variable)。这将在lambda运行后立即处理这些类型(在Run()的情况下),或者将它们安排在稍后与一个Job一起被Diposed,并返回JobHandle(在Schedule()/ScheduleParallel()的情况下)。

NOTE
当用Run()执行函数时,你可以写到非本地容器的捕获变量。然而,你仍然应该尽可能地使用blittable类型,以便该函数可以用Burst编译。

Supported Features(支持的功能)

  你可以用Run()在主线程上执行lambda函数,也可以用Schedule()作为一个单独的Job,或者用ScheduleParallel()作为一个并行Job。这些不同的执行方法对你访问数据的方式有不同的限制。此外,Burst使用了C#语言的一个限制性子集,所以当你使用这个子集之外的C#特性(包括访问托管类型)时,需要指定WithoutBurst()。

  下表显示了Entities.ForEach目前支持哪些功能,用于SystemBase中的不同调度方法。
在这里插入图片描述
  Entities.ForEach结构使用专门的中间语言(IL)编译后处理,将你为该结构编写的代码翻译成正确的ECS代码。这种翻译允许你表达你的算法的意图,而不必包括复杂的、模板式的代码。然而,这可能意味着一些常见的代码编写方式不被允许。

  目前不支持以下功能。
在这里插入图片描述

Dependencies(依赖性)

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

关于工作依赖性的更多一般信息,请参见Job依赖性。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值