ECS 三 访问Entity数据

一:Accessing entity data:访问实体数据

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

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

下面是一些访问实体的方法:

  • Entities.ForEach -- 最简单的遍历ECS component data的方法。
  • Job.WithCode -- 通过一个后台的job,执行一个lambda 方法
  • IJobEntity -- 第二简单的遍历 ECS component data的方法,当想只写一次job,然后调用多次的时候使用
  • IJobEntityBatch -- 一个"lower level" 的机制, 一个chunk一个chunk的访问ECS component data .
  • C# Job System -- 通过调用常用地c# job  访问.

1.1 Using IJobEntity jobs

IJobEntity, 当你想在多个system中传递数据的时候用这个,它会创建一个 IJobEntityBatch ,通过继承 IJobEntity 接口,创建一个job, 实现自定义的Execute 方法. 注意要带上 partial 关键字, 因为最终是在project/Temp/GeneratedCode/....下生成一个单独的文件,包含一个struct 继承于 IJobEntityBatch 接口。下面是一个例子,每帧都为特定的组件值+1:

public partial struct ASampleJob : IJobEntity
{
    // Adds one to every translation component
    void Execute(ref Translation translation)
    {
        translation.Value += 1f;
    }
}

public partial class ASample : SystemBase
{
    protected override void OnUpdate()
    {
        // Schedules the job
        new ASampleJob().ScheduleParallel();
    }
}

1.1.1 Specifying a Query:通过一个query 查询带组件的实体

为IJobEntity声明query的方法有两种 :

  1. 手动执行,以指定不同的调用需求
  2. 根据Excute  方法的参数,自动查找符合条件的实体.
partial struct QueryJob : IJobEntity
{
    // Iterates over all translation components and increments upwards movement by one.
    public void Execute(ref Translation translation)
    {
        translation.Value += math.up();
    }
}

public partial class QuerySystem : SystemBase
{
    // Query that matches QueryJob, specified for `BoidTarget`
    EntityQuery m_QueryBoidTarget;

    // Query that matches QueryJob, specified for `BoidObstacle`
    EntityQuery m_QueryBoidObstacle;
    protected override void OnCreate()
    {
        // Query that contains all of Execute params found in `QueryJob` - as well as additional user specified component `BoidTarget`.
        m_QueryBoidTarget = GetEntityQuery(ComponentType.ReadWrite<Translation>(),ComponentType.ReadOnly<BoidTarget>());

        // Query that contains all of Execute params found in `QueryJob` - as well as additional user specified component `BoidObstacle`.
        m_QueryBoidObstacle = GetEntityQuery(ComponentType.ReadWrite<Translation>(),ComponentType.ReadOnly<BoidObstacle>());
    }

    protected override void OnUpdate()
    {
        // Uses the BoidTarget query
        new QueryJob().ScheduleParallel(m_QueryBoidTarget);

        // Uses the BoidObstacle query
        new QueryJob().ScheduleParallel(m_QueryBoidObstacle);

        // Uses query created automatically that matches parameters found in `QueryJob`.
        new QueryJob().ScheduleParallel();
    }
}

1.1.2 Attributes:相关属性

因为这类似于job,所以用在job上的属性,在它身上也适用:

  • Unity.Burst.BurstCompile
  • Unity.Collections.DeallocateOnJobCompletion
  • Unity.Collections.NativeDisableParallelForRestriction
  • Unity.Burst.BurstDiscard
  • Unity.Collections.LowLevel.Unsafe.NativeSetThreadIndex
  • Unity.Collections.NativeDisableParallelForRestriction
  • Unity.Burst.NoAlias

并且, IJobEntity 还有额外的属性:

  • Unity.Entities.EntityInQueryIndex 用来表示当前实体在所有查询中的索引,和entityInQueryIndex 在 Entities.ForEach很像。

A sample of EntityInQueryIndex can be read as follows:

[BurstCompile]
partial struct CopyPositionsJob : IJobEntity
{
    public NativeArray<float3> CopyPositions;

    // Iterates over all `LocalToWorld` and stores their position inside `copyPositions`.
    public void Execute([EntityInQueryIndex] int entityInQueryIndex, in LocalToWorld localToWorld)
    {
        CopyPositions[entityInQueryIndex] = localToWorld.Position;
    }
}

public partial class EntityInQuerySystem : SystemBase
{
    // This query should match `CopyPositionsJob` parameters
    EntityQuery m_Query;
    protected override void OnCreate()
    {
        // Get query that matches `CopyPositionsJob` parameters
        m_Query = GetEntityQuery(ComponentType.ReadOnly<LocalToWorld>());
    }

    protected override void OnUpdate()
    {
        // Get a native array equal to the size of the amount of entities found by the query.
        var positions = new NativeArray<float3>(m_Query.CalculateEntityCount(), World.UpdateAllocator.ToAllocator);

        // Schedule job on parallel threads for this array.
        new CopyPositionsJob{CopyPositions = positions}.ScheduleParallel();

        // Dispose the array of positions found by the job.
        positions.Dispose(Dependency);
    }
}

 

1.1.3 IJobEntity 和 Entities.ForEach

IJobEntity 和 Entities.ForEach 相比最大的优点就是,job只写一次,然后在多个system 的调度

下面是一个关于 Entities.ForEach的例子:

public partial class BoidForEachSystem : SystemBase
{
    EntityQuery m_BoidQuery;
    EntityQuery m_ObstacleQuery;
    EntityQuery m_TargetQuery;
    protected override void OnUpdate()
    {
        // Calculate amount of entities in respective queries.
        var boidCount = m_BoidQuery.CalculateEntityCount();
        var obstacleCount = m_ObstacleQuery.CalculateEntityCount();
        var targetCount = m_TargetQuery.CalculateEntityCount();

        // Allocate arrays to store data equal to the amount of entities matching respective queries.
        var cellSeparation = CollectionHelper.CreateNativeArray<float3, RewindableAllocator>(boidCount, ref World.UpdateAllocator);
        var copyTargetPositions = CollectionHelper.CreateNativeArray<float3, RewindableAllocator>(targetCount, ref World.UpdateAllocator);
        var copyObstaclePositions = CollectionHelper.CreateNativeArray<float3, RewindableAllocator>(obstacleCount, ref World.UpdateAllocator);

        // Schedule job for respective arrays to be stored with respective queries.
        Entities
            .WithSharedComponentFilter(new BoidSetting{num=1})
            .ForEach((int entityInQueryIndex, in LocalToWorld localToWorld) =>
            {
                cellSeparation[entityInQueryIndex] = localToWorld.Position;
            })
            .ScheduleParallel();

        Entities
            .WithAll<BoidTarget>()
            .WithStoreEntityQueryInField(ref m_TargetQuery)
            .ForEach((int entityInQueryIndex, in LocalToWorld localToWorld) =>
            {
                copyTargetPositions[entityInQueryIndex] = localToWorld.Position;
            })
            .ScheduleParallel();

        Entities
            .WithAll<BoidObstacle>()
            .WithStoreEntityQueryInField(ref m_ObstacleQuery)
            .ForEach((int entityInQueryIndex, in LocalToWorld localToWorld) =>
            {
                copyObstaclePositions[entityInQueryIndex] = localToWorld.Position;
            })
            .ScheduleParallel();
    }
}

It can be rewritten as (remark, CopyPositionsJob can be found above):

public partial class BoidJobEntitySystem : SystemBase
{
    EntityQuery m_BoidQuery;
    EntityQuery m_ObstacleQuery;
    EntityQuery m_TargetQuery;

    protected override void OnUpdate()
    {
        // Calculate amount of entities in respective queries.
        var boidCount = m_BoidQuery.CalculateEntityCount();
        var obstacleCount = m_ObstacleQuery.CalculateEntityCount();
        var targetCount = m_TargetQuery.CalculateEntityCount();

        // Allocate arrays to store data equal to the amount of entities matching respective queries.
        var cellSeparation = CollectionHelper.CreateNativeArray<float3, RewindableAllocator>(boidCount, ref World.UpdateAllocator);
        var copyTargetPositions = CollectionHelper.CreateNativeArray<float3, RewindableAllocator>(targetCount, ref World.UpdateAllocator);
        var copyObstaclePositions = CollectionHelper.CreateNativeArray<float3, RewindableAllocator>(obstacleCount, ref World.UpdateAllocator);

        // Schedule job for respective arrays to be stored with respective queries.
        new CopyPositionsJob { CopyPositions = cellSeparation}.ScheduleParallel(m_BoidQuery);
        new CopyPositionsJob { CopyPositions = copyTargetPositions}.ScheduleParallel(m_TargetQuery);
        new CopyPositionsJob { CopyPositions = copyObstaclePositions}.ScheduleParallel(m_ObstacleQuery);
    }

    protected override void OnCreate()
    {
        // Get respective queries, that includes components required by `CopyPositionsJob` described earlier.
        m_BoidQuery = GetEntityQuery(typeof(LocalToWorld));
        m_BoidQuery.SetSharedComponentFilter(new BoidSetting{num=1});
        m_ObstacleQuery = GetEntityQuery(typeof(LocalToWorld), typeof(BoidObstacle));
        m_TargetQuery = GetEntityQuery(typeof(LocalToWorld), typeof(BoidTarget));;
    }
}

1.2 Using Job.WithCode

这种方法就类似创建了一个简单的job,Excute 方法就是lambda表达式:

public partial class RandomSumJob : SystemBase
{
    private uint seed = 1;

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

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

        // To get data out of a job, you must use a NativeArray
        // even if there is only one value
        NativeArray<float> result
            = new NativeArray<float>(1, Allocator.TempJob);

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

        // This completes the scheduled jobs to get the result immediately, but for
        // better efficiency you should schedule jobs early in the frame with one
        // system and get the results late in the frame with a different system.
        this.CompleteDependency();
        UnityEngine.Debug.Log("The sum of "
            + randomNumbers.Length + " numbers is " + result[0]);

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

如果执行 parallel job, 要实现 IJobFor, 然就才能调用ScheduleParallel() 。

1.2.1 在lambda 表达式中访问变量和返回值Variables

在 Job.WithCode lambda 方法中不能传递值或者返回值, 但是可以访问在 OnUpdate() 方法中出创建的局部变量。

当你在 C# Job System通过 Schedule()调度job时,有以下限制:

  • 访问的变量必须是 NativeArray -- 或者其它的native container --或者是 blittable
  • 要有返回值,必须把值存储到一个 native array 中,即使只有一个值。 (Note that you can write to any captured variable when executing with Run().)

Job.WithCode 中可以包含一些属性,比如: WithReadOnly 表明只读,  WithDisposeOnCompletion 表示在job执行完再dispose。

See Job.WithCode for more information about these modifiers and attributes.

 

1.2.2 IJobEntity 调度

有两种方式调用lambda表达式:

  • Schedule() -- 同步job,该job 是单独 执行,不是并行的,在后台线程执行。能够很好地利用CPU的资源。
  • Run() -- 在主线程立即执行. 大多数情况 Job.WithCode 可以使用 Burst compiled ,这样即使在主线程运行,也很快。

注意 :调用 Run()方法,会自动完成所有的  dependencies .如果你没有显示的传递一个 JobHandle 参数,system 会假设当前的 current Dependency 属性代表当前的依赖. (Pass in a new JobHandle if the function has no dependencies.) 

1.2.3 Dependencies

默认,system 通过  Dependency 属性管理job之间的依赖,system 把每个通过 Entities.ForEach 和 Job.WithCode 创建的job 添加到 Dependency job handle 中,顺序就是在 OnUpdate() 出现的顺序,也可以手动传递一个 JobHandle 给Schedule 方法,。

See Job dependencies for more general information about job dependencies.

Note:  如果完成了job ,update 还是执行创建job吗?

1.3 Using Entities.ForEach

通过  SystemBase  提供的 Entities.ForEach 方法访问,它作为一个简洁的方法,访问实体和它们的组件, Entities.ForEach 通过一个   entity query  来为每一个实体,执行你的方法。

通过 Schedule() 和 ScheduleParallel()调度job,在工作线程中,或者  Run(),在主线程中立即调用

下面的例子 是 通过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();
    }
}

ref 表示可以写入,in  表示只读的,节省性能

1.3.1 Selecting entities

Entities.ForEach 使用 WithAllWithAnyWithNone 进一步筛选实体。详情 See SystemBase.Entities for the complete list of query options.

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

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

1.3.2 Accessing the EntityQuery object:访问EntityQuery对象

访问 Entities.ForEach 创建的 EntityQuery 对象, 使用 [WithStoreEntityQueryInField(ref query)] 方法。

注意:EntityQuery 在OnCreate的时候创建了,这个方法只是重新赋值了。

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

1.3.3 Change filtering 组件改变筛选

筛选组件改变的实体,它是按 chunk 遍历的,如果你通过write 的方法,访问chunk,即使没改变具体的值,这个方法也会生效。


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

An entity query supports change filtering on up to two component types.

Note that change filtering is applied at the chunk level. If any code accesses a component in a chunk with write access, then that component type in that chunk is marked as changed -- even if the code didn’t actually change any data

 

1.3.4 Shared component filtering

Entities 拥有相同的share component ,且值一样的保存在相同的chunk,使用WithSharedComponentFilter()来筛选具有相同share component的实体.


public partial class ColorCycleJob : SystemBase
{
    protected override void OnUpdate()
    {
        List<Cohort> cohorts = new List<Cohort>();
        //得到了相同的组件,不同的值得组件集合,也就是有几个chunk
        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();
        }
    }
}

1.3.5 Defining the ForEach function:定义lambda表达式

When you define the lambda function to use with Entities.ForEach, you can declare parameters that the SystemBase class uses to pass in information about the current entity when it executes the function.

一般 lambda 表达式像这样:


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

默认最多传递8个参数,如果还想传递的更多,可以自定义delegate, define a custom delegate ,当你用默认的delegate时,要按照以下顺序传递参数:

  1. 按值传递的参数优先,也就是没有参数修饰符(ref,in,out,params)
  2. 可写参数,ref修饰 
  3. 只读参数最后,in 修饰 

所有的 components 不是使用ref 就是使用 in 参数修饰符, 否则,传递给函数的组件是副本而不是引用. 这意味着只读参数需要额外的内存拷贝 

1.3.6 Custom delegates:传递8个以上的参数

有三个特殊的命名参数 special, named parameters entityentityInQueryIndexnativeThreadIndex ,这三个不要加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();
    }
}

1.3.7 Component parameters

ref 修饰组件,表示该chunk 已经被改变了,即使没有真正改变 


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

注意:

目前还不能传递 chunk components 组件.

dynamic buffers组件 使用DynamicBuffer<T>。


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

1.3.8 Special, named parameters 特殊命名的参数

  • Entity entity — 当前的实体,entity 可以任意命名,只要类型是Entity
  • int entityInQueryIndex — 当使用 native array 时,当前实体的索引,同时也是 concurrent EntityCommandBuffer 的排序索引
  • int nativeThreadIndex — 当前执行该job 的线程索引,Run()--主线程,该值为0

1.3.9 Capturing variables

要访问变量,只能访问native container 类型的 或者 blittable结构

使用 WithReadOnly(variable)表示只读. See SystemBase.Entities for more information about setting attributes for captured variables. Entities.ForEach provides these as functions 因为c#  不允许在局部变量上加属性

WithDisposeOnCompletion(variable)表示在lambda 表达式运行完之后立即dispose。

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

 

注意:

当执行 Run() 方法时,可以写入不是 native containers的变量,但是尽可能的使用blittable 结构类型,它能使用  Burst 编译。

1.3.10 Supported Features

 Run(), Schedule(),  ScheduleParallel(). 这三种方法访问数据的限制也不同,此外, Burst 使用了 C# 的一个受限子集,所以在这个子集之外使用c# 的特征,需要使用 WithoutBurst()方法。

下面是对比:

Supported FeatureRunScheduleScheduleParallel
Capture local value typexxx
Capture local reference typex (only WithoutBurst and not in ISystem)
Writing to captured variablesx
Use field on the system classx (only WithoutBurst)
Methods on reference typesx (only WithoutBurst and not in ISystem)
Shared Componentsx (only WithoutBurst and not in ISystem)
Managed Componentsx (only WithoutBurst and not in ISystem)
Structural changesx (only WithStructuralChanges and not in ISystem)
SystemBase.GetComponentxxx
SystemBase.SetComponentxx
GetComponentDataFromEntityxxx (only as ReadOnly)
HasComponentxxx
WithDisposeOnCompletionxxx
WithScheduleGranularityx

Note: WithStructuralChanges() 会关闭 Burst. 如果想要高性能,就不要使用该属性,而是使用 EntityCommandBuffer.

 Entities.ForEach 结构使用Roslyn source generators 把代码转变成ECS 形式的代码,这意味着你写的代码很简洁,但是要有一些限制:

下面是一些限制(目前不支持):

Unsupported Feature
Dynamic code in .With invocations
SharedComponent parameters by ref
Nested Entities.ForEach lambda expressions
Calling with delegate stored in variable, field or by method
SetComponent with lambda parameter type
GetComponent with writable lambda parameter
Generic parameters in lambdas
In systems with generic parameters


1.4 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。
  • WithName(string) —将指定的字符串作为生成的job类的名字. 这个是可选的,使用可以在debug或者profiling的时候帮助识别函数

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

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

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

1.6 Using Entity Batch jobs

实现 IJobEntityBatch 或者 IJobEntityBatchWithIndex 接口,可以遍历在batches 中的实体。默认 batch 的大小是一整个chunk,但是你可以设置batch 的大小,不管batch 多大,batch 中的实体,总是在相同的chunk 中,你查询的实体都是一个chunk 中的,把访问的实体,分成多个batches  然后交给job 去执行,把结果存储在 native container 中。

IJobEntityBatchWithIndex 在需要实体在所有实体中的索引时,用这个job ,否则使用 IJobEntityBatch 它更高效,因为不需要计算索引值。

NOTE

 IJobEntityBatch or IJobEntityBatchWithIndex 比 Entities.ForEach 复杂 但更高效 

 IJobEntityBatch 取代了 IJobChunk. 最大的不同是 IJobEntityBatch 可以访问chunk 的一部分,而   不是整个chunk

1.6.1 通过 EntityQuery 查询数据

See Using an EntityQuery to query data for information about defining queries.

注意:

不要在 EntityQuery 包含可选择的组件,使用ArchetypeChunk.Has 方法来判断实体有没有组件. 因为batch 中的所有实体拥有的组件都是一样的,你需要每个块检查一遍,而不是每个实体检查一遍

1.6.2 Define the job struct

举例: IJobEntityBatch 的写法:

public struct UpdateTranslationFromVelocityJob : IJobEntityBatch
{
    public ComponentTypeHandle<VelocityVector> velocityTypeHandle;
    public ComponentTypeHandle<Translation> translationTypeHandle;
    public float DeltaTime;

    [BurstCompile]
    public void Execute(ArchetypeChunk batchInChunk, int batchIndex)
    {
        NativeArray<VelocityVector> velocityVectors =
            batchInChunk.GetNativeArray(velocityTypeHandle);
        NativeArray<Translation> translations =
            batchInChunk.GetNativeArray(translationTypeHandle);

        for(int i = 0; i < batchInChunk.Count; i++)
        {
            float3 translation = translations[i].Value;
            float3 velocity = velocityVectors[i].Value;
            float3 newTranslation = translation + velocity * DeltaTime;

            translations[i] = new Translation() { Value = newTranslation };
        }
    }
}

1.6.3 IJobEntityBatch 和 IJobEntityBatchWithIndex

 IJobEntityBatch 和 IJobEntityBatchWithIndex 唯一的不同是 IJobEntityBatchWithIndex 在执行Excute  方法时,传递了一个  indexOfFirstEntityInQuery 参数,这个参数是第一个实体在当前batch 中所有实体中的索引. 因为存储了索引值,索引性能稍差一些。

注意:

当向 [EntityCommandBuffer.ParallelWriter] 中添加命令时, batchIndex 参数作为排序的键 ( sortKey),作为command buffer function 的参数。不需要通过IJobEntityBatchWithIndex 得到每一个实体上的sort keyj. batchIndex 这两种job 都可以访问

1.6.4 声明job 访问的数据

job 中访问的数据,分为以下四种:

访问 entity component 和 buffer data

分三步:

第一, 声明一个 ComponentTypeHandle 字段 :

public ComponentTypeHandle<Translation> translationTypeHandle;

第二, 在  Execute 方法中访问包含该组件的NativeArray 数据:

NativeArray<Translation> translations =
    batchInChunk.GetNativeArray(translationTypeHandle);

最后, 在system 的 onupdate 方法中通过 ComponentSystemBase.GetComponentTypeHandle 方法赋值:

// "this" is your SystemBase subclass
updateFromVelocityJob.translationTypeHandle
    = this.GetComponentTypeHandle<Translation>(false);

每次调度job 的时候就赋值一次,不要缓存它.

可以用 ComponentTypeHandle 来访问 EntityQuery 中不包含的组件类型,但是得检查当前batch 中是否包含你想访问的组件。 使用 Has 方法来检查当前batch 中是否包含组件:

ComponentTypeHandle 字段是线程安全的,它防止的线程竞争。通过设置  GetComponentTypeHandle  的isReadOnly 参数 来更准确的访问组件.

Looking up data for other entities

通过 EntityQuery 和 IJobEntityBatch 或者 Entities.ForEach 访问数据很有效,但是,经常有需要以随机访问方式查找数据的情况,比如, 当一个实体的数据依赖于另一个实体,你必须把这两种组件类型都传递给Job:

ComponentDataFromEntity -- 方法:访问任意包含该组件的实体

BufferFromEntity -- 方法:访问任意包含该Buffer 组件的实体

因为随机访问,所以性能稍差,并且增加了线程竞争的机会。

For more information, see Looking up data.

Accessing other data

也可以在job 中自定义数据类型,然后在调度job 之前赋值,它是在所有batch 中共享的。

比如 要 持续移动物体,就得需要一个时间变量,可以声明一个DeltaTime 的字段,然后在 OnUpdate 赋值,在 job 的 Execute 方法中调用. 这样每一帧都会计算 DeltaTime。 

1.6.5 写 Execute 方法

IJobEntityBatch.Execute 方法是:

void Execute(ArchetypeChunk batchInChunk, int batchIndex)

IJobEntityBatchWithIndex.Execute 方法是:

void Execute(ArchetypeChunk batchInChunk, int batchIndex, int indexOfFirstEntityInQuery)

The batchInChunk parameter

batchInChunk 参数是 ArchetypeChunk 的实例,包含了所有实体和组件数据. 因为chunk 只包含一种 ArcheType,所以所有的实体的组件都是一样的. 默认它包含该chunk 中所有的实体,但是当通过 ScheduleParallel 调度job 时,可以声明只包含chunk中一部分的实体

通过 batchInChunk获取要访问的组件数据到 NativeArray 字段。

The batchIndex parameter

batchIndex 参数表示当前的 batch  在job 创建的batches 中的索引. job 中的batch 不一定是按索引处理的,因为有的完成的快,有的完成的慢.

如果想以batch 为序,存储数据,可以使用这个

如果使用 parallel writing entity command buffer,  batchIndex 作为command buffer functions 的 sortKey 参数。

The indexOfFirstEntityInQuery parameter

IJobEntityBatchWithIndex 中的参数 indexofFirstEntityInQuery. 它表示当前batch 中的第一个实体,在所有实体中的索引值. 

Optional components

Any 可以在query 中使用, 也可以使用 ArchetypeChunk.Has 方法检测组件是否存在:

// If entity has Rotation and LocalToWorld components,
// slerp to align to the velocity vector
if (batchInChunk.Has<Rotation>(rotationTypeHandle) &&
    batchInChunk.Has<LocalToWorld>(l2wTypeHandle))
{
    NativeArray<Rotation> rotations
        = batchInChunk.GetNativeArray(rotationTypeHandle);
    NativeArray<LocalToWorld> transforms
        = batchInChunk.GetNativeArray(l2wTypeHandle);

    // By putting the loop inside the check for the
    // optional components, we can check once per batch
    // rather than once per entity.
    for (int i = 0; i < batchInChunk.Count; i++)
    {
        float3 direction = math.normalize(velocityVectors[i].Value);
        float3 up = transforms[i].Up;
        quaternion rotation = rotations[i].Value;

        quaternion look = quaternion.LookRotation(direction, up);
        quaternion newRotation = math.slerp(rotation, look, DeltaTime);

        rotations[i] = new Rotation() { Value = newRotation };
    }
}

1.7 Schedule the job

运行一个IJobEntityBatch job, 首先要创建一个job 实例,然后给它的字段赋值,然后在system 的 OnUpdate 方法中调度, system 会每一帧都调度一次.

public partial class UpdateTranslationFromVelocitySystem : SystemBase
{
    EntityQuery query;

    protected override void OnCreate()
    {
        // Set up the query
        var description = new EntityQueryDesc()
        {
            All = new ComponentType[]
                   {ComponentType.ReadWrite<Translation>(),
                    ComponentType.ReadOnly<VelocityVector>()}
        };
        query = this.GetEntityQuery(description);
    }

    protected override void OnUpdate()
    {
        // Instantiate the job struct
        var updateFromVelocityJob
            = new UpdateTranslationFromVelocityJob();

        // Set the job component type handles
        // "this" is your SystemBase subclass
        updateFromVelocityJob.translationTypeHandle
            = this.GetComponentTypeHandle<Translation>(false);
        updateFromVelocityJob.velocityTypeHandle
            = this.GetComponentTypeHandle<VelocityVector>(true);

        // Set other data need in job, such as time
        updateFromVelocityJob.DeltaTime = World.Time.DeltaTime;

        // Schedule the job
        this.Dependency
            = updateFromVelocityJob.ScheduleParallel(query, this.Dependency);
    }

调用 GetComponentTypeHandle 方法时 参数isReadOnly 表示该组件是只读的。

不要缓存handle

1.7.1 Scheduling options

三种方法:

  • Run -- 在主线程中立即调用,在它之前会完成该job 依赖的所有job, Batch size 永远为 1 (an entire chunk).

  • Schedule --在工作线程上调用,也是完成它依赖的所有job 之后,每一个chunk 执行一次Excute方法,Chunks 按队列访问. Batch size 永远为1.

  • ScheduleParallel -- 可以声明 batch size, batches 并行访问 parallel (在工作线程上) 。

1.7.2 Setting the batch size

使用 ScheduleParallel 可以设置batch 的大小,通过设置  batchesPerChunk 参数,1表示整个chunk

注意

一般来说设置为1,更高效,但是当你的算法很耗时,你只想执行一部分,就可以并行运行了.

1.7.3 跳过实体没有改变的chunk

如果只想更新组件有改变的实体,可以通过 EntityQuery 设置:

EntityQuery query;

protected override void OnCreate()
{
    query = GetEntityQuery(
        new ComponentType[]
        {
            ComponentType.ReadOnly<InputA>(),
            ComponentType.ReadOnly<InputB>(),
            ComponentType.ReadWrite<Output>()
        }
    );

//这个表示只筛选下面两个组件有变化的实体
    query.SetChangedVersionFilter(
            new ComponentType[]
            {
                typeof(InputA),
                typeof(InputB)
            }
        );
}

EntityQuery change filter 最多支持两个组件,如果想要检查更多的组件,使用  ArchetypeChunk.DidChange 方法,与  system 的 LastSystemVersion 作比较,如果返回false,你就可以跳过这个chunk,因为它可上一帧没有变化 。

You must use a struct field to pass the LastSystemVersion from the system into the job, as follows:

struct UpdateOnChangeJob : IJobEntityBatch
{
    public ComponentTypeHandle<InputA> InputATypeHandle;
    public ComponentTypeHandle<InputB> InputBTypeHandle;
    [ReadOnly] public ComponentTypeHandle<Output> OutputTypeHandle;
    public uint LastSystemVersion;

    [BurstCompile]
    public void Execute(ArchetypeChunk batchInChunk, int batchIndex)
    {
        var inputAChanged = batchInChunk.DidChange(InputATypeHandle, LastSystemVersion);
        var inputBChanged = batchInChunk.DidChange(InputBTypeHandle, LastSystemVersion);

        // If neither component changed, skip the current batch
        if (!(inputAChanged || inputBChanged))
            return;

        var inputAs = batchInChunk.GetNativeArray(InputATypeHandle);
        var inputBs = batchInChunk.GetNativeArray(InputBTypeHandle);
        var outputs = batchInChunk.GetNativeArray(OutputTypeHandle);

        for (var i = 0; i < outputs.Length; i++)
        {
            outputs[i] = new Output { Value = inputAs[i].Value + inputBs[i].Value };
        }
    }
}

As with all the job struct fields, you must assign its value before you schedule the job:

public partial class UpdateDataOnChangeSystem : SystemBase {

    EntityQuery query;

    protected override void OnUpdate()
    {
        var job = new UpdateOnChangeJob();

        job.LastSystemVersion = this.LastSystemVersion;

        job.InputATypeHandle = GetComponentTypeHandle<InputA>(true);
        job.InputBTypeHandle = GetComponentTypeHandle<InputB>(true);
        job.OutputTypeHandle = GetComponentTypeHandle<Output>(false);

        this.Dependency = job.ScheduleParallel(query, this.Dependency);
    }

    protected override void OnCreate()
    {
        query = GetEntityQuery(
            new ComponentType[]
            {
                ComponentType.ReadOnly<InputA>(),
                ComponentType.ReadOnly<InputB>(),
                ComponentType.ReadWrite<Output>()
            }
        );
    }
}

注意:

change version 是对整个chunk 说的,而不是单个实体. 

1.8  Manual iteration

可以通过 IJobParallelFor 显式的请求访问在NativeArray 里的chunk,比如:

public class RotationSpeedSystem : SystemBase
{
   [BurstCompile]
   struct RotationSpeedJob : IJobParallelFor
   {
       [DeallocateOnJobCompletion] public NativeArray<ArchetypeChunk> Chunks;
       public ArchetypeChunkComponentType<RotationQuaternion> RotationType;
       [ReadOnly] public ArchetypeChunkComponentType<RotationSpeed> RotationSpeedType;
       public float DeltaTime;

       public void Execute(int chunkIndex)
       {
           var chunk = Chunks[chunkIndex];
           var chunkRotation = chunk.GetNativeArray(RotationType);
           var chunkSpeed = chunk.GetNativeArray(RotationSpeedType);
           var instanceCount = chunk.Count;

           for (int i = 0; i < instanceCount; i++)
           {
               var rotation = chunkRotation[i];
               var speed = chunkSpeed[i];
               rotation.Value = math.mul(math.normalize(rotation.Value), quaternion.AxisAngle(math.up(), speed.RadiansPerSecond * DeltaTime));
               chunkRotation[i] = rotation;
           }
       }
   }

   EntityQuery m_Query;   

   protected override void OnCreate()
   {
       var queryDesc = new EntityQueryDesc
       {
           All = new ComponentType[]{ typeof(RotationQuaternion), ComponentType.ReadOnly<RotationSpeed>() }
       };

       m_Query = GetEntityQuery(queryDesc);
   }

   protected override void OnUpdate()
   {
       var rotationType = GetArchetypeChunkComponentType<RotationQuaternion>();
       var rotationSpeedType = GetArchetypeChunkComponentType<RotationSpeed>(true);
       var chunks = m_Query.CreateArchetypeChunkArray(Allocator.TempJob);

       var rotationsSpeedJob = new RotationSpeedJob
       {
           Chunks = chunks,
           RotationType = rotationType,
           RotationSpeedType = rotationSpeedType,
           DeltaTime = Time.deltaTime
       };
       this.Dependency rotationsSpeedJob.Schedule(chunks.Length,32, this.Dependency);
   }
}

1.9.1 Iterating manually

可以使用EntityManager 类来手动管理是遍历实体还是遍历chunk,尽管这样性能不是最好,最好在测试或者debug 情况下使用。

比如:

var entityManager = World.Active.EntityManager;
var allEntities = entityManager.GetAllEntities();
foreach (var entity in allEntities)
{
   //...
}
allEntities.Dispose();

var entityManager = World.Active.EntityManager;
var allChunks = entityManager.GetAllChunks();
foreach (var chunk in allChunks)
{
   //...
}
allChunks.Dispose()
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Entity Framework 可以实现多数据上传,可以通过以下步骤实现: 1. 创建一个包含所有数据上传的实体类。 2. 实现一个上传数据的方法,使用 Entity Framework 进行数据保存。 3. 在上传数据的方法中,使用事务来确保数据的完整性。 4. 在上传数据的方法中,使用 DbContext 类的实例来操作数据库。 5. 在上传数据的方法中,使用 DbSet 类的实例来操作特定的实体。 6. 在上传数据的方法中,使用 LINQ 查询语言来操作数据。 例如,以下代码展示了如何在 Entity Framework 中实现多数据上传: ``` public void UploadData(List<MyEntity> entities) { using (var context = new MyDbContext()) { using (var transaction = context.Database.BeginTransaction()) { try { foreach (var entity in entities) { context.MyEntities.Add(entity); } context.SaveChanges(); transaction.Commit(); } catch (Exception ex) { transaction.Rollback(); throw ex; } } } } ``` 在这个示例中,我们创建了一个名为 MyEntity 的实体类,并实现了一个 UploadData 方法,该方法接受一个 MyEntity 类型的列表作为参数。在方法中,我们使用 Entity Framework 的 DbContext 类的实例来打开数据库连接,并使用 MyDbContext 类来操作数据库。我们还使用事务来确保数据的完整性,如果在上传数据的过程中发生任何错误,我们将回滚事务。最后,我们使用 SaveChanges() 方法来保存所有更改,并使用 Commit() 方法提交事务。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TO_ZRG

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值