写完 EasyECS 1.1.0,我为什么还是没把它做成完整 ECS?

做到 EasyECS 1.1.0 的时候,我其实已经具备了继续往前走的条件。

现在已经有:

bash 复制代码
ECSList
ECSDictionary
SoA / AoS Hybrid
Managed / Unmanaged Hybrid
Ref
Direct Column
Source Generator
多 Backend
生命周期检查
Benchmark

如果继续加:

bash 复制代码
Entity
World
System
Query
Archetype
Scheduler

再往前走一步,它完全可以开始向一个"完整 ECS Framework"发展。

但我最后没有这么做。

甚至可以说:

这是我刻意停下来的地方。

因为 EasyECS 做到后面,我越来越确定:

它真正有价值的地方,恰恰不是成为另一个完整 ECS。


一开始,我真正想优化的只是一个 List

整个 EasyECS 最初的问题非常小。

项目里有:

bash 复制代码
List<RoleData>

数据量越来越大。

然后这种代码开始变成热点:

bash 复制代码
RoleData role = list[i];

role.mHP -= 1;

list[i] = role;

于是我开始思考:

如果底层不是 AoS,而是 SoA,会怎么样?

后来才有:

bash 复制代码
[ECS]
↓
Source Generator
↓
SoA Storage

再后来,为了让它真正能用,又不得不加入:

bash 复制代码
Hybrid Storage
Ref
ECSList
ECSDictionary
Direct Column

EasyECS 一点一点长到了现在。

但你会发现:

所有这些东西始终围绕"数据怎么存"展开。

它从来没有真正开始管理:

bash 复制代码
游戏逻辑应该怎么组织
System 应该什么时候执行
Entity 应该属于哪个 World

这条边界我最后决定保留下来。


因为真实项目里,我并不想重写架构

如果是一个新项目,

当然可以在第一天就决定:

bash 复制代码
Entity
+
Component
+
System

所有业务都按照 ECS 思路设计。

但现实情况往往不是这样。

很多项目已经存在:

bash 复制代码
class Character
class Monster
class Skill
class Buff
class UIManager
class ResourceManager

周围可能已经有几十万行业务代码。

现在只是发现:

bash 复制代码
怪物数据
Buff 数据
子弹数据
角色运行时属性

里面几个批量循环比较慢。

为了优化这些地方,

要求整个项目改成:

bash 复制代码
World
System
Query
Archetype

我觉得代价太高了。

EasyECS 想解决的反而是:

能不能只换掉最慢的那部分?


所以我更喜欢这种状态

例如角色依然可以是:

bash 复制代码
public class Character
{
	public CharacterAI mAI;
	public CharacterModel mModel;
	public int mDataIndex;
}

AI、动画、表现、业务逻辑继续按照 OOP 写。

真正的大量运行时数据放进:

bash 复制代码
RoleDataECSList mRoleDataList;

于是整个项目变成:

bash 复制代码
Character
负责业务语义

        +

EasyECS
负责热点数据

需要批量处理时:

bash 复制代码
HP[]
Position[]
Speed[]

连续跑。

需要处理某个角色的复杂业务时:

bash 复制代码
Character

照常使用。

我觉得这比"所有东西必须统一成一种架构"更符合实际项目。


EasyECS 最适合什么地方?

做到现在,我觉得它最适合的场景已经比较清楚了。

例如:

bash 复制代码
大量角色运行时数据
怪物
子弹
Buff
碰撞数据
粒子状态
寻路节点
服务器实体状态

它们通常有几个共同特征:

bash 复制代码
数量多
+
生命周期相对统一
+
存在批量循环
+
一次经常只访问少数字段

这时候:

bash 复制代码
SoA
Dense Storage
Direct Column
SwapBack

都能真正发挥作用。


但这些地方我反而不会用 EasyECS

例如一个 UI 面板只有:

bash 复制代码
20 条数据

每次打开才访问一次。

我不会为了它写:

bash 复制代码
[ECS]

普通:

bash 复制代码
List<T>

已经够好了。

再比如:

bash 复制代码
配置数据
任务描述
剧情信息
少量临时数据
编辑器工具

它们根本不是性能热点。

这里强行换 EasyECS,只会增加理解成本。

还有一种数据:

bash 复制代码
每次都需要完整读取整个 Struct

AoS 本身就非常合理。

EasyECS 也从来不是:

看见 struct 就加 [ECS]


数据量小的时候,也不用急着优化

这一点我觉得尤其重要。

如果:

bash 复制代码
100 条数据

一帧只跑一次,

就算某种方案理论上快:

bash 复制代码
3 倍

最终可能只是:

bash 复制代码
0.003 ms
→
0.001 ms

这种优化对游戏根本没有意义。

却可能让代码复杂很多。

所以我更推荐:

bash 复制代码
先正常写
↓
Profiler 找热点
↓
确认是数据访问问题
↓
再考虑 EasyECS

而不是从第一天就把整个项目都变成"高性能代码"。


EasyECS 真正想优化的是热点,不是代码风格

这也是为什么前面一直有:

bash 复制代码
Indexer
↓
Ref
↓
Direct Column

三层访问方式。

如果 EasyECS 只追求 Benchmark,

完全可以要求所有人:

bash 复制代码
全部 Direct Column

肯定更漂亮。

但最后业务代码就会变成:

bash 复制代码
hp[i]
speed[i]
positionX[i]
positionY[i]

那和手写 SoA 已经差不了多少了。

我更希望:

bash 复制代码
普通代码 → 好写

热点代码 → 可以快

极端热点 → 可以直接到底层

这才是一个我愿意长期放进项目里的工具。


为什么没有 World?

因为 EasyECS 不需要知道:

这些 RoleData 属于哪个游戏世界。

为什么没有 System?

因为它不需要规定:

谁负责修改 HP。

为什么没有 Query?

因为它目前解决的不是:

如何动态组合 Component。

而是:

已经有一批数据,怎样让 CPU 更高效地处理它们。

这些看起来像"缺失的 ECS 功能"。

但对 EasyECS 来说,很多其实是:

有意不做

功能越多不一定越好。

一个框架开始承担越多职责,

它对项目架构的侵入就越深。


做到最后,我反而越来越喜欢"小框架"

以前做框架很容易有一种冲动:

bash 复制代码
既然已经有 A
那就顺便做 B

有了 B
C 也不远了

最后:

bash 复制代码
数据容器
↓
Entity
↓
System
↓
Scheduler
↓
World
↓
整个项目都必须围着框架转

框架越来越完整。

替换成本也越来越高。

EasyECS 这次我反而希望控制住这种冲动。

它解决:

bash 复制代码
数据布局
高频访问
批量处理

就够了。

其他事情继续交给项目现有架构。


如果现在让我重新设计一次

有些东西我依然会保留。

比如:

bash 复制代码
[ECS] / [NotECS]

Source Generator

Hybrid Storage

Indexer → Ref → Column

ECSList + ECSDictionary

Unsafe / Safe Backend

因为这些能力都围绕同一个目标:

让普通项目能够低成本使用更好的数据布局。

我也依然不会急着加入:

bash 复制代码
World
System
Archetype

除非以后真的出现一个问题:

不做这些东西,现有数据层已经无法解决。

而不是因为:

一个叫 ECS 的框架"理论上应该有这些东西"。


这 19 篇文章最后其实只在讲一件事

从最开始:

bash 复制代码
List<struct>

到:

bash 复制代码
AoS / SoA

再到:

bash 复制代码
Hybrid Storage
Managed / Unmanaged
Ref
Column
RemoveAtSwapBack
ECSDictionary
IL2CPP
Resize
Source Generator

看起来讲了很多东西。

但它们最终都围绕同一个问题:

如何让数据更适合 CPU,同时又不要让整个项目为性能付出过高的工程成本?

EasyECS 给出的答案不一定是唯一答案。

但至少是我目前比较喜欢的一种:

bash 复制代码
业务层
继续保持熟悉的写法

↓

Source Generator
承担机械复杂度

↓

数据层
变成更适合 CPU 的布局

写在最后

EasyECS 1.1.0 做完以后,我越来越不想把它称为:

一个完整 ECS 框架。

它更准确的定位仍然是:

OOP-compatible SoA data layout optimizer for Unity

也就是:

一个尽量兼容现有 OOP 项目、专门优化数据布局的 Unity 工具。

它没有打算告诉你:

bash 复制代码
游戏应该怎么写
架构应该怎么设计
所有逻辑应该放在哪里

它只想在真正的数据热点出现时回答:

这批数据,能不能摆得更适合 CPU 一点?

如果答案是可以,

那就加一个:

bash 复制代码
[ECS]

然后让编译器去干那些本来需要我们手写的大量脏活。

我觉得做到这里,EasyECS 作为一个独立工具已经足够完整了。

至于未来会不会继续变化,

当然会。

但至少有一件事情我不准备改变:

框架应该帮业务解决问题,而不是让业务为了使用框架重新变成另一个样子。


项目地址

EasyECS 是 MyFramework 仓库中的独立 Unity Package。

MyFramework:

bash 复制代码
https://github.com/ZHOURUIH/MyFramework

EasyECS UPM:

bash 复制代码
https://github.com/ZHOURUIH/MyFramework.git?path=/Packages/com.zhourui.easyecs

EasyECS 1.1.0 系列,到这里正式收官。

:::

相关推荐
SmalBox8 小时前
01-04-认知篇-基础-Unity Addressable Assets全面解析
unity3d·游戏开发
SmalBox1 天前
01-03-认知篇-基础-Unity原生AssetBundle全面解析
unity3d·游戏开发
fujisheng6611 天前
FUI 编译期装配实践:从反射注册到 Source Generator
c#·unity3d
fujisheng6612 天前
Unity UI 生命周期状态机:处理 Covered、异步竞态与事务回滚
c#·unity3d
_zhourui_h_2 天前
EasyECS 最慢的地方,居然是 Resize
unity3d
fujisheng6612 天前
Unity 异步 UI 实战:取消令牌、版本校验与旧句柄隔离
c#·unity3d
SmalBox2 天前
01-02-认知篇-基础-Unity资源管理发展史
unity3d·游戏开发
SmalBox3 天前
01-01-认知篇-概念-YooAsset是什么
unity3d·游戏开发
SmalBox4 天前
YooAsset-全篇导览
unity3d·游戏开发