前言
2020年新年开了一个非常不好的头,澳洲丛林大火越发觉得全球变暖,两级冰川融化,蝗虫灾害,然而牵动国人的却是跟03年SARS类似的新型冠状肺炎病毒肆虐,已经导致三万多人感染,全国都处于一级戒备状态,搞的人心惶惶。病毒的源头还是野味,人类管不住自己的嘴,还有无良的商贩,这些种种灾害不都是人类自己造的孽?我们一直在谋发展,却不惜以破坏自然生态为代价,打破自然规律,现在开始遭到大自然的惩罚。
上面是跟本文无关的感叹,毁灭人类的不是天灾,而是人祸,是人类自己一手造成的,肯请人类善待大自然!
DOTS
DATA-ORIENTED TECH STACK(多线程数据导向型技术堆栈),是由ECS+JobSystem+Burst组成。
ECS
一种面向数据的开发模式-Entity(实体)、Component(组件)、System(系统)组成,命名也是取他们的首字母的缩写。
Entities实体
实体的本质是一个ID,可以视为轻量级的GameObject。
创建实体
使用EntityManager.CreateEntity创建实体,实体创建于EntityManager在相同的World中。
也可以通过以下方式逐个创建实体:
- EntityManager.CreateEntity(ComponentType[]),使用组件数组来创建实体。
- EntityManager.CreateEntity(EntityArchetype),使用实体原型来实体。
- EntityManager.Instantiate(Entity),复制现有实体(包括当前数据)。
也可以一次创建多个实体:
- 使用EntityManager.CreateEntity(EntityArchetype,NativeArray<Entity)将具有相同原型的新实体填充到NativeArray。
- 使用EntityManager.Instantiate(Entity,NativeArray
)向NativeArray填充现有实体的副本,包括其当前数据。 - 使用EntityManager.CreateChun(EntityArchetype,NativeArray
,Int32)根据指定的实体原型,指定的个数,以构成一个实体组(ArchetypeChunk)的形式添加到NativeArray。
World
一个World拥有一个EntityManager和一系列的ComponentSystems。
Components组件
ECS组件主要是代表数据,ECS的组件实现了下列”标记接口”之一的结构体:
- IComponentData
- ISharedComponentData
- ISystemStateComponentData
- ISharedSystemStateComponentData
ComponentData
ComponentData是一个包含entity的数据实体的结构体。ComponentData不能包含方法。ComponentData对应的接口名为IComponentData。IComponentData结构中补鞥呢包含对托管对象的引用。
Shared ComponentData
IComponentData适用于实体之间数据不同的情况,但有很多实体他们拥有相同的数据时,就要用到ISharedComponentData了。例如好多Cube的RenderMesh相同,例如:1
2
3
4
5
6
7
8
9[System.Serializable]
public struct RenderMesh : ISharedComponentData
{
public Mesh mesh;
public Material material;
public ShadowCastingMode castShadows;
public bool receiveShadows;
}
ISharedComponentData最大的有点是,每个实体的内存的开销理论上都是零。
关于SharedComponentData的一些重要说明
- 具有相同的SharedComponentData数据值的多个实体将被组合存在在一块(Chunks)中。SharedComponentData的索引将被一次性存储在Chunk中,而不是每个实体各存一次。因此在第一个SharedComponentData实体被构建时,已经产生此Chunk,并构建了SharedComponentData,在Chunk中保留了此SharedComponentData的引用,后续增加实体时,则不会因为其数据组件产生任何内存开销。
- 使用EntityQuery,我们可以遍历所有具有同类数据组件的实体。
- 我们可以使用EntityQuery.SetFilter()遍历具有指定的SharedComponentData值的实体。由于数据布局的原因,次遍历具有较低的开销。
- 使用EntityManager.GetAllUniqueSharedComponents,我们可以检索到已经鄂弼添加到任意活动实体的所有SharedComponentData特例。
- SharedComponentData是自动引用计数的。
- 尽可能的少修改SharedCompoonentData,修改SharedComponentData涉及到使用memcpy将该实体的所有ComponentData复制到另一个Chunk中。
ComponentSystems
Unity中ComponentSystem用于对实体(entities)执行操作。
JobSystem
- 在Job System对外之前,Unity虽然内部是多线程处理,但是外部代码必须泡在主线程上。
- C#虽然支持Thread,但是在Unity中只能处理数据,例如:网络消息、下载。如果想在Thread中调用Unity的API那是不行的。
- 有了Job System就可以充分利用CPU的多核,例如:在多线程中修改Transform旋转、缩放、平移。
- 例如:MMO游戏判断碰撞、大量的同步角色坐标、大量的血条飘字等都比较适合在Job System。
- Unity没有直接将Thread开放出来,可以有效避免Thread被滥用,开发者可放心使用Job而不用太多关心如线程安全、枷锁这些问题。
- Job最好配合Burst编译器,这样能生成高效的本地代码
- Job中数据类型只能是值类型
- Job中不能使用引用类型,T[]数组属于引用类型,所以无法在job中使用
- Job中使用NativeArray代替T[]
启动1000个Job 发现各个工作线程并不是同时执行的,但有个好处是确保数据是正确的。
HPC
高性能C#
- .NET Core比C++慢2倍
- Mono比.NET Core慢3倍
- IL2CPP比Mono快2-3倍,IL2CPP与.NET Core效率相当,但是依然比C++慢2倍
- Unity使用Burst编译后可以让C#代码的运行效率比C++更快
介绍
- C# 引用类型数据的内存分配在堆上,程序员无法主动释放,但必须等到.NET垃圾回收才可以真正清理。
- IL2CPP虽然将IL转成C++代码,实际上还是模拟了.NET的垃圾回收机制,所以效率并非等于Cpp
- HPC#就是NativeArray
可代替数组T[]数据类型包括值类型(float,int,uint,short,bool…),enums,structs和其他类型的指针 - NavtiveArray可以在C#层分配C++中的对象,可以主动释放不需要等C#的垃圾回收
- Job System中使用的就是NavtiveArray
验证
上图代码和内存图可见,创建NativeArray在堆内存上永远只有32B,而我们手动创建的数组就12.2KB的堆内存,差别还是很明显的。
添加组件
- Entities
- Mathematics
- Hybrid Renderer
- Jobs
效果图对比
1.用ECS创建10000个cube移动和传统方式创建10000cube移动帧率会有明显差异
2.利用Job System提高CPU的运算速度
可以看出利用JobSystem计算速度提升了10倍左右
再利用Burst更惊人,直接变成了0.2毫秒
性能提高了3000倍
3.在一组例子
能看出明显的帧率的变化
执行顺序
参考文档
https://connect.unity.com/p/unity-ecs-wu-liao-jie-systemzhi-xing-shun-xu
三个基本的ComponentSystemGroup
- InitializationSystemGroup 负责初始化工作
- SimulationSystemGroup 负责逻辑运算工作
- PresentationSystemGroup 负责结果与图形渲染工作
排序原则
- 根据其[UpdateBefore/After(typeof(MySystem))]属性。如果在进行排序时在同一组中找不到属于此属性的系统类型,则它无效,并且会向您发送警告。
- 如果没有[UpdateBefore/After],它将会尝试分配到合适的位置,但仍确保那些具有[UpdateBefore/After]属性的System顺序不会改动。
- 如果该ComponentSystemGroup包含另一个ComponentSystemGroup,则将对其进行递归排序。
- 它可以检测到您的循环依赖关系[UpdateBefore/After]并记录信息。
添加标签改变执行顺序
1 | [UpdateInGroup(typeof(InitializationSystemGroup))] |
1 | [UpdateInGroup(typeof(SimulationSystemGroup))] |
更改标签1
2
3
4
5
6
7
8
9[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateAfter(typeof(SequenceSystemB))]
public class SequenceSystemA : ComponentSystem
{
protected override void OnUpdate()
{
Debug.Log("SequenceSystemA Updating");
}
}
1 | [UpdateInGroup(typeof(SimulationSystemGroup))] |
C#任务系统
- Job中数据类型只能是值类型
- Job中不能使用引用类型,T[]数组属于引用类型,所以无法在job中使用
- Job中使用NativeArray
代替T[]
启动1000个Job
发现各个工作线程并不是同时执行的,但有个好处是确保数据是正确的。
IJobParallelFor(Job并行执行)
- IJOB是一个一个的开县城任务,因为数据是顺序执行的所以他可以确保数据正确性。
- 如果想让线程任务真正的并行,那么可以采用IJobParallelFor
- 一旦线程任务并行的话,就意味着数据的执行下顺序不是线性的,每一个Job里的数据不能完全依赖上一个Job执行后的结果。
- [ReadOnly]声明数据是否制度,如果数据是只读的,意味着这个数据不需要加锁。
- 如果不声明默认数据是Read/Write的,数据一旦需要修改,那么Job就一定要等它。
- 然而这一切Unity都已经帮我们做好,不需要自己做加解锁逻辑。
可以看出完全并行执行了。
Burst编译器原理
- Burst编译器是以LLVM为记住的后端编译技术
- 编译器的原理会分为5个步骤:源代码->前端->优化器->后端->机器码
- LLVM的定义了个抽象语言IR,前端负责将源码C#编译成IR,优化器负责优化IR,后端负责将IR生成目标语言这里就是机器码
- 正是因为抽象语言IR的存在,所以LLVM支持的语言很多,而且也方便扩展C#、ActionScript、OC、Python、Swift等语言。
- LLVM代码是开源了,所以Unity很适合用它来做Burst的编译。
- 遗憾的是LLVM对C#的GC做的不好,所以Burst只支持值类型的数据编译,不支持引用类型的数据编译。
Unity.Mathematics数学库
- Unity.Mathematics提供了矢量类型(float4,flaot3),它可以直接银蛇到硬件SIMD寄存器。
- Unity.mathematics的Math类中也提供了直接银蛇到硬件SIMD寄存器。
- 这样原本CPU需要一个个计算的,有了SIMD可以一次性计算完毕。
- 需要注意的是Unity之前的Math类默认是不支持映射SIMD寄存器的。
启动BurstCompile
在Struct上添加上BurstCompile标签,Struct必须继承IJob类,否则无效。
Component组件
- 组件是一个简单的数据存储,它是struct值类型,并且实现IComponentData接口,它不能写方法,没有任何行为,比如前面提到的Position或者Rotation。
- struct中建议使用float3代替Vector3、quaternion代替Quaternion,原因就是我们之前提到了Unity重写的数学库。
- Unity.Mathematics的math类中也提供了直接映射到硬件SIMD寄存器。
相同类型的组件会连续排布,提高cache命中率。
Archetype原型
- 遍历组件之所以特别高效,得益于Archetype原型。
- Archetype是一个容器,并且Unity规定每一个Archetype的大小是16kb,不够就再开一个,始终保持内存的连续性。
- 如果我们在同一帧创建了100个实体,每个实体都有Position和Rotation组件,因为他们都用了相同的ArcheType所以他们在内存中都是连续的。
System系统
- System系统只关心Component组件,并不关心Component到底属于哪个Entity。
- System系统中定义它所关心的组件,组件都连续的保存在ArcheType中,所以查找速度非常快。
- System系统中在Update里可以统一更新自己关心的组件。
ISharedComponentData共享组件
- IComponentData是结构体,结构体本身就是值类型数据,如果大量结构体中保存到 数据完全相同,那么在内存中也会产生多份。
- ISharedComponentData表示共享组件,比较典型的例子就是场景中可能有很多物体渲染时的mesh和材质是相同的,如果使用IcomponentData来保存这就是内存的白白浪费。ISharedComponentData组件需要实现IEquateble
接口,用于判断两个组件是否相等。
World世界
- World包含EntityManager(实体管理器)、ComponentSystem(组件系统)、ArcheTypes(原型)
- 注意EntityManager包含的是这个世界里所有的Entity、ComponentSystem则包含世界里所有的组件、ArcheTypes包含世界里所有的原型。
- ECS默认提供了一个世界,我们也可以自己创建多个世界。
- 世界与世界之间不具备互通性,每个世界都是唯一的,多个世界则可以同时并行。
- World world = new World(“MyWorld”);可以new一个新的世界。
ECS+JOB+BURST让性能飞起来
- ECS里的Component已经具有超高的Cache命中率,性能已经比传统的脚本方式快了不少。
- 但是ComponentSystem知识运行在主线程中。
- 这就意味着每个ComponentSystem都必须等上一个执行完毕才能执行自己的。
- ECS要配合JobSystem才能让性能飞起来。
JobComponentSystem
- JobComponentSystem继承ComponentSystem。
- JobCompoonentSystem的Update和ComponentSystem一样都是一个执行完毕再执行喜爱一个。
- 区别是JobComponentSystem的Update可以很快就返回,将复杂的计算丢给Job去完成。
- 通过[ReadOnly]标志job中的数据是否为只读,只读的话Job是可以完全并行的。
- 如果Job中某一个数据发声修改,那么这块数据就不能访问这块数据其他的Job并行运行。
DOTS实践-删除游戏对象
- 给一个空的GameObject绑定渲染组件和旋转组件。
- 以前需要给GameObject板顶MeshFilter和MeshRender组件,现在都不需要了。
- 使用ComponentDataProxy可以把一个struct绑定在游戏对象上。
彻底删除游戏对象
- Hierachy中现在仅有一个空的游戏对象,通过proxy将渲染、渲染组件绑定。
- 进一步优化,把这个空游戏对象也彻底从Hierarchy视图中删除。
场景导出ECS
点击新生成的脚本中的Close按钮
Prefab导出ECS
保存Prefab,通过GameObjectConversionUtility.ConvertGameObjectHierarchy直接将Prefab加载成ECS对象,注意Prefab只能是普通Mesh,如果带有谷歌动画是不能转成ECS的。1
2
3var manager = World.Active.EntityManager;
Entity entity = GameObejctConversionUtility.ConvertGameObjectHierarchy(prefab,World.Active);
Entity go = manager.Instantiate(entity);
ECS渲染
- ECS本身是不包含渲染的,但是游戏的渲染其实跟实体Entity是紧密绑定的。
- 原理大致是通过ECS在JOB中先准备渲染的数据,然后通过GPU Instancing一次渲染,中间不产生GameObject。
- GPU Instancing是不待裁剪的,并且需要每帧在Update里调用,这样会产生额外的开销,即使物体不发生位置或者属性变化都需要强制刷新。
- 建议使用CommandBuffer来渲染GPU Instancing,这样只有当物体位置或者属性发声改变再新型
- 强制刷新。
BatchRendererGroup
GPU Instancing烘焙贴图
- Lightmap红配贴图需要用到UV2
- UV1是Mesh从自身贴图中采样,UV2则是Mesh从Lightmap烘焙贴图采样,最后在PS里将烘焙颜色叠加在一起,Mesh就能显示烘焙样色了。
- 如果常经理大量相同的网格,但是由于红配后UV2不同,这样网格就不相同了,将大量占内存。
- 所以Unity采用的方式是模型导入Unity后会统一生成相同的UV2,每个Rnederer组件需要制定Lightmap的贴图ID以及Vector4偏移量,这个偏移量需要传入shader中,GPU中根据UV2加上偏移量后再去Lightmap中采样颜色。
- 这样做完网格都可以大量重用,知识每个Renderer组件增加一个id和一个vector4。
Unity官方案例
- https://github.com/Unity-Technologies/EntityComponentSystemSamples
- https://github.com/Unity-Technologies/DOTSSample
- https://github.com/Unity-Technologies/Animation-Instancing
- https://github.com/Unity-Technologies/animation-jobs-samples
- https://github.com/UnityTechnologies/AngryBots_ECS
- https://github.com/Unity-Technologies/Unity.Mathematics
更多资料
- https://github.com/dingxiaowei/Unity_ECS_HelloWorld 我整理的DOTS学习笔记
- https://docs.unity3d.com/Packages/com.unity.entities@0.5/manual/ecs_entities.html 官方ECS教程
- https://www.bilibili.com/video/av79798154?from=search&seid=14615607387336506986 宣雨淞的DOTS分享
- https://connect.unity.com/p/unityecs-qian-yan
- https://unity.com/cn/dots/packages Unity官方介绍
- http://www.benmutou.com/archives/category/unity3d/ecs/unityecs_beginner 笨木头的ECS课程
- https://blog.csdn.net/qq_30137245/category_9213857.html Cloudhu的教程
- https://blog.csdn.net/andrewfan/category_8967683.html 官方ECS教程翻译
- https://www.bilibili.com/video/av86966089?from=search&seid=8151346948396978636 Unity Dots Physics
- http://dingxiaowei.cn/
- https://blog.codingnow.com/2017/06/overwatch_ecs.html 云风大佬ECS的介绍