美文网首页Unity探路营
在ECS系统中使用IJobChunk作业

在ECS系统中使用IJobChunk作业

作者: 洪智 | 来源:发表于2020-12-14 10:29 被阅读0次

    洪流学堂,让你快人几步。你好,我是跟着大智学Unity的萌新,我叫小新,最近在跟着大智学习DOTS。

    你可以在系统内部实现IJobChunk,用于逐块遍历数据。当你在系统的OnUpdate()中安排IJobChunk作业时,该作业为每个符合entity查询条件的chunk调用一次Execute()。然后,你可以遍历每个chunk中的entity上的数据。

    使用IJobChunk

    与Entities.ForEach相比,使用IJobChunk进行迭代需要更多的代码,但是也更直接。

    按块进行迭代的另一个好处是,你可以使用Archetype.Has<T>()来检查每个块中是否存在可选组件,然后相应地处理块中的所有entity。

    实现IJobChunk作业的步骤如下:

    1. 创建一个EntityQuery来标识要处理的entity。
    2. 定义job结构体,并包含ArchetypeChunkComponentType对象的字段,这些对象标识job必须直接访问的组件的类型。另外,指定作业是只读还是写入这些组件。
    3. 实例化作业并在系统OnUpdate()方法中安排作业。
    4. Execute()函数中,获取NativeArray作业读取或写入的组件的实例,然后在当前块上进行遍历以执行所需的工作。

    后面会有实例演示如何使用IJobChunk

    使用EntityQuery查询数据

    EntityQuery定义原型必须包含的一组组件类型,系统才能处理其关联的块和实体。原型中可以有其他组件,但是它必须至少有EntityQuery定义的组件。你还可以排除包含特定类型组件的原型。

    对于简单查询,可以使用SystemBase.GetEntityQuery()函数并按如下所示传入组件类型:

    public class RotationSpeedSystem : SystemBase
    {
        private EntityQuery m_Query;
    
        protected override void OnCreate()
        {
            m_Query = GetEntityQuery(ComponentType.ReadOnly<Rotation>(),
                ComponentType.ReadOnly<RotationSpeed>());
            //...
        }
    
    

    对于更复杂的情况,你可以使用EntityQueryDescEntityQueryDesc提供了灵活的查询机制,以指定的组件类型:

    • All:此数组中的所有组件类型必须存在于原型中
    • Any:原型中必须至少存在此数组中的一种组件类型
    • None:原型中不能存在此数组中的任何组件类型

    例如,以下查询包括包含RotationQuaternionRotationSpeed组件的原型,但不包括包含Frozen组件的任何原型:

    protected override void OnCreate()
    {
        var queryDescription = new EntityQueryDesc()
        {
            None = new ComponentType[]
            {
                typeof(Static)
            },
            All = new ComponentType[]
            {
                ComponentType.ReadWrite<Rotation>(),
                ComponentType.ReadOnly<RotationSpeed>()
            }
        };
        m_Query = GetEntityQuery(queryDescription);
    }
    
    

    查询时可以使用ComponentType.ReadOnly<T>而不是更简单的typeof表达式来指定系统不会写入RotationSpeed

    你还可以组合多个查询,传入EntityQueryDesc对象数组而不是单个实例。ECS使用逻辑或运算来组合每个查询。下面的示例选择包含RotationQuaternionRotationSpeed组件(或两者同时都有)的所有原型:

    protected override void OnCreate()
    {
        var queryDescription0 = new EntityQueryDesc
        {
            All = new ComponentType[] {typeof(Rotation)}
        };
    
        var queryDescription1 = new EntityQueryDesc
        {
            All = new ComponentType[] {typeof(RotationSpeed)}
        };
    
        m_Query = GetEntityQuery(new EntityQueryDesc[] {queryDescription0, queryDescription1});
    }
    

    注意:请勿在EntityQueryDesc中包含可选组件。要处理可选组件,在IJobChunk.Execute()使用chunk.Has<T>()内部方法检查当前ArchetypeChunk是否有可选组件。因为同一块中的所有实体都具有相同的组件,所以你只需要一个块检查一次即可,不用每个实体检查一次。

    为了提高效率并避免创建不必要地垃圾收集的引用类型,应在系统OnCreate()方法中创建EntityQueries,然后将结果存储在实例变量中。(在上面示例中,m_Query变量就是这个用途)

    定义IJobChunk结构体

    IJobChunk结构体中定义了作业运行时需要的数据以及作业的Execute()方法。

    要访问系统传给Execute()方法的块内的组件数组,必须为作业读取或写入的每种类型的组件创建一个ArchetypeChunkComponentType<T>对象。你可以使用这些对象获取NativeArray实例,通过这些NativeArray可以访问实体的组件。包括作业的Execute()方法读取或写入的EntityQuery中引用的所有组件。你还可以用ArchetypeChunkComponentType获取未包含在EntityQuery中的可选组件类型。

    在访问当前块之前,必须检查确保当前块有可选组件。例如,HelloCube IJobChunk示例定义了一个作业结构体,该结构定义了两个组件的ArchetypeChunkComponentType<T>变量,RotationQuaternionRotationSpeed

    [BurstCompile]
    struct RotationSpeedJob : IJobChunk
    {
        public float DeltaTime;
        public ComponentTypeHandle<Rotation> RotationTypeHandle;
        [ReadOnly] public ComponentTypeHandle<RotationSpeed> RotationSpeedTypeHandle;
    
        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
        {
            // ...
        }
    }
    

    系统为OnUpdate()函数中的这些变量赋值,ECS在运行作业时会在Execute()方法内使用这些变量。

    这个作业还使用Unity的delta时间为3D对象的旋转设置动画。该示例使用struct字段将delta值传递给Execute()方法。

    编写Execute方法

    IJobChunk Execute()方法的签名为:

    public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    

    chunk参数是内存块的句柄,包含此作业的迭代时必须处理的实体和组件。因为块只能包含一个原型,所以块中的所有实体都具有相同的组件。

    使用chunk参数获取组件的NativeArray实例:

    
    var chunkRotations = chunk.GetNativeArray(RotationTypeHandle);
    var chunkRotationSpeeds = chunk.GetNativeArray(RotationSpeedTypeHandle);
    
    

    这些数组是对齐的,同个实体在所有数组中具有相同的索引。可以使用正常的for循环来遍历组件数组。使用chunk.Count得到存储在当前块的实体的数量:

    
    var chunkRotations = chunk.GetNativeArray(RotationTypeHandle);
    var chunkRotationSpeeds = chunk.GetNativeArray(RotationSpeedTypeHandle);
    for (var i = 0; i < chunk.Count; i++)
    {
        var rotation = chunkRotations[i];
        var rotationSpeed = chunkRotationSpeeds[i];
    
        // Rotate something about its up vector at the speed given by RotationSpeed.
        chunkRotations[i] = new Rotation
        {
            Value = math.mul(math.normalize(rotation.Value),
                quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * DeltaTime))
        };
    }
    
    

    如果在EntityQueryDesc中有Any过滤器,或者可选的组件没有写在查询中,则可以在使用之前用ArchetypeChunk.Has<T>()函数测试当前块是否包含这些组件:

    if (chunk.Has<OptionalComp>(OptionalCompType))
    {//...}
    

    注意:如果使用并发的entity command buffer,将chunkIndex参数作为sortKey参数传递给命令缓冲区函数。

    跳过没有变化的实体的块

    如果仅在组件值发生更改时才需要更新实体,可以将该组件类型添加到EntityQuery的更改筛选器中。例如,如果你的系统读取两个组件,并且仅在前两个组件中的一个已更改时才需要更新第三个组件,则可以按以下方式使用EntityQuery:

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

    EntityQuery更改过滤器最多支持两个组件。如果你想进行更多检查或不使用EntityQuery,则可以手动进行检查。要进行这个检查,可以使用ArchetypeChunk.DidChange()函数将组件的块的更改版本与系统的LastSystemVersion进行比较。如果此函数返回false,则可以完全跳过当前块,因为自从上次系统运行以来,该类型的组件均未更改。

    你必须使用一个struct字段将LastSystemVersion从系统传递到作业中,如下所示:

    
    [BurstCompile]
    struct UpdateJob : IJobChunk
    {
        public ComponentTypeHandle<InputA> InputATypeHandle;
        public ComponentTypeHandle<InputB> InputBTypeHandle;
        [ReadOnly] public ComponentTypeHandle<Output> OutputTypeHandle;
        public uint LastSystemVersion;
    
        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
        {
            var inputAChanged = chunk.DidChange(InputATypeHandle, LastSystemVersion);
            var inputBChanged = chunk.DidChange(InputBTypeHandle, LastSystemVersion);
    
            // If neither component changed, skip the current chunk
            if (!(inputAChanged || inputBChanged))
                return;
    
            var inputAs = chunk.GetNativeArray(InputATypeHandle);
            var inputBs = chunk.GetNativeArray(InputBTypeHandle);
            var outputs = chunk.GetNativeArray(OutputTypeHandle);
    
            for (var i = 0; i < outputs.Length; i++)
            {
                outputs[i] = new Output { Value = inputAs[i].Value + inputBs[i].Value };
            }
        }
    }
    
    

    与所有作业结构体字段一样,在计划作业之前,必须先赋值:

    
    protected override void OnUpdate()
    {
        var job = new UpdateJob();
    
        job.LastSystemVersion = this.LastSystemVersion;
    
        job.InputATypeHandle = GetComponentTypeHandle<InputA>(true);
        job.InputBTypeHandle = GetComponentTypeHandle<InputB>(true);
        job.OutputTypeHandle = GetComponentTypeHandle<Output>(false);
    
        this.Dependency = job.ScheduleParallel(m_Query, this.Dependency);
    }
    
    

    注意:为了提高效率,更改版本适用于整个块,而不是单个实体。如果另一个具有写入该类型组件功能的作业访问了块,ECS就会增加该组件的更改版本,并且DidChange()函数会返回true。即使声明对组件进行写如访问的作业实际上并未更改组件的值,ECS也会增加更改版本。

    实例化并安排作业

    要运行IJobChunk作业,必须创建作业结构体的实例,给结构体字段赋值,然后安排作业。当你在SystemBase的OnUpdate()方法中执行时,系统会将安排作业每帧运行一次。

    protected override void OnUpdate()
    {
        var job = new RotationSpeedJob()
        {
            RotationTypeHandle = GetComponentTypeHandle<Rotation>(false),
            RotationSpeedTypeHandle = GetComponentTypeHandle<RotationSpeed>(true),
            DeltaTime = Time.DeltaTime
        };
        this.Dependency =  job.ScheduleParallel(m_Query, this.Dependency);
    }
    

    调用GetArchetypeChunkComponentType<T>()函数设置组件类型变量时,确保将只需要读取不需要写入的组件的isReadOnly参数设置为true,这些参数可能会对ECS框架安排作业的效率产生重大影响。这些访问模式的设置,需要在结构体定义和EntityQuery中匹配。

    不要在系统类变量中缓存GetArchetypeChunkComponentType<T>()返回值。你必须在每次系统运行时调用这个函数,并将更新后的值传给作业。

    扩展阅读

    【扩展学习】洪流学堂回复DOTS可以阅读本系列所有文章,更有视频教程等着你!


    呼~ 今天小新絮絮叨叨的真是够够的了。没讲清楚的地方欢迎评论,咱们一起探索。

    我是大智,你的技术探路者,下次见!

    别走!点赞收藏哦!

    好,你可以走了。

    相关文章

      网友评论

        本文标题:在ECS系统中使用IJobChunk作业

        本文链接:https://www.haomeiwen.com/subject/thtjiktx.html