ECS+Job实例总结

ECS+Job实例总结

环境: Unity2022.3.5f + com.unity.entities 1.0.14 + URP

entities 不支持Builtin管线 需要切换URP or HDRP 否则表现上Game视图中不会显示Subscene中物体

在空工程导入entities包后 Collections中存在多项Native数据类型报错 重启工程后解决

实例工程采用Subscene方式 借助Subscene以及实现的相应组件的Baker将组件与实体进行绑定

Subscene 会将子场景内的对象转为实体, 并依据对象当前挂载的组件进行对应组件的关系绑定。 节省了手动AddComponent操作。 但用户自定义组件。需要相应继承Mono的Authoring类挂载在相应对象身上 并实现一个继承Baker的绑定类,将Authoring类型中的属性与组件属性绑定关联起来。

ECS

非托管对象

内存连续 避免缓存未命中 避免访问主存以及检查缓存是否有空余快可装进

避免GC

Entity

实体
    public struct Entity : IEquatable<Entity>, IComparable<Entity>

存在Index Version 属性

Index相当于实体ID。

Version记录当前实体版本号

实体被销毁时,索引会被回收,并自增版本号。

当判断两个实体是否为同一实体时,需要同时判断Index和Version。

创建以及销毁实体
EntityManager
// Creates an entity that contains a cleanup component.
Entity e = EntityManager.CreateEntity(
    typeof(Translation), typeof(Rotation), typeof(ExampleCleanup));

// Attempts to destroy the entity but, because the entity has a cleanup component, Unity doesn't actually destroy the entity. Instead, Unity just removes the Translation and Rotation components. 
EntityManager.DestroyEntity(e);

// The entity still exists so this demonstrates that you can still use the entity normally.
EntityManager.AddComponent<Translation>(e);

// Removes all the components from the entity. This destroys the entity.
EntityManager.DestroyEntity(e, new ComponentTypes(typeof(ExampleCleanup), typeof(Translation)));

// Demonstrates that the entity no longer exists. entityExists is false. 
bool entityExists = EntityManager.Exists(e);

EntityCommandBuffer

可以通过Buffer控制行为在指定位置响应 例如指定在开始模拟或模拟之后触发

EntityCommandBuffer entityCommandBuffer =
            SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);
            
EntityCommandBuffer entityCommandBuffer =
            SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);
            
Entity spawnedEntity = entityCommandBuffer.CreateEntity();
// 或者通过Entity进行实例化
Entity spawnedEntity = 
entityCommandBuffer.Instantiate(redSpawnerComponent.PlayerPrefab);
// 为实体设置LocalTransofrm组件(即Transofrm)
entityCommandBuffer.SetComponent(spawnedEntity, new LocalTransform()
            {
                Position = new float3(50f, 0.171f, 50f),
                Rotation = new quaternion(0f,0f,0f,0f),
                Scale = 1f
            });

但是要注意代码中CommandBuffer中创建的实体时还未正式创建实例化。所以此时spawnedEntity的Index值此时还为-1。真实的Index需要在响应位置真正创建好后才会设置为一个>0的数值 并更新版本

获取实体
// 获取当前Manager中所有拥有PlayerTag组件的实体队列
EntityQuery playerTagEntityQuery =
            EntityManager.CreateEntityQuery(typeof(PlayerTag));
// 将实体队列转换为一个实体数组副本
NativeArray<Entity> entityNativeArray =
            playerTagEntityQuery.ToEntityArray(Unity.Collections.Allocator.Temp);
// 后续操作
实体单例

对于获取全局只会存在一个的实例实体 可以使用

SystemAPI.GetSingletonEntity<Entity>();

Component

组件

组件起标记作用

创建新的组件时,需要为struct并继承 IComponentData

组件可以为空,也可以设置需要标记的属性

空组件时,其并不占用存储控件。但也可以像存在属性的组件一样做响应操作

// 借助Baker 将Target组件与实体进行关联

 public class TargetAuthoring : MonoBehaviour
    {
        class Baker : Baker<TargetAuthoring>
        {
            public override void Bake(TargetAuthoring authoring)
            {
                var entity = GetEntity(TransformUsageFlags.Dynamic);
                AddComponent<Target>(entity);
            }
        }
    }

    public struct Target : IComponentData
    {
        public Entity Value;
    }
创建 销毁 修改

同样可以使用EntityManager和EntityCommandBuffer来进行操作

EntityManager.AddComponent<Translation>(entity);

EntityCommandBuffer entityCommandBuffer =
            SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);

// 为实体设置LocalTransofrm组件(即Transofrm)
entityCommandBuffer.SetComponent(spawnedEntity, new LocalTransform()
            {
                Position = new float3(50f, 0.171f, 50f),
                Rotation = new quaternion(0f,0f,0f,0f),
                Scale = 1f
            });

RefRO<> RefRW<>

限制组件中属性为只读或可读写

ComponentTypeHandle
ComponentLookup
组件单例

// 获取一个可读写的组件单例

RefRW<RandomComponent> randomComponent = SystemAPI.GetSingletonRW<RandomComponent>();

System

驱动所有有需求的组件执行行为逻辑

SystemBase
public partial class MySystem : SystemBase 
{
	protected override void OnUpdate()
    {
        throw new System.NotImplementedException();
    }
}

限制只能运行在主线程

ISystem
    public partial struct MySystem : ISystem
    {
        // Called once when the system is created.
        // Can be omitted when empty.
        [BurstCompile]
        public void OnCreate(ref SystemState state)
        {
        }

        // Called once when the system is destroyed.
        // Can be omitted when empty.
        [BurstCompile]
        public void OnDestroy(ref SystemState state)
        {
        }

        // Usually called every frame. When exactly a system is updated
        // is determined by the system group to which it belongs.
        // Can be omitted when empty.
        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
        }

可以配合Job进行多线程执行任务

执行顺序

可以为System添加标签指定其相对于某一System的执行顺序

[UpdateBefore(typeof(AfterSystem))]

[UpdateAfter(typeof(AfterSystem))]
操作组件

官方示例 获取及操作组件队列数据

namespace ExampleCode.Queries
{
    public partial struct MySystem : ISystem
    {
        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            EntityQuery myQuery = SystemAPI.QueryBuilder().WithAll<Foo, Bar, Apple>().WithNone<Banana>().Build();
            ComponentTypeHandle<Foo> fooHandle = SystemAPI.GetComponentTypeHandle<Foo>();
            ComponentTypeHandle<Bar> barHandle = SystemAPI.GetComponentTypeHandle<Bar>();
            EntityTypeHandle entityHandle = SystemAPI.GetEntityTypeHandle();

            // Getting array copies of component values and entity ID's:
            {
                // Remember that Temp allocations do not need to be manually disposed.

                // Get an array of the Apple component values of all
                // entities that match the query.
                // This array is a *copy* of the data stored in the chunks.
                NativeArray<Apple> apples = myQuery.ToComponentDataArray<Apple>(Allocator.Temp);

                // Get an array of the ID's of all entities that match the query.
                // This array is a *copy* of the data stored in the chunks.
                NativeArray<Entity> entities = myQuery.ToEntityArray(Allocator.Temp);
            }

            // Getting the chunks matching a query and accessing the chunk data:
            {
                // Get an array of all chunks matching the query.
                NativeArray<ArchetypeChunk> chunks = myQuery.ToArchetypeChunkArray(Allocator.Temp);

                // Loop over all chunks matching the query.
                for (int i = 0, chunkCount = chunks.Length; i < chunkCount; i++)
                {
                    ArchetypeChunk chunk = chunks[i];

                    // The arrays returned by `GetNativeArray` are the very same arrays
                    // stored within the chunk, so you should not attempt to dispose of them.
                    NativeArray<Foo> foos = chunk.GetNativeArray(ref fooHandle);
                    NativeArray<Bar> bars = chunk.GetNativeArray(ref barHandle);

                    // Unlike component values, entity ID's should never be
                    // modified, so the array of entity ID's is always read only.
                    NativeArray<Entity> entities = chunk.GetNativeArray(entityHandle);

                    // Loop over all entities in the chunk.
                    for (int j = 0, entityCount = chunk.Count; j < entityCount; j++)
                    {
                        // Get the entity ID and Foo component of the individual entity.
                        Entity entity = entities[j];
                        Foo foo = foos[j];
                        Bar bar = bars[j];

                        // Set the Foo value.
                        foos[j] = new Foo { };
                    }
                }
            }

            // SystemAPI.Query:
            {
                // SystemAPI.Query provides a more convenient way to loop
                // through the entities matching a query. Source generation
                // translates this foreach into the functional equivalent
                // of the prior section. Understand that SystemAPI.Query
                // should ONLY be called as the 'in' clause of a foreach.

                // Each iteration processes one entity matching a query
                // that includes Foo, Bar, Apple and excludes Banana:
                // - 'foo' is assigned a read-write reference to the Foo component
                // - 'bar' is assigned a read-only reference to the Bar component
                // - 'entity' is assigned the entity ID
                foreach (var (foo, bar, entity) in
                         SystemAPI.Query<RefRW<Foo>, RefRO<Bar>>()
                             .WithAll<Apple>()
                             .WithNone<Banana>()
                             .WithEntityAccess())
                {
                    foo.ValueRW = new Foo { };
                }
            }
        }
    }
}

SystemGroup

System的上层管理节点 作用为对System做分类分组

可以对System添加标签 进入指定分组

[UpdateInGroup(typeof(MySystemGroup))]

Job + Burst

JobSystem 工作任务 可多线程并行执行任务

Job主要是利用CPU来降低计算负载,数量级上远不如GPU,但是比GPU更适合处理复杂逻辑

或者如果数据是从CPU发起也更适合,避免了数据拷贝到GPU的开销

Burst 会把IL变成使用LLVM优化的CPU代码,执行效率会大幅提升

主线程执行单一Job
namespace ExampleCode.IJobs
{
    // An example job which increments all the numbers of an array.
    public struct IncrementJob : IJob
    {
        // The data which a job needs to use should all
        // be included as fields of the struct.
        public NativeArray<float> Nums;
        public float Increment;

        // Execute() is called when the job runs.
        public void Execute()
        {
            for (int i = 0; i < Nums.Length; i++)
            {
                Nums[i] += Increment;
            }
        }
    }

    // A system that schedules the IJob.
    public partial struct MySystem : ISystem
    {
        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            var job = new IncrementJob
            {
                Nums = CollectionHelper.CreateNativeArray<float>(1000, state.WorldUpdateAllocator),
                Increment = 5f
            };

            JobHandle handle = job.Schedule();
            handle.Complete();
        }
    }
}
根据输入粒度切割的并行Job
namespace ExampleCode.IJobParallelFors
{
    // An example job which increments all the numbers of an array in parallel.
    public struct IncrementParallelJob : IJobParallelFor
    {
        // The data which a job needs to use must all
        // be included as fields of the struct.
        public NativeArray<float> Nums;
        public float Increment;

        // Execute(int) is called when the job runs.
        public void Execute(int index)
        {
            Nums[index] += Increment;
        }
    }

    // A system that schedules the IJobParallelFor.
    public partial struct MySystem : ISystem
    {
        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            var job = new IncrementParallelJob
            {
                Nums = new NativeArray<float>(1000, state.WorldUpdateAllocator),
                Increment = 5f
            };

            JobHandle handle = job.Schedule(
                job.Nums.Length, // number of times to call Execute
                64); // split the calls into batches of 64
            handle.Complete();
        }
    }
}
Chunk粒度的Job
namespace ExampleCode.IJobChunks
{
    // An example IJobChunk.
    [BurstCompile]
    public struct MyIJobChunk : IJobChunk
    {
        // The job needs type handles for each component type
        // it will access from the chunks.
        public ComponentTypeHandle<Foo> FooHandle;

        // Handles for components that will only be read should be
        // marked with [ReadOnly].
        [ReadOnly] public ComponentTypeHandle<Bar> BarHandle;

        // The entity type handle is needed if we
        // want to read the entity ID's.
        public EntityTypeHandle EntityHandle;

        // Jobs should not use an EntityManager to create and modify
        // entities directly. Instead, a job can record commands into
        // an EntityCommandBuffer to be played back later on the
        // main thread at some point after the job has been completed.
        // If the job will be scheduled with ScheduleParallel(),
        // we must use an EntityCommandBuffer.ParallelWriter.
        public EntityCommandBuffer.ParallelWriter Ecb;

        // When this job runs, Execute() will be called once for each
        // chunk matching the query that was passed to Schedule().

        // The useEnableMask param is true if any of the
        // entities in the chunk have disabled components
        // of the query. In other words, this param is true
        // if any entities in the chunk should be skipped over.

        // The chunkEnabledMask identifies which entities
        // have all components of the query enabled, i.e. which entities
        // should be processed:
        //   - A set bit indicates the entity should be processed.
        //   - A cleared bit indicates the entity has one or more
        //     disabled components and so should be skipped.

        // The `unfilteredChunkIndex` is the index of the chunk in the sequence of all chunks matching the query: the first
        // chunk matching the query is index 0, the second is index 1, and so forth. This value is mainly useful as
        // a *sort key* passed to the methods of `EntityCommandBuffer.ParallelWriter`. Each recorded command includes
        // a sortKey, and in playback, the commands are sorted by these keys before the commands are executed.
        // This sorting effectively guarantees the commands will execute in a deterministic order even though the
        // original recorded order of the commands was non-deterministic.
        [BurstCompile]
        public void Execute(in ArchetypeChunk chunk,
            int unfilteredChunkIndex,
            bool useEnableMask,
            in v128 chunkEnabledMask)
        {
            // Get the entity ID and component arrays from the chunk.
            NativeArray<Entity> entities = chunk.GetNativeArray(EntityHandle);
            NativeArray<Foo> foos = chunk.GetNativeArray(ref FooHandle);
            NativeArray<Bar> bars = chunk.GetNativeArray(ref BarHandle);

            // The ChunkEntityEnumerator helps us loop over
            // the entities of the chunk, but only those that
            // match the query (accounting for disabled components).
            var enumerator = new ChunkEntityEnumerator(useEnableMask, chunkEnabledMask, chunk.Count);

            // Loop over all entities in the chunk that match the query.
            while (enumerator.NextEntityIndex(out var i))
            {
                // Read the entity ID and component values.
                var entity = entities[i];
                var foo = foos[i];
                var bar = bars[i];

                // If the Bar value meets a criteria, we
                // record a command in the ECB to remove it.
                if (bar.Value < 0)
                {
                    Ecb.RemoveComponent<Bar>(unfilteredChunkIndex, entity);
                }

                // Set the Foo value.
                foos[i] = new Foo { };
            }
        }
    }

    // A system that schedules and completes the above IJobChunk.
    public partial struct MySystem : ISystem
    {
        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            // Get an EntityCommandBuffer from
            // the BeginSimulationEntityCommandBufferSystem.
            var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
            var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);

            // Create the job.
            var job = new MyIJobChunk
            {
                FooHandle = state.GetComponentTypeHandle<Foo>(false),
                BarHandle = state.GetComponentTypeHandle<Bar>(true),
                Ecb = ecb.AsParallelWriter()
            };

            var myQuery = SystemAPI.QueryBuilder().WithAll<Foo, Bar, Apple>().WithNone<Banana>().Build();

            // Schedule the job.
            // By calling ScheduleParallel() instead of Schedule(),
            // the chunks matching the job's query will be split up
            // into batches, and these batches may be processed
            // in parallel by the worker threads.
            // We pass state.Dependency to ensure that this job depends upon
            // any overlapping jobs scheduled in prior system updates.
            // We assign the returned handle to state.Dependency to ensure
            // that this job is passed as a dependency to other systems.
            state.Dependency = job.ScheduleParallel(myQuery, state.Dependency);
        }
    }
}
实体粒度的Job
namespace ExampleCode.IJobEntitys
{
    // An example IJobEntity that is functionally equivalent to the IJobChunk above.
    // An `IJobEntity` is more concise than its `IJobChunk` equivalent because
    // its source generation takes care of some boilerplate.

    // Only entities having the Apple component type will match the job's implicit query
    // even though the job does not access the Apple component values.
    [WithAll(typeof(Apple))]
    // Only entities NOT having the Banana component type will match the job's implicit query.
    [WithNone(typeof(Banana))]
    [BurstCompile]
    public partial struct MyIJobEntity : IJobEntity
    {
        // Thanks to source generation, an IJobEntity gets the type handles
        // it needs automatically, so we do not include them manually.

        // EntityCommandBuffers and other fields still must
        // be included manually.
        public EntityCommandBuffer.ParallelWriter Ecb;

        // Source generation will create an EntityQuery based on the
        // parameters of Execute(). In this case, the generated query will
        // match all entities having a Foo and Bar component.
        //   - When this job runs, Execute() will be called once
        //     for each entity matching the query.
        //   - Any entity with a disabled Foo or Bar will be skipped.
        //   - 'ref' param components are read-write
        //   - 'in' param components are read-only
        //   - We need to pass the chunk index as a sortKey to methods of
        //     the EntityCommandBuffer.ParallelWriter, so we include an
        //     int parameter with the [ChunkIndexInQuery] attribute.
        [BurstCompile]
        public void Execute([ChunkIndexInQuery] int chunkIndex, Entity entity, ref Foo foo, in Bar bar)
        {
            // If the Bar value meets this criteria, we
            // record a command in the ECB to remove it.
            if (bar.Value < 0)
            {
                Ecb.RemoveComponent<Bar>(chunkIndex, entity);
            }

            // Set the Foo value.
            foo = new Foo { };
        }
    }

    // A system that schedules and completes the above IJobEntity.
    public partial struct MySystem : ISystem
    {
        // We don't need to create the query manually because source generation
        // creates one inferred from the IJobEntity's attributes and Execute params.

        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            // Get an EntityCommandBuffer from the BeginSimulationEntityCommandBufferSystem.
            var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
            var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);

            // Create the job.
            var job = new MyIJobEntity
            {
                Ecb = ecb.AsParallelWriter()
            };

            // Schedule the job. Source generation creates and passes the query implicitly.
            state.Dependency = job.Schedule(state.Dependency);
        }
    }
}
Job的依赖

var job = new MyJob{ Nums = myArray};

var otherJob = new MyJob{ Nums = myArray};

JobHandle handle = job.Schedule(); //Schedule 和Complete 只能在主线程执行

handle.Complete(); //Job完成后才会返回

JobHandle otherHandle = otherJob.Schedule();

//必须要在handle.Complete 后执行 因为引用同一数组 同时Schedule 会发生竞争行为

或者

将第一个handle 作为句柄传递给第二个调度调用来使第二个Job依赖第一个Job的完成。所以当第一个Job未完成时 第二个Job不会从Job队列中取出

JobHandle otherHandle = otherJob.Schedule(handle);

handle.Complete();

otherHandle.Complete();

// 可以使用CombineDependencies 时单个Job依赖多个JobHandle

JobHandle combined = JobHandle.CombineDependencies(…);

JobHandle handleA = jobA.Schedule(combined);

在这里插入图片描述

注意事项

注意避免循环依赖,造成死锁

Job只能依赖于在其之前调度的Job

并且一旦调度 Job无法更改依赖关系

不允许在Native容器里添加托管类型

不可以访问静态变量


暂时先写这些大体 后期再补充细节吧

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值