ECS, Jobs, Burst 不是三个完全独立的系统。 ECS 负责:数据怎么组织,逻辑怎么批量处理。 Jobs 负责:这些处理怎么放到多线程执行。 Burst 负责:把适合的 C# 代码编译成高优化的原生机器码
NOTEUnity 官方也明确把 Burst 描述为与 Job System 密切配合的高性能编译器,同时 Burst 也是 ECS 技术体系的重要组成部分。
ECS 到底是什么
E = EntityC = ComponentS = SystemEntiy:一个”编号”
Entity 可以非常粗暴的理解为:一个没有具体功能的 ID。
他不像 GameObject 对象那样自带 Transform、Renderer、Collider 等大量东西。
它更像:这又一个东西,它的标号是 9527;真正的数据来自 Component。
Component:数据
ECS 的 Component 更强调”数据”,这里并没有行为方法。它描述的是这个 Entity 有什么数据。
System:负责做事情
Component 是数据,System 就是处理数据的逻辑。
public partial struct MoveSystem : ISystem{ public void OnUpdate(ref SystemState state) { // 找到拥有 MoveSpeed 和 LocalTransform 的 Entity // 然后让它们移动 }}ECS 与传统对象的对比
// 传统对象Enemy ├─ Transform ├─ Enemy.cs │ ├─ hp │ ├─ speed │ └─ Update() └─ Renderer
// ECSEntity 1 ├─ LocalTransform ├─ MoveSpeed └─ Health
Entity 2 ├─ LocalTransform ├─ MoveSpeed └─ Health
Entity 3 ├─ LocalTransform └─ MoveSpeed
MoveSystem ↓处理所有:LocalTransform + MoveSpeed这就是 ECS 和 MonoBehaviour 思维最大的区别。
ECS 为什么快
TIP一个误解:ECS 快是不是因为 Entity 比 GameObject 快?
ECS 快的真正原因是他的数据更加集中,传统的写法会让数据散落在大量的对象里(分散在内存的各处,物理上不连续)。
ECS 更倾向于把同类型的数据组织的非常紧凑(物理上连续)。
CPU 处理的时候非常适合:批量连续读取,批量连续计算,批量连续写入。这就特别适合:
- CPU cache
- 多线程
- SIMD
- Burst
这就是 ECS 真正强大的来源。
初识 ECS 世界
ECS 并不是简单多出几个类这么简单。
它建立在 Entities Package 以及相关 DOTS 技术包上。Unity 官方对 Enitties 的定义就是一套现代的 Entity Component System 实现。
Unity.Entities ↓ECS 核心
Unity.Transforms ↓实体 Transform 相关功能
Unity.Collections ↓NativeArray / NativeList 等高性能容器
Unity.Jobs ↓多线程任务
Unity.Burst ↓高性能编译System 的生命周期
OnCreate() // 创建 System,初始化 ↓OnUpdate() // 每次系统运行 ↓OnDestroy() // 销毁OnCreate
它类似于 MonoBehaviour 中的 Awake;也就是 System 创建时执行。通常用来做:
- 初始化状态
- 创建 EntityQuery
- RequireForUpdate
- 准备数据
OnUpdate
这是 ECS System 最重要的函数;它相当于 MonoBehaviour 中的 Update,也就是系统运行时,真正处理游戏逻辑得到地方。
- 移动
- 攻击
- 生成/销毁
- 修改数据
OnDestroy
System 被销毁时调用,可以理解为 MonoBehaviour 中的 OnDestroy。
- 释放资源
- 清理自己大的数据
SystemState 是什么
Unity 给当前 System 提供的一组运行环境和控制入口。
state.RequireForUpdate<MoveSpeed>();这段案例代码的作用是:只有存在 MoveSpeed 数据的时候,我这个 System 才需要运行。
RequireForUpdate 是干什么的?
public void OnCreate(ref SystemState state){ state.RequireForUpdate<MoveSpeed>();}如果世界上一个 MoveSpeed 都没有,这个 System 不需要执行 OnUpate;直接粗暴理解为”没有这个数据,就别让我跑。“
System 的完整最小模板
using Unity.Burst;using Unity.Entities;
[BurstCompile]public partial struct MoveSystem : ISystem{ [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate<MoveSpeed>(); }
[BurstCompile] public void OnUpdate(ref SystemState state) { }
[BurstCompile] public void OnDestroy(ref SystemState state) { }}[BurstCompile]
这是 Burst,Unity 官方说明,Burst 会使用 LLVM,把 .NET IL 转换成针对目标 CPU 优化过的原生代码。
Burst 不是另一个逻辑系统,而是把适合 Burst 的代码编译的更快。
Jobs 系统是什么
Jobs 系统会将适合拆开的代码拆成多个部分,让后交给多个核心并行处理。
一段案例代码:
for (int i = 0; i < 100000; i++){ data[i] *= 2;}Jobs 将任务拆开:
Job 1 → 0 ~ 24999Job 2 → 25000 ~ 49999Job 3 → 50000 ~ 74999Job 4 → 75000 ~ 99999于是便可以并处处理。
一个简单的 Jobs 案例
[BurstCompile]public struct MyJob : IJob{ public void Execute() { // 工作 }}
// =========
MyJob job = new MyJob();
JobHandle handle = job.Schedule();案例中的 Schedule() 不是马上执行,而是把这个 Job 交给 Job System。之后 handle.Complete();;表示我要确保这个 Job 已经完成。
Jobs 不能随便使用普通的 C# 对象
这是 Jobs 中非常重要的一条规则。
Jobs/Burst 并不喜欢这种托管对象;因为他们背后需要更加可控的内存布局。
Unity 官方也明确说明,Burst 不支持 managed objects,所以 Job 中通常需要使用 unmanaged 类型以及 NativeContainer(使用 NativeArray<int> 而不是 int[])。
NativeArray 是什么
可以将其理解为:适合 Jobs 使用的原生内存数组。
NOTEUnity 的 NativeContainer 还带有 Job Safety System,会跟踪读写关系,避免多个 Job 对共享数据进行危险访问。
Allocator.TempJob 是什么
new NativeArray<int>(1000, Allocator.TempJob);- Temp 非常短
- TempJob 给 Job 临时使用
- Persistent 长期存在
NOTEUnity 官方对 TempJob 的定位就是适用于生命周期较短的 Job 数据,并要求即使 Dispose(
array.Dispose();)。
Jobs + Burst 最简单组合
using Unity.Burst;using Unity.Collections;using Unity.Jobs;
[BurstCompile]public struct AddJob : IJob{ public NativeArray<int> Numbers;
public void Execute() { Numbers[0] += 10; }}
// 后续调用
NativeArray<int> numbers = new NativeArray<int>(1, Allocator.TempJob);
AddJob job = new AddJob{ Numbers = numbers};
JobHandle handle = job.Schedule();
handle.Complete();
numbers.Dispose();SystemAPI
现代 ECS 中 SystemAPI 至关重要。可以将其理解成:从 ECS 世界里查找我需要的数据。
// 最常用到的查询操作。// 找到具有 LocalTransform 的 Entity,后续可以对其进进行读写操作。SystemAPI.Query<RefRW<LocalTransform>>()RefRO 和 RefRW
RefRO
RefRO<MoveSpeed> // 只读操作RefRW
RefRW<LocalTransform> // 读写操作SystemAPI.Time.DeltaTime
当前 ECS 世界这一帧经过的时间,也就是对应 Mono 中的
Time.deltaTime。 获取每帧时间
SystemAPI.Query 做了什么
SystemAPI.Query<RefRW<LocalTransform>,RefRO<MoveSpeed>>()可以直接翻译成:找出同时拥有LocalTransform和MoveSpeed的 Entity。
这就是 ECS 最核心的能力之一:按照组件组合筛选数据。
这就是 ECS 和传统写法最大的思维转换,这个转变比记住 API 重要的多。
ECS 中的另一个重要概念:World
可以粗暴将其理解为 ECS 的运行世界,里面放着 Entities,Systems,Components。
WARNING所以 Scene 和 ECS World 并不是完全等价的概念。
System 的执行顺序
如果你有多个 System,例如 MoveSystem 需要 InputSystem,那么我们就需要 System Update Order。
这部分相当庞大(SystemGroup 概念),在现阶段只需知道即可。
SystemGroup
所有的系统并不是全部扔在一起的,就像上面提到的,有时候我们需要控制系统的执行顺序。
SystemGroup 通常会有:
- Initialization
- Simulation
- Presentation
这样的更新阶段,他们的顺序依次进行,所以 ECS 实际运行并不是 System 逐一执行,而是一颗系统树。
ECS 代码通常会是 partial struct
public partial struct CubeSystem : ISystem这里的 partial 并不是我们要拆成多个文件。
NOTE现代 Entities 会使用代码生成等机制,partial 是 ECS 某些 API / Source Generation 工作方式的一部分。
当前阶段直接按照规范写即可。