前几篇一直在讲 AoS、SoA 和 CPU Cache。
到了这里,可以正式回答一个问题:
EasyECS 到底是什么?
看到名字里有 ECS,很容易把它理解成 Unity Entities 的另一种实现。
但实际上,EasyECS 从设计目标上就不是一套完整 ECS Framework。
它没有准备重新实现:
Entity
Component
System
World
Archetype
Query
这些东西。
EasyECS 真正关心的只有一件事:
如何让现有 Unity 项目中的大量 struct 数据拥有更高效的内存布局。
项目地址
EasyECS 本身就是 MyFramework 仓库中的一个独立 Unity Package。
MyFramework 主仓库:
https://github.com/ZHOURUIH/MyFramework
通过 Unity Package Manager 安装 EasyECS:
https://github.com/ZHOURUIH/MyFramework.git?path=/Packages/com.zhourui.easyecs
目前 EasyECS 已经发布到 1.1.0。
EasyECS 首先是一个数据布局工具
假设项目里有:
public struct RoleData
{
public int mHP;
public float mSpeed;
public float mPositionX;
public float mPositionY;
}
正常情况下,我们可能使用:
List<RoleData> roleList = new();
底层数据基本按照 AoS 排列:
HP Speed X Y
HP Speed X Y
HP Speed X Y
HP Speed X Y
如果每帧都批量处理 HP:
HP → HP → HP → HP
中间却夹杂着大量 Speed、X、Y。
而 EasyECS 可以这样定义:
using EasyECS;
[ECS]
public struct RoleData
{
public int mHP;
public float mSpeed;
public float mPositionX;
public float mPositionY;
}
Source Generator 会根据这个结构自动生成对应的 SoA Storage。
逻辑上还是:
RoleData
底层则可以变成:
HP[] → HP HP HP HP...
Speed[] → S S S S ...
PositionX[]→ X X X X ...
PositionY[]→ Y Y Y Y ...
这就是 EasyECS 最核心的工作。
为什么还叫 ECS?
因为它采用的确实是 ECS 中非常核心的数据导向思想:
把大量相同组件的数据集中起来批量处理。
例如:
[ECS]
public struct BulletData
{
public float mPositionX;
public float mPositionY;
public float mSpeed;
public float mLifeTime;
}
它非常适合:
所有 PositionX 一起处理
所有 PositionY 一起处理
所有 LifeTime 一起处理
但 EasyECS 并不进一步规定:
谁是 Entity
谁负责 System
World 怎么管理
Query 怎么匹配
这些都还是项目自己的事情。
所以准确来说:
EasyECS 借用了 ECS 的数据布局思想,但没有试图接管整个游戏架构。
和 Unity Entities 有什么区别?
Unity Entities 是完整 ECS 方案。
它不仅关心数据怎么存,还会涉及:
Entity 生命周期
Component
System
Query
Archetype
Chunk
Job
Burst
使用它以后,项目的程序组织方式也可能随之变化。
EasyECS 则没有这个目标。
比如原来的项目里已经存在:
public class Character
{
public CharacterAI mAI;
public CharacterModel mModel;
public int mDataIndex;
}
完全可以继续保留。
只把真正大量计算的数据换成:
RoleDataECSList mRoleData;
于是:
Character
仍然负责 OOP 业务逻辑。
RoleDataECSList
负责高频数据处理。
两者可以同时存在。
和手写 SoA 又有什么区别?
如果只追求性能,当然可以直接写:
int[] hp;
float[] speed;
float[] positionX;
float[] positionY;
这种方式甚至可能是最直接的。
但工程中很快就会遇到:
Add 怎么写?
Resize 怎么写?
Insert 怎么写?
RemoveAt 怎么写?
新增字段怎么办?
Managed 字段怎么办?
Dictionary 怎么办?
如果有几十种数据结构,维护成本会迅速增加。
EasyECS 的思路是把这些重复代码交给 Source Generator。
开发者维护的仍然只是:
[ECS]
public struct RoleData
{
...
}
字段变化以后,对应存储代码重新生成即可。
所以它不是想证明:
自动生成一定比手写更快。
而是想解决:
如何把手写 SoA 的性能优势变成一个长期可维护的工程方案。
和普通 List 又是什么关系?
EasyECS 也不是要消灭:
List<T>
如果只有几十条数据,或者数据访问频率很低:
List<ConfigData>
完全没有必要替换。
真正值得考虑 EasyECS 的通常是:
数据量较大
+
访问频繁
+
存在明显热点循环
+
经常批量访问相同字段
例如:
角色属性
怪物运行时数据
子弹
Buff
粒子
碰撞数据
寻路节点
服务器大量状态数据
这些才是它更擅长的场景。
EasyECS 不只是一种访问方式
为了兼顾易用性和极限性能,EasyECS 没有强迫所有代码都使用最底层接口。
目前可以大致分成几层。
最直接:
list[i].mHP -= 1;
需要连续访问多个字段,可以保存 Ref:
RoleDataRef role = list[i];
role.mHP -= 1;
role.mPositionX += role.mSpeed;
如果是真正的热点循环,还可以直接访问 Column。
整体思路是:
Indexer
↓
Local Ref
↓
Direct Column
越往下越接近底层数据。
也就是说,EasyECS 不要求:
为了性能,整个项目都必须写成最难看的代码。
普通逻辑可以保持简单。
Profiler 真正发现热点以后,再进一步优化。
EasyECS 也不是纯 SoA
实际业务中,并不是所有字段都值得拆开。
所以 EasyECS 同时支持:
[ECS]
public struct RoleData
{
public int mHP;
public float mSpeed;
[NotECS]
public int mModelID;
[NotECS]
public int mCamp;
}
这样高频访问的:
HP
Speed
可以走 SoA。
而低频字段:
ModelID
Camp
仍然可以保持 AoS。
最终形成:
SoA + AoS Hybrid
这也是 EasyECS 和"简单把 struct 全部拆数组"相比,一个非常重要的区别。
这个设计后面会专门用一篇文章详细讲。
EasyECS 现在包含哪些能力?
到了 1.1.0,EasyECS 已经不只是最初的 ECSList。
目前主要包括:
[ECS] / [NotECS]
SoA / AoS Hybrid Storage
Managed / Unmanaged Hybrid
ECSList
ECSDictionary<TKey>
Ref
Direct Column
Insert
RemoveAt
RemoveAtSwapBack
Unsafe Backend
SafeSpan Backend
SafeRegistry Backend
Source Generator
生命周期检查
Runtime Test
Benchmark
但所有这些功能,最终仍然围绕同一个目标:
让数据布局更适合 CPU,同时尽可能降低业务层改造成本。
EasyECS 的边界反而很明确
EasyECS 不负责:
游戏对象架构
System 调度
Entity 生命周期
Job 调度
多线程框架
完整 World 管理
它不是 Unity Entities 的替代品。
更适合把它理解成:
普通 Unity / OOP 项目
↓
找到数据热点
↓
替换数据容器
↓
EasyECS 自动生成高效布局
所以一个项目完全可以:
90% 继续使用原来的代码
10% 热点数据使用 EasyECS
而不是非得二选一。
写在最后
如果要用一句话描述 EasyECS,我更愿意说:
EasyECS 是一个利用 Source Generator,把普通 struct 转换成高性能 SoA / Hybrid Storage 的 Unity 数据布局优化工具。
它借用了 ECS 的数据导向思想。
但并不要求项目变成完整 ECS 架构。
它真正想解决的是一个非常实际的问题:
项目已经写了很多年,我现在只是想让几个几十万数据量的热点循环更快,能不能别让我把整个架构重写?
EasyECS 就是针对这个问题做出来的。
下一篇开始真正进入使用:
只加一个 [ECS] 会发生什么?Unity EasyECS 五分钟快速上手
下一篇我们会直接从一个普通 struct 开始,看看添加 [ECS] 后,EasyECS 到底生成了什么,又应该怎样创建和使用第一个 ECSList。