查看: 130|回复: 15

[简易教程] Unity ECS编程官方文档选译——Getting Started

[复制链接]

750

主题

76

听众

7784

积分

头头

Rank: 12Rank: 12Rank: 12

TA的排名榜

积分:NO. 3 名

发帖:NO. 1 名

在线:NO. 2 名

发表于 2021-2-5 10:28 |显示全部楼层
译注:Unity项目往往稍微大点就会有性能瓶颈。Unity 2018提出了面向数据编程模式的ECS系统,结合新的安全的多线程机制Job System来解决这个问题,来取代过去的单线程、面向对象的GameObject/MonoBehaviour模式。

一、前言



1,我们试图解决什么问题?


当用 GameObject/MonoBehaviour模式做应用时,很容易编写代码,但结果却是难以阅读、难以维护、难以优化。这是多种因素综合导致的,包括:面向对象模式、由Mono编译的非机器码、垃圾回收和单线程编程。


2,使用Entity-Component-System(ECS)来拯救你的工程


ECS是一种编写代码的新方式,着重于你真正该解决的问题:构成你应用的数据(data)和行为(behavior)。

译注:所谓的行为,具体来说就是方法。

除了从设计角度讲这是种更好地编程方式之外,使用ECS还可让你发挥Unity Job System和Burst编译器的功力,充分利用当今的多核处理器。
我们发布了Unity原生的Job System,用户可以使用它并结合ECS C#脚本来获得多线程批处理的性能优势。这套Job System内置了用于检测线程竞争条件(race condition)的安全功能。
所以,我们需要引入一种新的思维和编码方式,以充分发挥Job System的优势。


二、什么是ESC?



1,MonoBehavior —— 亲切的老朋友


MonoBehaviours既包含数据也包含行为。一个进行旋转的简单例子:

  1. class Rotator : MonoBehaviour
  2. {
  3.         //数据-可以在编辑器面板里编辑值
  4.         public float speed;
  5.         //行为-从这个Coponent中读取speed,然后根据它改变Transform的rotation
  6.         void Update()
  7.         {
  8.                 transform.rotation *= Quaternion.AxisAngle(Time.deltaTime * speed, Vector3.up);
  9.         }
  10. }
复制代码

但是,MonoBehaviour继承了一大堆类;每个都包含了它自己的一大套数据——不管我们用不用得到。因此,我们无缘无故浪费了许多内存。所以,我们得想一想到底需要哪些数据,然后好好优化一下我们的代码。

译注:继承是面向对象编程的三大特征之一,同时也是一大缺点,它导致我们继承了一些无用的数据,浪费了太多内存,内存命中率低下。因此,我们不得不放弃过去的面向对象编程方式,用面向数据编程来进行连续紧凑的内存布局,以提高内存命中率。

2,ComponentSystem——迈入新领域的第一步


在我们的新模式中,一个Component只包含数据,而不包含行为。ComponentSystem才会包含行为,它负责用一组匹配的Component来更新所有GameObject(这些Component也不同于过去继承自MonoBehaviour的组件,它们是用结构struct体定义的,而非类class)。
用ComponentSystem实现上文MonoBehavior 相同的功能:
  1. private class Rotator : MonoBehaviour
  2. {
  3.     //数据 - 可以在编辑器面板里编辑值
  4.     public float Speed;
  5. }
  6. class RotatorSystem : ComponentSystem
  7. {
  8.     struct Group
  9.     {
  10.         //定义这个ComponentSystem需要处理哪些Component
  11.         Transform Transform;
  12.         Rotator   Rotator;
  13.     }
  14.     override protected OnUpdate()
  15.     {
  16.         // 我们马上看到了第一个优化
  17.         // 我们知道所有rotator的deltaTime都是一样的,
  18.         // 我们就把它存成个本地变量,以便获得根本更好的性能。
  19.         float deltaTime = Time.deltaTime;
  20.         // ComponentSystem.GetEntities<Group>
  21.         // 让我们可以高效地遍历每个有Transform & Rotator的GameObject
  22.         // (因为它们都被定义到上文的Group结构体里了)。
  23.         foreach (var e in GetEntities<Group>())
  24.         {
  25.             e.Transform.rotation *= Quaternion.AxisAngle(e.Rotator.Speed * deltaTime, Vector3.up);
  26.         }
  27.     }
  28. }
复制代码

三、混合ECS: 用ComponentSystem配合现存的GameObject/Component模式

目前还存在大量基于MonoBehaviour/GameObject编写的代码,我们或许想要不费力地让ComponentSystem配合现存的GameObject/Component一起工作。其实一次性把一个项目转换成ComponentSystem风格其实也不难的。
从上述例子你就能看出来,我们很轻易地用ComponentSystem配合GameObject/Component遍历了所有的Rotator和Transformt。


1,ComponentSystem是怎么知道GameObject身上的Rotator和Transform的?
EntityManager需要事先知道那些对应的Entity,然后才能像上面的例子那样去遍历所有的Component。
ECS附带一种GameObjectEntity组件。 当 OnEnable时,GameObjectEntity会创建一个包含GameObject身上所有Component的Entity。这样,整个GameObject和它的全部Component就都能被ComponentSystem遍历到了。

注意:所以,目前你必须要在你想让ComponentSystem能遍历得到或看得到的GameObject上添加GameObjectEntity组件。

2,这对我们程序来说意味着什么呢?
这意味着你可以一个接一个地,把MonoBehaviour.Update模式转变成ComponentSystem模式。
你可以继续使用GameObject.Instantiate 来创建实例等。
你只是简单地把MonoBehaviour.Update的内容移到了 ComponentSystem.OnUpdate里面去。 数据仍然保存在那个MonoBehaviour或别的类型的Component中。


你这么做能够:
    用更简洁地方式把数据和方法剥离;System对物体的操作都是批处理的, 避免了逐物体的虚拟调用(virtual call)。在批处理中进行优化就很简单了 (参见上述的deltaTime优化);你还能继续使用当前的编辑器面板及其他编辑器工具等;


你这么做不能够:
    实例化耗时得不到优化;加载化耗时得不到优化;数据是随机访问的,线性内存布局得不到保证;没有多线程;没有单指令流多数据流SIMD;


所以结合使用ComponentSystem、GameObject和MonoBehaviour是写ECS代码的良好开头,它能给你立即的性能提升,但是它并没有发挥出所有的性能潜力!


四、纯粹ECS:IComponentData 和 Job



使用ECS的动机之一就是你想让程序有最佳性能。所谓最佳性能是说,你写一些简单的ECS 代码就能得到跟你完全手写SIMD指令集代码差不多的性能。

C# Job System不支持托管的类(class),只支持结构体(struct)类型和原生容器(NativeContainer)。所以,只有IComponentData才能被安全地用于C# Job System。
EntityManager 为组件数据的线性内存布局做出了有力的保证。这是使用IComponentData可以实现的C# Job System的重要组成部分。

译注:为什么要进行线性内存布局,EntityManager 怎样为组件数据的线性内存布局做出了有力的保证的,后面的ECS In Detail(1)文章会有阐述。

下面是使用纯粹ECS方式实现上述相同功能的例子:
1,使用IComponentData储存数据:
  1. // 这个RotationSpeed就是简单地储存一下旋转速度
  2. [Serializable]
  3. public struct RotationSpeed : IComponentData
  4. {
  5.     public float Value;
  6. }
  7. // 目前而言,你要想添加或移除Component,就必须要使用这个ComponentDataWrapper,
  8. // 将来我们想把这个ComponentDataWrapper搞成自动的。
  9. public class RotationSpeedComponent : ComponentDataWrapper<RotationSpeed> { }
复制代码
2,使用JobComponentSystem实现对数据的多线程批处理:
  1. // 使用IJobProcessComponentData去遍历所有符合这个组件类型的Entity
  2. // Entity的处理时并行的。主线程只负责安排Job。
  3. public class RotationSpeedSystem : JobComponentSystem
  4. {
  5.     //IJobProcessComponentData是用来遍历所有带有所需Compoenent类型Enity的简单方法
  6.     //它也比IJobParallelFor更高效更便捷。
  7.     [ComputeJobOptimization]
  8.     struct RotationSpeedRotation : IJobProcessComponentData<Rotation, RotationSpeed>
  9.     {
  10.         public float dt;
  11.         public void Execute(ref Rotation rotation, [ReadOnly]ref RotationSpeed speed)
  12.         {
  13.             rotation.Value = math.mul(math.normalize(rotation.Value), math.axisAngle(math.up(), speed.Value * dt));
  14.         }
  15.     }
  16.     // 我们继承JobComponentSystem,这样System就可以自动提供给我们所需Job之间的依赖关系了。
  17.     // IJobProcessComponentData声明了它要对RotationSpeed读操作,并且对Rotation写操作。
  18.     // 这样声明以后,JobComponentSystem就连可以给我们Job之间的依赖关系了,包括之前已经安排好的要写Rotation或RotationSpeed的那些Job。
  19.     // 我们要把这个依赖关系renturn出来,这样,依据类型我们已经安排好的Job就能注册到下一个可能会运行的System里去了。
  20.     // 这么做意味着:
  21.     // * 主线程不发生等待, 主线程只需要根据依赖关系去安排Job (只有依赖关系被确定以后,Job才会被启动)。
  22.     // * 依赖关系为我们自动计算出来了, 这样我们就只写一些模块化的多线程代码就可以了。
  23.     protected override JobHandle OnUpdate(JobHandle inputDeps)
  24.     {
  25.         var job = new RotationSpeedRotation() { dt = Time.deltaTime };
  26.         return job.Schedule(this, 64, inputDeps);
  27.     }
  28. }
复制代码
译注:进一步内容可以参阅ECS In Detail(1)
楼主热帖
人人为我 我为人人 互相分享 互相学习 互相进步 一带一路

4

主题

2

听众

40

积分

问题学生

Rank: 1

升级   20%

TA的排名榜

积分:暂未上榜

发帖:NO. 301 名

在线:暂未上榜

发表于 2021-2-5 10:29 |显示全部楼层
为社区做贡献
回复

使用道具 举报

10

主题

1

听众

62

积分

问题学生

Rank: 1

升级   31%

TA的排名榜

积分:暂未上榜

发帖:NO. 92 名

在线:暂未上榜

发表于 2021-2-5 10:33 |显示全部楼层
谢谢分享,加油~!
回复

使用道具 举报

6

主题

2

听众

47

积分

问题学生

Rank: 1

升级   23.5%

TA的排名榜

积分:暂未上榜

发帖:NO. 201 名

在线:暂未上榜

发表于 2021-2-5 10:35 |显示全部楼层
Burst编译器是什么?
回复

使用道具 举报

4

主题

3

听众

46

积分

问题学生

Rank: 1

升级   23%

TA的排名榜

积分:暂未上榜

发帖:NO. 305 名

在线:暂未上榜

发表于 2021-2-5 10:38 |显示全部楼层
现在不是完全体,打算等更新到18.3的版本时再用。
回复

使用道具 举报

6

主题

2

听众

57

积分

问题学生

Rank: 1

升级   28.5%

TA的排名榜

积分:暂未上榜

发帖:NO. 204 名

在线:暂未上榜

发表于 2021-2-5 10:45 |显示全部楼层
这个优化想法太棒了,虽然目前的确很绕,底层应该还有很多代码简化的空间
回复

使用道具 举报

6

主题

1

听众

63

积分

问题学生

Rank: 1

升级   31.5%

TA的排名榜

积分:暂未上榜

发帖:NO. 203 名

在线:暂未上榜

发表于 2021-2-5 10:47 |显示全部楼层
应该是Unity的新编译器,同时还会推出新的数学库。
回复

使用道具 举报

10

主题

2

听众

73

积分

问题学生

Rank: 1

升级   36.5%

TA的排名榜

积分:暂未上榜

发帖:NO. 89 名

在线:暂未上榜

发表于 2021-2-5 10:53 |显示全部楼层
作为一个无脑的程序员,我有必要考虑要1分钟实现的东西,值不值得我去花5分钟优化
回复

使用道具 举报

4

主题

3

听众

44

积分

问题学生

Rank: 1

升级   22%

TA的排名榜

积分:暂未上榜

发帖:NO. 307 名

在线:暂未上榜

发表于 2021-2-5 10:56 |显示全部楼层
哈哈哈,这是个问题。
回复

使用道具 举报

5

主题

2

听众

54

积分

问题学生

Rank: 1

升级   27%

TA的排名榜

积分:暂未上榜

发帖:NO. 226 名

在线:暂未上榜

发表于 2021-2-5 10:56 |显示全部楼层
不同的地方选择不同的技术吧, 一般情况下用原来的GameObject-Component就好.
回复

使用道具 举报

温馨提示:求助请到“Unity技术讨论”版块中发帖,便于集中解决!
您需要登录后才可以回帖 登录 | 立即注册

Unity游戏引擎开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2021-4-15 07:31 , Processed in 0.164867 second(s), 36 queries .