UnityECS学习01-HelloCube
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 Mode
为Runtime Data
- 在
Editor
中开启Enter Play Mode Options
,但是关闭Reload Domain
和Reload Scene
。ReloadDomain了解更多
代码详解
在我们第一个例子当中,只定义了一个 Component
,也就是 RotationSpeed
1
2
3
4public 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
19public partial struct RotationSystem : ISystem
{
[ ]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<RotationSpeed>();
}
[ ]
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
三种状态:
OnCreate
:System
创建时调用一次OnUpdate
:满足情况时每帧调用OnDestroy
:System
销毁时调用一次
在此处示例的 OnUpdate
当中:1
state.RequireForUpdate<RotationSpeed>();
这段的意思是如果包含有 RotationSpeed
的 Entity
存在,就每帧调用 RotationSystem
的 OnUpdate
。
接着在 OnUpdate
方法当中,调用 SystemAPI.Query
来查询当前 World
中所有满足条件的 Entity
。其中 RefRW
和 RefRO
是对其中两个值的读写引用和只读引用。
回到类 RotationSpeedAuthoring
中,也就是我们挂载到 Cube
上面的脚本类。Unity
为 ECS
框架提供了一套数据机制。将 GameObject
转换成 Entity
,将 GameObject
上的Component
转换为 ECS 的 Component
,这个转换就叫 Baking
,即烘焙。这个 Baking
和过程和我们创建的 SubScene
是相关的。因为 Baking
是在后台异步执行的,当 SubScene
内容发生改变的时候后台就会执行 Baking
。
Unity 内置的 Transform
和 MeshRenderer
是由 ECS 自动处理完成的。而我们自己编写的组件就需要先定义一个 RotationSpeedAuthoring
,Authoring
组件的作用是可以让用户在 GameObject
什么编辑组件数据。其中再定义一个 Baker
类:1
2
3
4
5
6
7
8
9
10
11class 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
。
参考链接: