文章

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 进行授权