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的文档解释,和我个人的理解,我认为有两点。
- 提高了内存读写速度 但我们运行游戏内的逻辑时,CPU需要在内存里读取数据并放到缓存中等待读取,可是通常数据在内存里的存放顺序是随机的,所以这会很大降低内存寻址的效率。
但如果数据是连续存放的,那么内存寻址时的错误率会大大降低,从而减少CPU等待内存寻址的时间,进而提高了CPU的利用率。Unity ECS引入了Archetype和Chuck两个概念,Archetype即为Entity对应的所有组件的一个组合,然后多个Archetypes会打包成一个个Archetype chunk,按照顺序放在内存里,当一个chunck满了,会在接下来的内存上的位置创建一个新的chunk。因此,这样的设计在CPU寻址时就会更容易找到Entity相关的component。 Archetype示意图 Chunk示意图
- 多线程的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用的工具。
-
Burst Complier:Burst会把C#代码编译成更高效的机器语言,提高游戏运行速度
-
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个的具体区别(因为性能上表现比较一致)所以我这里仅复制粘贴文档,不做解释…
- Run() : Runs the job immediately on the current thread.
- Schedul(): Adds an IJobEntityBatchWithIndex instance to the job scheduler queue for sequential (non-parallel) execution.
- 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/