ECS学习笔记 Lesson2 <使用Job生成Cube>

System生成Cube和Job+CommandBuffer生成Cube的区别

System方式

  • 代码简洁
  • 运行在主线程,生成数量较多时,会引起卡顿
  • 频繁调用api,导致缓存命中降低
  • 存在线程安全隐患

Job+CommandBuffer方式

  • 代码较复杂
  • 并行运行在多个job子线程
  • 数据布局连续缓存命中高
  • CommandBuffer生成指缓存池,在主线程统一提交,保证线程安全

提示:System也可以使用CommandBuffer来保证线程安全。另外使用Job的方式不一定能够带来性能上的提升,因为使用Job存在线程调度本身的开销。需要衡量从生成cube获得的收益以及多线程开销之间的关系,以后的课程中会对这一点进行更深入的探讨

修改Lesson1的代码

添加命名空间

  • 确保所有代码都在Learn.Lesson2.Scripts命名空间下,作者由于ide配置了根据文件夹添加命名空间,且都是在ide中创建代码文件,所以会默认添加上命名空间,目的是避免Lesson2拷贝复用代码后出现冲突,以后的文章都会遵从这个规则

判断System的OnUpdate是否需要运行,避免获取不存在的组件报错。这里采用添加RequireForUpdate方式:

  • 打开SpawnerSystem.cs
  • 给SpawnerSystem添加特性[RequireMatchingQueriesForUpdate],在OnCreate方法中添加
  •    state.RequireForUpdate<RandomCom>(); 
       state.RequireForUpdate<Spawner>();
    

在这里插入图片描述

复用Lesson1的代码,构建Lesson2场景

  • 构建跟Lesson1相同的目录结构,将Lesson1中的代码拷贝到Lesson2中。并将所有代码命名空间修改为Learn.Lesson2.Scripts

  • 将Lesson1/Res的cube拷贝到Lesson2/Res下

  • 在Prject窗口Lesson2/Scene目录下,创建新的场景,名称修改为JobExample

  • 打开JobExample场景,新增子场景。子场景下创建空的节点,命名为Authoring

  • Authoring节点上分别挂载Lesson2的SpawnerAuthoring和RandomAuthoring, 并将Lesson2中的cube关联到SpawnerAuthoring.Prefab字段上,SpawnRate修改为3
    在这里插入图片描述

  • 运行,看到跟Lesson1的结构一致,至此构建Lesson2场景完毕

再次说明,之所以要给Lesson1的代码加上命名空间和给system加上RequireForUpdate,均是避免代码拷贝后运行报错。可以试下,如果不给Learn.Lesson1.Scripts.SpawnerSystem加RequireForUpdate,运行时会报下面错误,获取不到Learn.Lesson1.Scripts.RandomCom组件。而获取这个组件的地方为Learn.Lesson1.Scripts.SpawnerSystem.OnUpdate(),说明这个system一直在运行,这跟传统monobehaviour.Update()不太一样,后者需要挂载到场景中,但是system不需要,system只要代码存在,运行时就会一直在运行,这一点对于初学者来说很容易忽视。通过这里可以学习到,我们不光可以用RequireForUpdate来避免报错,也可以用其来控制system的OnUpdate执行的时机,在不需要的时候不运行,比通过空引用判断性能要更好,这个一个常见的性能优化技巧
在这里插入图片描述

将System方式修改为Job+CommandBuffer方式

实现ProcessSpawnerJob

  • 打开SpawnerSystem.cs

  • 定义修饰符为partial的结构体ProcessSpawnerJob,并继承IJobEntity。IJobEntity可以替代SystemAPI.Query,是job中用来query数据的另一种方式

  • 增加字段public RefRW Random用来算随机数,注意需要在前面添加特性[NativeDisableUnsafePtrRestriction]。因为RefRW是ECS提供的引用包装器,其内部直接通过指针操作组件内存地址,绕过常规内存安全检查,是一个unsafe Ptr。为了保证访问安全,Job默认限制使用unsafe Ptr,运行时会报错,报错信息见下图。但如果我们能确保多个job同时对unsafe Ptr的访问是安全的,那么可以加[NativeDisableUnsafePtrRestriction]来解除这个限制。而Unity.Mathematics.Random本身是线程安全的,所以我们加上这个特性。在以后的项目中,这个特性也会经常使用到,用来提升性能,但是需要确保数据访问的安全性
    在这里插入图片描述

  • 增加字段public EntityCommandBuffer.ParallelWriter Ecb。在Job中,是不允许进行structual change操作的,例如创建和销毁entity,或者增加删除component等。但ECS为我们提供了EntityCommandBuffer,其为ECS处理structual change的核心机制,举具备两个特征:

    • 允许在Job中记录实体创建/销毁等结构性变更命令,通过主线程Playback统一执行,解决Job中无法创建entity等问题
    • 将分散的结构性变更合并为批量操作,减少因频繁调用EntityManager引发的同步等待,提升性能
  • 增加系统时间字段public double ElapsedTime

EntityCommandBuffer.ParallelWriter和EntityCommandBuffer的区别

EntityCommandBuffer.ParallelWriter

  • 按内存块(Chunk)划分Job
  • 每个Chunk生成一个Job,映射到多核并行处理
  • 适合大规模数据,减少调度开销
  • Job被ScheduleParallel()多线程调用时使用
  • 不需要手动调用Playback和释放内存

EntityCommandBuffer

  • 按迭代次数划分Job
  • 每个迭代生成独立Job
  • 适合少量迭代
  • Job被Schedule()调用时使用
  • 需要手动调用Playback和释放内存

实现Execute方法

  • Execute方法是Job的核心执行入口。在Job被Schedule()或ScheduleParallel()调用后,Execute方法会被工作线程分块执行,如何分块可参考EntityCommandBuffer.ParallelWriter和EntityCommandBuffer的区别。由于我们采用EntityCommandBuffer.ParallelWriter方式,则每个块为一个Chunk。这样可以保证数据访问和写入的安全性,避免竞争
  • 增加参数[ChunkIndexInQuery] int chunkIndex,其即为当前执行的chunk的索引。增加这个的目的是,让ECB(EntityCommandBuffer)记录每次api是被哪个chunk调用的,在playback的时候,也会按照这个索引顺序来执行,保证时序上的正确性,避免因并行乱序导致逻辑错误(如先删除再创建同一实体)。这个参数也只在调用EntityCommandBuffer.ParallelWriter的api时需要,调用EntityCommandBuffer的api则不需要
  • 增加参数ref Spawner spawner,表明按照Spawner组件查询生成Job
  • 使用Ecb.Instantiate和Ecb.SetComponent替代state.EntityManager.Instantiate和state.EntityManager.SetComponentData。这个替换不光可以在Job中使用,也可以在system中使用。比如在system中,有前后几次不连续的EntityManager api调用,因为这发生在主线程,每次调用都会产生一个同步点(sync points)。如果使用ECB替代EntityManager,将多个同步点汇总成一个,就可以减少性能开销,这也是性能优化的常见技巧

同步点(sync points)是 ECS 架构中强制主线程等待所有已调度 Job 完成的机制,用于确保数据一致性。当发生结构性修改(Structural Changes)时,ECS 必须暂停并行任务执行,并在主线程同步数据状态

修改SpawnerSystem

增加方法GetEntiyCommandBuffer

  • 获取BeginSimulationEntityCommandBufferSystem组件的全局唯一实例,BeginSimulationEntityCommandBufferSystem是EntityCommandBufferSystem的子类,其属于SimulationSystemGroup。SimulationSystemGroup在每帧模拟阶段开始时执行,关于SystemGroup更多信息以后会讲到,现在可以理解为在一个unity生命周期内,用来管理system执行时机的工具。所有在 SimulationSystemGroup中注册的 ECB 命令都会在模拟逻辑前完成执行,一般用在处理需要在物理/逻辑计算前完成的结构性变更(如实体创建、组件增删)
  • 通过BeginSimulationEntityCommandBufferSystem实例创建一个ECB
  • 将ECB转换成ParallelWriter返回

修改OnUpdate方法

  • 删除foreach代码块
  • 调用GetEntiyCommandBuffer获取EntityCommandBuffer.ParallelWriter
  • new一个ProcessSpawnerJob,并使用ScheduleParallel进行调用

运行,结果跟Lesson1一致

在这里插入图片描述

修改后完整的System代码

using Unity.Entities;
using Unity.Burst;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Mathematics;
using Unity.Transforms;

namespace Learn.Lesson2.Scripts
{
    [BurstCompile]
    [RequireMatchingQueriesForUpdate]
    public partial struct SpawnerSystem : ISystem
    {
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<RandomCom>();
            state.RequireForUpdate<Spawner>();
            state.RequireForUpdate<BeginSimulationEntityCommandBufferSystem.Singleton>();
        }

        public void OnDestroy(ref SystemState state)
        {
        }

        public void OnUpdate(ref SystemState state)
        {
            // 获取RandomCom组件
            var random = SystemAPI.GetSingletonRW<RandomCom>();
            
            EntityCommandBuffer.ParallelWriter ecb = GetEntiyCommandBuffer(ref state);

            new ProcessSpawnerJob
            {
                Random = random,
                Ecb = ecb,
                ElapsedTime = SystemAPI.Time.ElapsedTime
            }.ScheduleParallel();
        }

        private EntityCommandBuffer.ParallelWriter GetEntiyCommandBuffer(ref SystemState state)
        {
            var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
            var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
            return ecb.AsParallelWriter();
        }
    }

    [BurstCompile]
    public partial struct ProcessSpawnerJob : IJobEntity
    {
        [NativeDisableUnsafePtrRestriction]public RefRW<RandomCom> Random;
        public EntityCommandBuffer.ParallelWriter Ecb;
        public double ElapsedTime;
        
        private void Execute([ChunkIndexInQuery] int chunkIndex, ref Spawner spawner)
        {
            if (spawner.NextSpawnTime < ElapsedTime)
            {
                Entity newEntity = Ecb.Instantiate(chunkIndex, spawner.Prefab);
                float3 pos = new float3(Random.ValueRW.random.NextFloat(-5f,5f), Random.ValueRW.random.NextFloat(-3f,3f),0);
                Ecb.SetComponent(chunkIndex, newEntity, LocalTransform.FromPosition(pos));
                spawner.NextSpawnTime = (float)ElapsedTime + spawner.SpawnRate;
            }
        }
    }
}

GitHub路径

  • https://github.com/BenTuLX/LearnECS

参考资料

  • unity官方文档
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值