ECS 架构

ECS 三个字母分别代表的为:

  • Entity:实体,相当于游戏或程序中的事物(可以理解为一个空的 GameObject )。
  • Component:组件,附加到实体的数据,重点不是对象而是数据(可以理解为 Unity 中的一个脚本,但是只含有数据没有逻辑)。
  • System:系统,程序的主要逻辑所在(可以理解为 Unity 中的一个脚本,但是只含有方法逻辑没有数据)。

为什么 ECS 很快?

首先我们需要知道,CPU 自身有三级缓存,速成高速缓存。CPU 访问第一级(L1)的缓存最快,但是容量最小。往后依次速度变慢容量变小。CPU 访问内存所需的时钟周期远远大于访问高速缓存的时钟周期。

CPU 操作数据会先从高速缓存中读取,速度非常快。一般不会出现让 CPU 处理完数据在干等着数据读入的情况。但是有些情况,我们访问的数据不在这三级缓存当中,也就是 Cache Miss ,此处 Cache Miss 指的是高速缓存全部都没有命中,需要从内存读取数据的情况。此时需要寻址到内存中的数据(包含这个数据的一整块数据都将被读入到缓存当中),并把目标数据放入到高速缓存当中。

在我们传统的 OOP 模式当中,比如我们要移动场景中的一个物体,肯定是去修改其 Transform 当中的 Position 。但是使用的时候整个 Transform 都会被加载到缓存当中,其中肯定不止我们需要的 Position,还有一些诸如 eulerAngles 等等的我们不需要的属性。这些多余的属性就占用了很大的缓存空间,造成严重的内存浪费。若我们要移动的物体有成千上万个,那么缓存当中就可能存在超过 50% 的内存垃圾,再加上这些属性在内存中排放都是无序的,从而造成缓存命中的几率大大降低,导致性能下降。

而 ECS 是组件化的,需要什么数据就声明什么数据。需要 Position 只需声明一个 float3,与传统的 Transform 比起来占用内存微乎其微了。

HelloCube

dots 安装

使用 Unity 2022.2 及以上,创建 URP 模板(否则 com.unity.entities.graphics 安装不上),接着安装如下包:

  • com.unity.entities
  • com.unity.entities.graphics (for rendering entities)
  • com.unity.collections (unmanaged collection types)
  • com.unity.physics (for collision detection and physical simulation of entities)

接着在 preferences 中修改:

  • Entities 中设置 Scene View ModeRuntime Data
  • Editor 中开启 Enter Play Mode Options,但是关闭 Reload DomainReload SceneReloadDomain了解更多

代码详解

在我们第一个例子当中,只定义了一个 Component,也就是 RotationSpeed

1
2
3
4
public struct RotationSpeed : IComponentData
{
public float RadiansPerSecond;
}

在 ECS 当中,最常用的 Component 叫作 Unmanaged components,定义一个 struct 实现 IComponentData 。结构体里面只有数据没有方法逻辑。并且结构体的字段类型必须是可以直接映射到 unmanaged 内存的数据类型。具体有哪些参考 官方文档

接着是 System,定义了一个 RotationSystem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public partial struct RotationSystem : ISystem 
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<RotationSpeed>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float deltaTime = SystemAPI.Time.DeltaTime;
// Ref 包含引用, RW 只写,RO 只读
foreach (var (transform, speed) in
SystemAPI.Query<RefRW<LocalTransform>, RefRO<RotationSpeed>>())
{
transform.ValueRW = transform.ValueRO.RotateY(speed.ValueRO.RadiansPerSecond * deltaTime);
}
}
}

System 根据是否要处理 managed 内存的数据可以有两种实现方式:

  • 定义一个 struct,实现 ISystem 接口
  • 如果需要处理 managed 内存数据。则创建一个 class,继承自 SystemBase

前者可以经过 Burst 编译优化。

System 三种状态:

  • OnCreateSystem 创建时调用一次
  • OnUpdate:满足情况时每帧调用
  • OnDestroySystem 销毁时调用一次

在此处示例的 OnUpdate 当中:

1
state.RequireForUpdate<RotationSpeed>();

这段的意思是如果包含有 RotationSpeedEntity 存在,就每帧调用 RotationSystemOnUpdate

接着在 OnUpdate 方法当中,调用 SystemAPI.Query 来查询当前 World 中所有满足条件的 Entity。其中 RefRWRefRO 是对其中两个值的读写引用只读引用

回到类 RotationSpeedAuthoring 中,也就是我们挂载到 Cube 上面的脚本类。UnityECS 框架提供了一套数据机制。将 GameObject 转换成 Entity ,将 GameObject 上的Component 转换为 ECS 的 Component,这个转换就叫 Baking,即烘焙。这个 Baking 和过程和我们创建的 SubScene 是相关的。因为 Baking 是在后台异步执行的,当 SubScene 内容发生改变的时候后台就会执行 Baking

Unity 内置的 TransformMeshRenderer 是由 ECS 自动处理完成的。而我们自己编写的组件就需要先定义一个 RotationSpeedAuthoringAuthoring 组件的作用是可以让用户在 GameObject 什么编辑组件数据。其中再定义一个 Baker 类:

1
2
3
4
5
6
7
8
9
10
11
class Baker : Baker<RotationSpeedAuthoring>
{
public override void Bake(RotationSpeedAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent(entity, new RotationSpeed()
{
RadiansPerSecond = math.radians(authoring.DegreesPerSecond)
});
}
}

重写这个 Bake 将我们的数据转换为 ECS 的 Compnent

参考链接:

HelloCube walkthrough (Unity EntityComponentSystem samples)

HelloCube代码背后的秘密

Unity 革命性技术DOST入门二 ECS简单使用介绍