Unity Dots
Unity Dots
什么是 DOTS
充分利用多核处理器,多线程处理让游戏运行速度更快,更高效,DOTS(面向数据的技术堆栈)是以下 3 个系统的集合
- Job Systems 任务系统 用于高效运行多线程代码
- Entity Component System(ECS)实体组件系统 用于默认编写高新能代码
- Burst Compiler burst 编译器 用于生成高度优化的本地代码,Unity 使用 Burst 编译器可以使 C#代码运行效率高于 c++
Job Systems 和 ECS 可以独立使用,组合在一起才能发挥最大优势。
C# job systems
- 在 job systems 之前,Unity 内部是多线程处理,但是外部代码必须在主线程上。
- c# 虽然支持 thread 但是 Unity 中只能处理数据,例如网络消息,下载,但是不能在 Tread 中调用 Unity API
- 有了 Job System 可以充分利用多线程,例如在多线程中处理 Transform 旋转,缩放,平移。
- Unity 没有直接将 Tread 开放出来,这样可以避免 Tread 滥用,开发者可以放心使用 Job Systems 而不用太关心线程安全,锁
- Job System 最好配合 burst 编译器,这样可以生成本地高效代码。
- Job System 中的数据类型,只能是值类型(float,int,bool),enum,struct 和其他类型的指针
- Job System 不能使用引用类型,例如 T[]
- Job 中可以使用 NativeArray
代替 T[]
HPC# High Performance C#
- C# class 类型数据的数据内存分配在堆上,程序员无法主动释放,必须等到.NET 进行垃圾回收才会释放。
- IL2CPP 虽然将 IL 转成 c++代码,实际上还是模拟了.Net 垃圾回收机制,效率并没有等同与 c++
- HPC# 就是 NativeArrary
,NativeHashmap 可替代数组[] 数据类型 包括值类型(float,uint,bool...) - NativeArray 可以在 c#层分配 c++中对象,可以主动释放,不需要进行 c#的垃圾回收
- Job System 使用的就是 NateiveArray
IJOBPARALLELFOR 并行
- IJOB 是一个一个开线程任务,因为数据是顺序执行的,所以它可以保证数据的正确性
- 使用 IJOBPARALLELFOR 线程是并行执行的,因此数据执行不是顺序执行的,每一个 JOB 数据不能完全依赖上一个 JOB 执行后的结果
- “ReadOnly” 意味着数据是只读的,不需要加锁
- 如果不声明默认数据是 Read/Write,数据一旦改写,Job 一定要等它
- Unity 已经为我们做好着些,不需要我们再写加解锁逻辑
Unity.MathEMatics
- 提供矢量类型,可以直接映射到 SIMD 寄存器
- 有了 SIMD,CPU 可以一次计算
- unity 之前的 math 类 默认不支持 simd
启动 BurstComile
- Struct 上加上 BurstCmoplie 标签
- Struct 必须继承 IJob,IJobParallelfor,否则无效
ECS
旧版本的一些总结
- 设计模式-组件模式
- 脚本数据在内存中是不连续的
- 脚本中没必要的数据也提供了,每个 GameObject 都包含了 Transform,这些额外的数据也会占用内存
- 脚本和脚本是偶尔关系,会出现相互调用
- 脚本属于引用类型,也就是全局声明的所有变量都会占用堆内存,会产生 GC
- 脚本是非常重量的产物,手机上挂脚本会额外占用 0.01s
- 脚本不支持热更新
Entity
- Entity 非常轻量,它就是一个变量 ID,用 int 保存
- Entity 可以根据自己需求绑定组件,比如只有位置则只绑定 Position
- 相同组件会连续排列在内存中,Cache 命中增加,system 遍历速度增加
Component
- 组件是一个简单的数据存储,它时 struct 值类型,并且实现 IComponentData 接口,它不能写方法,没有任何行为,比如 position,rotation
- struct 中建议使用 float3 代替 vector3,quaternion 代替 Quaternion,原因就是 Unity.MathEMatics
ArcheType 原型
- Cache 命中增加就得益于 ArcheType
- ArcheType 是一个容器,并且 Unity 规定每一个大小都是 16kb,不够就再开一个,始终保持内存连续性
System
- System 只关心 Component 组件,它不关心这个 Component 属于哪个 Entity
- System 系统中定义它所关心的组件,组件都是连续保存在 ArcheType 中的,所以查找速度非常快
- System 在 Update 中可以同意更新自己关心的组件
ISharedComponentData
- IComponentData 是结构类型,如果大量结构体中,保存的数据一样,那么在内存中也会产生多分
- ISharedComponentData 是共享组件,典型的例子是场景中可能还有很多物体渲染的 mesh 和材质是相同的,如果 IComponentData 是浪费
- ISharedComponentData 组件需要实现 IEquatable
接口,用于判断两个组件是否相等
World
- World 包含 EntityManager,ComponentSystmes,ArcheTypes
- EntityManager 包含这个世界里所有的的 Entity,ComponentSystems 则包含世界里所有的组件,ArcheTyps 包含世界里所有的原型
- ECS 默认提供了一个世界,我们也可以自己创建
- 世界与世界不互通,每一个世界都是唯一的,多个世界可以同时并存
- World world =new World(“MyWorld”)
ECS+JOB+BURST
- ECS 里的 Component 已经具有超高的 Cache 命中率,性能比传统脚本快很多
- ComponentSystem 只能运行在主线程
- 每一个 ComponentSystem 只能等上一个运行完才能运行自己的
- ECS 要配合 job 才能让性能起飞
JOBCOMPONENTSYSTEM
- JobComponentSystem 继承于 ComponentSystem
- JobComponentSystem 的 update 和 ComponentSystem 一样都是执行完毕一个在执行下一个
- 区别 JobComponentSystem 的 update 可以很快就返回,将复杂的计算丢给 JOB 去做
- 通过 Readonly 将 job 标记为是否只读,只读的话 job 是完全可以并行的
- 如果 job 中某一个数据发生更改,那么这块数据这不能和访问这块数据的其他 job 并行
Dots 实践
- 使用 ComponentDataProxy 可以把一个 struc 绑定在游戏对象上
场景导出 ECS
- PackageManager 添加 Hybrid 组件
- Hybrid 的 Scene 组件可以把场景批量转换成 ECS 组件
prefab 导出 ECS
- 保存 Prefab,通过 GameObjectConaversionUtility.ConvertGameObjectHierarchy 直接将 prefab 转成 ECS 对象
- prefab 只能是普通的 mesh,如果带骨骼动画不能转成 ECS
ECS 渲染
- ECS 自身是不包含渲染的,但是游戏的渲染和 entity 是密切绑定的
- 原理大致是通过 ECS 在 JOB 中先准备渲染的数据,然后通过 Gpu instancing 一次渲染,中间不产生 GameObject
- Gpu instancing 是不带裁剪,并且每帧在 update 里调用,这样会产生额外开销,即使物体不发生位置旋转变化也会强制刷新
- 建议使用 CommanderBuffer 来渲染 GPU instancing,这样只有物体属性发生改变时强制刷新
- 使用 BatchRenderGroup 代替 Graphics.DrawMeshInstanced 和 CommandBuffer.DrawMeshInstanced
- BatchRenderGroup 需要提供每个渲染物体的包围盒区域用户 job 中判断是否在视野范围内
BatchRenderGroup 内都会调自动 GPU Instancing 前提是开启 gpu instancing,并且没有 1023 个数量限制
本文由作者按照 CC BY 4.0 进行授权