不做完整 ECS,只优化数据布局:Unity EasyECS 到底是什么

前几篇一直在讲 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

相关推荐
虫小宝12 小时前
优惠券省钱APP查询性能优化:Elasticsearch与Canal实现的多维度商品搜索毫秒级响应方案
elasticsearch·性能优化·jenkins
李高钢1 天前
【WPF】高级 UI 与性能优化实战:从卡顿到丝滑
ui·性能优化·wpf
郝学胜-神的一滴1 天前
Horse3D 游戏引擎研发笔记(七):Clydesdale——从流式日志到多输出订阅
c++·qt·unity·游戏引擎·图形渲染·unreal engine·opengl
江上闲人.2 天前
Wardogs战犬\战狗Beta测试启动报错UE崩溃卡顿?解决方法来了
游戏·游戏引擎·虚幻·战犬·战狗·wardogs
小贺儿开发2 天前
Unity 程序员编曲花絮:汪峰《河流》细节修复
科技·学习·unity·程序员·音乐·demo·写代码
598866753@qq.com2 天前
Unity UniText笔记
unity·unitext
杨丰玮4182 天前
从零手写Java飞机躲障碍游戏|Swing绘图、鼠标跟随、计时器碰撞检测实战(五)
java·python·游戏·游戏引擎·图形渲染·动画·贴图
鼎艺创新科技2 天前
不依赖 UE/Unity:我们如何从零搭建一套国产三维 GIS 渲染引擎
人工智能·算法·unity·游戏引擎·三维电子沙盘