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官方文档
1095

被折叠的 条评论
为什么被折叠?



