Unity ECS学习笔记(入门)

Posted by Nathan on July 20, 2021

Unity ECS学习笔记(入门)

芯片性能提升速度

引言

最近在学习A*寻路算法时,无意间发现Unity在2019年推出了一个新的框架ECS。了解片刻后,被这个框架带来的极大的性能提升所震撼,于是打算认真的学一下这个框架。这篇文章简单总结一下我近一周的学习心得,本人技术有限,如果文章有所疏漏也烦请指正!

ECS也就是Entity Component System的缩写,ECS采用了面向数据编程的思想,将之前笨重的GameObject拆分成值类型的Entity和Component,然后再使用System进行统一管理和运行。该框架亦引入了多线程编程技术,这样大大利用了设备CPU的性能。

这篇文章会包含如下内容:

  • 为什么Unity要开发ECS
  • ECS是如何带来性能提升的
  • 什么情况要使用ECS
  • ECS实例

为什么Unity要开发ECS

芯片性能提升速度

根据Unity自己的说法,目前芯片的性能迭代早已不遵循摩尔定律,即每18个月芯片性能提高一倍。所以最大化的利用的芯片性能是之后是开发者需要关注的重中之重。

在我看来,其实Unity早就应该提出这一套开发框架了。。本身C#的性能就比C++低不少,而且Unity的MonoBehavior class也非常笨重,这一次的框架更新真的可以给未来基于Unity的游戏带来更多的可能。

ECS是如何带来性能提升的

根据Unity的文档解释,和我个人的理解,我认为有两点。

  1. 提高了内存读写速度 但我们运行游戏内的逻辑时,CPU需要在内存里读取数据并放到缓存中等待读取,可是通常数据在内存里的存放顺序是随机的,所以这会很大降低内存寻址的效率。 芯片性能提升速度

但如果数据是连续存放的,那么内存寻址时的错误率会大大降低,从而减少CPU等待内存寻址的时间,进而提高了CPU的利用率。Unity ECS引入了Archetype和Chuck两个概念,Archetype即为Entity对应的所有组件的一个组合,然后多个Archetypes会打包成一个个Archetype chunk,按照顺序放在内存里,当一个chunck满了,会在接下来的内存上的位置创建一个新的chunk。因此,这样的设计在CPU寻址时就会更容易找到Entity相关的component。 Archetypes Archetype示意图 Chunk Chunk示意图

  1. 多线程的Job system提高了CPU利用率 这个比较好理解,Job system会把entities的分发到CPU的线程上,每个线程都会处理一系列任务。

什么情况要使用ECS

我个人认为并不是所有的项目/人都适合采用ECS架构,首先ECS的学习曲线较为曲折,太多新的方法,另外编程思想也不一样,整体学习的体验像是学一门新的语言,因此并不建议新手一上来就学ECS架构。

其次ECS主要解决的是CPU瓶颈,如果你的项目更多的是GPU瓶颈,那么没有必要把项目完全改成ECS框架,请考虑采用图形学方面的优化。

ECS比较适用于场景中有大量独立的agent逻辑的场景,比如RTS,模拟经营这类游戏就很适合用ECS进行优化。

ECS实例

实例下载:https://github.com/moecia/UnityECS ###前言 在开始之前提一下刚刚没有讲到的2个ECS用的工具。

  1. Burst Complier:Burst会把C#代码编译成更高效的机器语言,提高游戏运行速度

  2. JobSystem:Unity开发的无痛多线程代码运行库。在最新的ECS 0.17.0版本中,似乎不需要再额外写专门的Job struct来实现多线程,只需要让我们的System类继承SystemBase class,并在OnUpdate的Entites.ForEach loop后,加上一个extension method即可实现多线程运行。

因为Unity ECS的更新比较频繁,所以在我学习过程中发现不少教程的方法都已经deprecate了。。所以我这个教程在1,2年后也难免变得不可用。

目前无论是ComponentSystem还是JobSystem,都被Unity宣布弃用。(不少老教程还在用这两个Class)所以我这里一律会使用SystemBase。

SystemBase的和ComponentSystem和JobSystem的主要区别是,在OnUpdate的Entites.ForEach loop后,必须要选择一个extension method,主要有3个常用的extension method,这里目前我还不太完全理解3个的具体区别(因为性能上表现比较一致)所以我这里仅复制粘贴文档,不做解释…

  1. Run() : Runs the job immediately on the current thread.
  2. Schedul(): Adds an IJobEntityBatchWithIndex instance to the job scheduler queue for sequential (non-parallel) execution.
  3. ScheduleParallel(): Adds an IJobEntityBatchWithIndex instance to the job scheduler queue for parallel execution.

    代码部分:

    MoveSpeed.cs ``` using Unity.Entities;

namespace EcsSample { public struct MoveSpeedComponent : IComponentData { public float MoveSpeed; } }

MoveSystem.cs

using Unity.Entities; using Unity.Transforms;

namespace EcsSample { public class MoveSystem : SystemBase { protected override void OnUpdate() { var dt = Time.DeltaTime; Entities.ForEach((ref Translation translation, ref MoveSpeedComponent moveSpeedComponent) => { translation.Value.y += moveSpeedComponent.MoveSpeed * dt; if (translation.Value.y > 5f || translation.Value.y < -5f) { moveSpeedComponent.MoveSpeed *= -1; } }).ScheduleParallel(); } } }

TestController.cs
这里额外提下用到的SetSharedComponentData()方法。这个方法用于共享的component,然后会把所有share component打包到一个chunk,这样当需要更新share component时,因为它们内存中的存放位置时连续的,所以访问速度会更快。

using Unity.Collections; using Unity.Entities; using Unity.Mathematics; using Unity.Rendering; using Unity.Transforms; using UnityEngine; using Random = UnityEngine.Random;

namespace EcsSample { public class TestController : MonoBehaviour { [SerializeField] private Mesh mesh; [SerializeField] private Material material; void Start() {

        var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
        // Add entity achetype
        var entityArchetype = entityManager.CreateArchetype(
            typeof(MoveSpeedComponent),
            typeof(Translation),
            typeof(RenderMesh),
            typeof(LocalToWorld),
            typeof(RenderBounds));
        var entityArray = new NativeArray<Entity>(500000, Allocator.Temp);
        // Instantiate entities
        entityManager.CreateEntity(entityArchetype, entityArray);
        for (int i = 0; i < entityArray.Length; i++)
        {
            var entity = entityArray[i];
            entityManager.SetComponentData(entity, new MoveSpeedComponent { MoveSpeed = Random.Range(1f, 2f) });
            entityManager.SetComponentData(entity, new Translation { Value = new float3(Random.Range(-8f, 8f), Random.Range(-5f, 5f), 0) });

            entityManager.SetSharedComponentData(entity, new RenderMesh
            {
                mesh = this.mesh,
                material = this.material
            });
        }

        entityArray.Dispose();
    }
} } ``` 代码结构比较简单,MoveSpeed.cs就是一个移动速度的Component class,第二个MoveSystem就是控制Entity移动的,当entity移动到屏幕地图顶上就会往下移动,反之到了底部就会往上移动。最后的TestController.cs就是用于生成Entity。

参考资料

官方文档: https://docs.unity3d.com/Packages/com.unity.entities@0.17/manual/index.html

官方入门教学系列: https://learn.unity.com/tutorial/editor-scripting

官方样例: https://github.com/Unity-Technologies/EntityComponentSystemSamples

中文入门文档: http://dingxiaowei.cn/2020/02/09/ https://www.lfzxb.top/unity-dots-ecs-burst-complier-jobsystem/