Flecs ECS 游戏开发技术专题全层级3 / 31 已发布

ECS 范式的起源与 Flecs 的诞生故事

· 0 分钟阅读
ECS 范式的起源与 Flecs 的诞生故事:从《守望先锋》的“后悔药”到 Sander Mertens 的补丁 The Origin of ECS & the Birth of Flecs — Flecs ECS 技术专题

ECS 范式的起源与 Flecs 的诞生故事:从《守望先锋》的“后悔药”到 Sander Mertens 的补丁 The Origin of ECS & the Birth of Flecs

为什么“这套东西从哪来”是整个系列的第一块基石

2019 年我第一次看 Tim Ford 那场 GDC 演讲(《Overwatch Gameplay Architecture and Netcode》),屏幕上一打出 ECS 三个字母,我脑子里蹦出来的第一个词是“缓存友好”。整场听完,我记住的也是性能。他在台上说了一句:我们做这套架构,第一动机不是性能,是网络同步。我当时把这句话当成大厂工程师的谦辞,直接跳过去了。

两年后我自己做一个四人联机的小项目,需要做延迟补偿,需要让服务器退回 150 毫秒前的世界重算一次命中。我打开自己的代码,看见玩家对象里挂着物理刚体指针、动画状态机指针、两个 UI 引用、一个回调函数列表,还有第三方库里我自己都不清楚是什么的东西。那一刻我才明白 Tim Ford 不是在客气,他是在陈述一个事实:能倒带的世界,和跑得快的世界,是同一种世界;而面向对象的世界,两种都不是。

这就是为什么一个三十篇的技术专题要从“起源”讲起。接下来你会读到查询系统怎么分层、关系为什么是 Flecs 的招牌、调度器怎么排 Phase、多线程的边界在哪、渲染和物理怎么接、网络同步怎么玩。这些内容全都踩在同一个前提上:你认不认“把数据摆在一起”这件事,值得你换掉写代码的舒适区。这个前提是怎么被逼出来的、为什么最后长成了 Flecs 那个样子,就是这篇要聊的。

顺带说个不太客气的观察:我见过太多团队把 ECS 当成“性能优化方案”引进,最后灰头土脸地退回去。原因几乎一模一样——他们以为自己在换一套更快的容器,其实他们在换“谁有权决定游戏里能出现什么”。这个区别,就是本文的全部内容。

第一层:OOP 不是死在教科书里的,是死在策划提需求的那一天

先别急着讲 DOD、讲缓存行,我们从一个人人都经历过的下午说起。

你有一个 Enemy 基类,下面派生着 GroundEnemy、FlyingEnemy、RangedEnemy,还有一个 BossEnemy 继承自 FlyingEnemy。这天策划走过来说:我要一个会飞的 Boss,它身上着火的时候会往下掉,被冰冻住的时候不掉,被雷劈的时候会分裂成两个小怪。你怎么办?

再造三个类?那“着火掉高度”这个能力就要在三个地方各写一遍,以后改数值要改三处。用多重继承?那你很快会遇到 C++ 里那个著名的菱形继承问题,然后发现你为了“能着火”这件事,把整棵继承树都搅浑了。用接口?接口的意思是你得在每个用到它的系统里写一遍判断——“如果这个单位有结冰能力就减速”。写到第十个判断的时候你会发现一件很有意思的事:你其实已经在写 ECS 了,只是还没有那个名字。你在做的事就是“按能力筛选对象”,而“能力”就是组件的雏形。

所以 OOP 在游戏里崩掉的地方,从来不是“理论不优雅”。它崩在横切关注点上:一种能力会横穿整棵继承树,而继承树只能沿着一条线长。玩家、敌人、载具、可破坏物,都想要“能着火”“能被冻”“有护盾”,这些东西无法落到任何一个基类里。

Unity 早期的 GameObject-Component 模型,其实是行业对这件事的第一次体面妥协:把“是一个什么”换成“有什么东西”。这个转身很关键,它让“组合”第一次在主流工具里变得便宜。但它只解决了一半问题——组件还是类,每个组件对象还是各自散落在堆上,遍历还是靠一层层的虚函数调用。它换掉了类型结构,没换掉内存结构。

举个具体点的账。假设你有一千个敌人,每个敌人的对象在堆上大概两百字节,其中“位置”这个字段占十二字节。你要更新一帧的位置,CPU 得把两千字节乘以一千、也就是差不多两百 KB 的数据拖进缓存,而真正用到的只有十二 KB。剩下那九成多,是买一赠十四。Tony Albrecht 2009 年那篇《Pitfalls of Object Oriented Programming》讲的就是这件事,他在 PS3 上做游戏,协处理器的本地内存只有 256 KB,一次缓存未命中的代价大到你能用肉眼看出帧率抖动。PC 上没那么夸张,一个对象跳一下也就几十纳秒,但道理是一样的:你不是在遍历敌人,你是在遍历内存。

不过说句实话,对绝大多数独立游戏来说,性能真不是 OOP 的致命伤。一千个实体以下的游戏,你写成什么样大概率都能跑满帧。真正让我这个策划坐不住的,是第二层痛:内容的迭代权被代码结构扣住了。

我做过一个粗略的统计(说明一下,这是我待过的三个项目攒出来的经验值,不是行业调研):在一个中等复杂度的 OOP 项目里,一个“新敌人”从策划提需求到美术能在引擎里看见它动,平均要两到五个工作日。这五天里真正写玩法逻辑的可能只有半天,剩下的时间在开会、在改基类、在评估“这个改动会不会影响到那个怪”、在跑一轮回归测试。而在一个组合式的架构里,同样的需求可能就是一个配置加两个标记,半小时。这个差距不是程序员打字速度的差距,是“这件事能不能由一个人独立完成”的差距。一个人能独立完成一件事的时候,他才敢一天试八个想法;要五个人才能完成一件事的时候,一周能试两个就不错了。

记住这句话,后面三十篇都会用到:ECS 的第一性收益不是帧率,是把“能不能做出来”和“好不好玩”这两件事拆开。以前你要先问“做这个要改多少代码”,以后你只问“这个好玩吗”。

第二层:DOD 那帮人想说的是“先想内存怎么走”,而不是“先想类怎么分”

2009 年有两篇文章几乎同时出现,Noel Llopis 的《Data-Oriented Design》和前面提到的 Tony Albrecht 那篇,算是把“数据导向”这个概念正式摆上了台面。真正把它变成一场运动的是 2014 年 Mike Acton 在 CppCon 的那场《Data-Oriented Design and C++》——那场演讲里他有一页幻灯片,把一个“球”的经典面向对象结构拆开,逐条问:这个字段你真的需要吗?这个虚函数你一年调用几次?这份抽象帮你省了多少事,又让你多搬了多少字节?台下坐着一群写了十年 C++ 的人,脸色都不太好看。

把 DOD 翻译成人话,其实就一句:CPU 根本不在乎你的类叫什么名字,它只在乎下一口要吃的东西是不是已经在嘴边了。从内存搬一个字节进寄存器,比在寄存器里做一次加法慢两个数量级。所以决定你帧时间的,从来不是算法多聪明,是数据摆放的顺序。你写得再优雅,数据散在内存各处,CPU 就是在做无用功。

最直观的对照就是“结构体数组”和“数组结构体”这两种摆法。同一个敌人列表,两种摆法长这样:

摆法人话描述遍历“更新位置”时会发生什么
按对象排(结构体数组)每个敌人是一个完整的包裹,位置、血量、名字、模型指针全打包在一起你只需要十二字节的位置,却把整个两百字节的包裹一路拖过去,九成带宽浪费
按字段排(数组结构体)所有敌人的位置挤在一个连续数组里,所有敌人的血量挤在另一个数组里你顺序读一段连续内存,每读一个都是你要的,缓存命中率接近百分之百

看起来只是把字段挪了个位置,但如果你的游戏一帧要遍历一万次位置,这个“挪位置”就是三倍到十倍的差别。而且这里有个反直觉的地方:按字段排之后,代码反而变简单了。你要算所有敌人的位置,就是一个从头到尾的循环,没有虚函数、没有空指针检查、没有“这个敌人是不是还活着”的分支。数据的形状变简单了,代码的形状也跟着简单。

这里必须把一件事掰清楚,因为太多人在这上面犯迷糊:DOD 不等于 ECS。

DOD 是一种看世界的方式——先问“数据长什么样、按什么顺序被访问”,再问“代码怎么写”。ECS 是这套思路的一种制度化写法:用实体当编号、用组件当数据块、用系统当处理逻辑。你可以写一堆按字段排的数组,非常 DOD,但你压根没有 ECS;你也可以用着最时髦的 ECS 框架,却在组件里塞指针、在系统里塞全局状态,那就是穿了 ECS 皮的 OOP,两头不讨好。

再给一个我特别喜欢的实战案例:Factorio。据开发者在自家博客里的说法,游戏里实体之间不用指针互相引用,而是存编号,需要的时候去查表。这个决定看起来只是个编码习惯,实际后果大得吓人:因为世界里没有隐藏的指针,整个存档就是把一堆数组摊平写进文件,加载的时候再按编号把关系重建一遍。社区里一直在传他们十几年前的旧存档还能读,这件事的技术前提就是“世界里没有脏东西”。你换成一套指针乱飞的 OOP 结构试试,光是给每个类写序列化和版本迁移,就能写到你怀疑人生。

所以这两层的关系是:DOD 给的是世界的形状,ECS 给的是世界的制度。形状对了,制度才有意义;形状不对,制度只是一堆仪式。

第三层:《守望先锋》的 ECS 不是为了跑得快,是为了“能倒带”

现在说正主。

2016 年的《守望先锋》,六打六,射击游戏。射击游戏有个绕不过去的物理事实:玩家屏幕上看到的敌人,永远是“过去的”敌人。因为网络有延迟,客户端还要做插值平滑,你看到的那个人大概是他一百毫秒之前的位置。可玩家不这么想,玩家觉得“我明明打中了”。

要让玩家觉得公平,服务器就得干一件很反直觉的事:收到你的开枪指令时,先把整个世界倒退回到“你按下鼠标那一刻看到的世界”,在那个旧世界里判定这一枪打没打中,判完再回到现在。这个操作叫延迟补偿,英文里叫 rewind,中文开发圈有时管它叫“后悔药”。

OOP 的世界做这件事有多痛?你得给每一个需要回滚的东西单独写一份“保存”和“恢复”。玩家的位置、朝向、生命值、当前动画帧、子弹的飞行状态——每加一个新玩法,你就得记得“这个也要进快照”。总有一次你会忘,忘了的后果不是崩溃,是灵异现象:被打死的人在地上还能开枪,被冻住的人解冻之后攻速变成两倍。这种 bug 你们项目组一定有人半夜三点修过。

《守望先锋》的解法是把这个问题从根上掐掉:让整个世界变成一个可以被整体复制的东西。他们给架构定了三条规矩,我认为这是理解 ECS 的关键,比任何性能数据都重要:

一、组件是纯数据。没有方法、没有虚函数、不指向别的游戏对象。想引用另一个实体?存编号,别存指针。这一条的价值在于——纯数据可以被无脑拷贝、可以被写进文件、可以被塞进网络包,不需要为每个类型定制序列化。

二、系统无状态。系统的所有输入来自组件,所有输出写回组件,系统自己不留“我上一帧算到哪了”的记忆。这一条最容易被抄漏,也最致命。系统一旦有内部状态,你就没法“回到上一帧重算”,因为系统的记忆回不去。

三、实体只是个编号。给一个实体加组件,就是往某块连续内存里多写一行。实体没有身份、没有类型、没有继承关系,它只是个索引。

三条加在一起,“保存世界”就变成了“把这堆数组拷一遍”,“恢复世界”就变成了“拷回来”。Tim Ford 在演讲里说,这套结构让他们能把整个游戏逻辑世界快照、回滚、再前滚,这是整个网络代码的地基。

但他还讲了一个我认为被严重低估的收益:当所有状态都在数据里,工具就能看见一切。编辑器里点一个实体,能列出它身上挂的全部组件;策划改一个数值不用重启游戏;程序员改一个系统可以热重载,因为系统之间只通过数据通信,换掉一个不影响别人。这三点对独立团队的杀伤力,比对大厂还大——大厂有钱造工具,你没钱,你只能靠架构本身长得干净。

给个量级感受(下面是公开演讲和社区分析里反复出现的量级,不是官方精确值,别拿去当规范抄):服务器模拟大概二十赫兹出头,客户端渲染六十帧以上,补偿窗口通常在一百到两百毫秒这个区间。这意味着每秒钟服务器要处理十几次“倒带—重算—再前进”的循环。你可以想象一下,如果你的世界是一张指针乱飞的对象图,这个循环会是什么地狱。

但代价必须讲清楚,不然就是耍流氓。ECS 让一个功能散落在几十个文件里。你想知道“敌人为什么会被冻住”,在 OOP 里你去 FrozenEnemy 这个类里找;在 ECS 里,你得在几十个系统里搜“冰冻”这个词,还得看它们之间的执行顺序。他们团队的对策是极严格的命名约定加大量内部工具。

我的看法很不客气:ECS 把复杂度从“代码结构”搬到了“命名规范和工具链”上,它没有消灭复杂度,只是换了个地方收租。你要是没打算在命名和工具上付这笔租金,就别搬进来住。

还有一个边界感必须提前说:《守望先锋》的 ECS 世界里只有游戏逻辑。渲染资源、音频、网络套接字、文件句柄,全都不在里面。这个世界小到可以被完整复制,才撑得起这套玩法。你如果什么都往里塞,它就不再是那个能被整体快照的世界了——这个坑我们后面单独骂(本系列第 27 篇专门聊 Flecs 和引擎的边界)。

第四层:Flecs 补的那块拼图,是“实体之间的关系”

时间快进到 2018、2019 年。ECS 已经不新鲜了,C++ 圈子里的 EnTT 已经被一批人用起来,Unity 那边 DOTS 还在憋大招,Bevy 连影子都没有。这时候荷兰开发者 Sander Mertens 开始公开他的 ECS 项目,就是后来的 Flecs。他在博客上连载过一整套“从零造 ECS”的文章,可以看作是 Flecs 的施工日志。

他当时看到的空位在哪?我的理解是这样:那几年的 ECS 生态是两极分化的。一极是极简派,给你实体和组件,性能做到极致,模板玩到飞起,但你想要“父子层级”“预制体继承”这种游戏里天天用的东西,得自己拿组件手搓一套。另一极是引擎内置派,什么都给你,但你被绑死在那个引擎上,动弹不得。中间那块地——一个能表达复杂世界结构、又不强迫你换引擎换编译流程的 ECS——是空的。

Flecs 有四个“生下来就不一样”的决定,我按对游戏设计的影响从大到小排:

决定一:关系是一等公民。父子关系(ChildOf)、原型继承(IsA)、以及你自己定义的任意关系,全部内建,不需要你拿普通组件拼出来。这一条最狠。你回想一下 OOP 里的“关系”是怎么表达的?指针、引用、对象数组。而在一个数据世界里,指针恰恰是最脏的东西:不能序列化、不能回滚、不能跨网络传、不能拿来查询。Flecs 的做法是把关系拆成“谁 + 什么关系 + 谁”,于是关系本身也变成了数据,能存、能传、能查、能被批量遍历。

举个特别具体的游戏场景你就能感受到差别。做一个三阶段 Boss:第一形态有护盾,护盾破了进第二形态狂暴,狂暴状态下免疫冰冻,打完进第三形态。用最朴素的纯数据 ECS 写:你需要一堆状态标记组件,然后在十几个系统里写“如果狂暴标记在,就跳过冰冻判定”。用 Flecs 写:把“狂暴”做成一个可以挂上去的标签,把“免疫冰冻”做成挂在那个标签上的附加关系,状态切换就是挂一个、摘一个。系统的判定逻辑不变,但状态之间的结构关系被框架管住了,你不用自己维护一堆互相打架的标志位。

决定二:纯 C99,不需要代码生成。不少 ECS 要求你先跑一个代码生成器,把组件注册成某种模板,编译流程多一步,改个字段要重新生成,CI 上还要多一个环节。Flecs 不要这一步。这个差别听起来只是工程洁癖,但对独立开发者的实际意义是:“今天下午能不能把想法试出来”。我见过太多独立项目死在流程摩擦上——不是想不出玩法,是每次试玩法都要走一遍三十秒的流程,走三十次以后人就懒了。

决定三:运行时的自由。组件、系统、关系都能在运行的时候加进去、拿掉。这对做编辑器、做 mod 支持、做热重载的人价值极大。对做普通游戏的人,这个能力的意义是:你的存档能带结构,你的调试工具能现场改世界。

决定四:世界是可以被整体操作的数据。这一条直接继承自《守望先锋》那条线。Flecs 把“世界”当成一个对象来对待,可以整体复制、序列化、跨世界迁移。这是本系列后面第 15 篇(网络同步与回滚)和第 17 篇(持久化与存档)的地基。你要是没理解这一层,读那两篇会觉得我在变魔术。

设计意图和代价得一起说。Sander 公开表达过一个取向:他不想做一个只有专家才能用的库。所以他在 API 的可读性和文档上花了大力气,这也解释了为什么 Flecs 的接口看起来比同类“重”一点——它多了一层你可以直接读懂的抽象。代价是,它在极限性能上不像某些极简框架那样把每个字节都榨干(这正是第 03 篇横向对比要摊开算账的地方)。这是个很典型的取舍:天平一端是上手成本,另一端是极限性能,Flecs 明确地往第一端压了一点点。

而我这个策划最在乎的,恰恰是这一端。因为独立游戏的死因排行榜上,“性能不够”排不进前十,“想法试得太慢”能排前三。

参数化判断:三张表帮你决定要不要上、从哪里开始迁

聊完历史,落到能用的东西上。下面三张表是我自己在项目里用的判断工具,刻度是我定的,不是学术标准,你可以按自己的项目手感调。

表一:该不该上 ECS(四项打分,总分 0-8)

维度0 分1 分2 分
峰值同时存在的实体数500 以下500 到 50005000 以上
行为组合复杂度每种单位只有一套固定行为有 5 到 15 种可叠加的状态状态互相组合,且每周都在加新的
是否需要“整体快照”不需要,重开一局就行存档要做,但不能读一半崩一半联机回滚,或者要做到秒级热重载
内容迭代频率一个月加几个新东西每周都在加每天都要试新组合

怎么读:0 到 2 分——别上,上了就是给自己加税;3 到 5 分——局部用,只把最高频的那一两个系统(通常是移动和碰撞检测)迁过去尝尝味道;6 到 8 分——值得全面上,这时候不上的代价已经超过上的代价了。

表二:三种数据摆法,什么时候用哪个

形态人话描述适合什么代价
按对象打包每个实体是一个完整包裹实体很少、字段经常一起用、编辑器要整体拖拽遍历浪费带宽,规模一大就疼
按字段分列同一个字段的所有值挤成一条连续数组字段各自被不同系统高频访问,比如位置和血量增删实体要维护一堆数组的同步,手写容易错
按组合分桶组件组合完全相同的实体,挤在同一张连续表里组合种类可控、需要大量按“拥有哪些组件”筛选组合种类一多就碎成很多小表,查询变慢(这叫 archetype 碎片化)

第三行就是 Flecs 走的路,也就是本系列第 06 篇专门拆解的 Archetype 架构。你现在只需要记住一句话:它把“哪些实体有同样的组件组合”这件事变成了物理上的分桶,好处是筛选极快,坏处是桶太多会碎。

表三:从 OOP 迁移的优先级和工时估算(以一个两万行左右的独立项目为参照)

迁移顺序涉及内容预估工时风险
第一批纯数据类:位置、速度、生命值、状态标记2 到 4 人日低,几乎无痛
第二批高频遍历:移动、碰撞粗筛、AI 感知范围3 到 6 人日中,收益最直观,适合拿来说服团队
第三批带第三方库状态的:物理刚体、动画状态机、音频源5 到 10 人日高,用“句柄组件”包一层就停手,别硬塞
随时可做编辑器与存档:建立结构可视化和序列化能力3 到 8 人日低,而且越早做越好,这是 ECS 最容易被浪费的红利

这张表里最值钱的是最后一行。太多团队迁完数据就跑了,编辑器还是老样子,结果享受不到“工具能看见一切”的红利,白瞎。你要是只做一件事,就做“点一下实体能列出它身上所有东西”这个功能,用不了一天。

双路径:五分钟速览,和想往下挖的人

【初级路径 · 五分钟版】

  • ECS 不是性能优化方案,它是“让整个世界能被打包、复制、回滚”的方案。性能是顺带的副产品。
  • OOP 真正崩掉的地方不是“跑得慢”,是“策划想加一个组合,程序要改三天”。你损失的是试错的次数。
  • DOD 一句人话:先想数据怎么排,再想代码怎么写。ECS 是 DOD 的一种制度化写法,不是唯一写法。两者别混。
  • 你的游戏如果峰值实体不到 500、状态就那么几种、也不联机不做热重载,别上 ECS。上了你付出的是可读性,买回来的东西你用不上。
  • Flecs 的独特之处是“关系”这件事被当成一等公民。这决定了它更适合“世界结构复杂”的游戏(大量层级、装备、增益来源),而不是“单位特别多”的游戏。

【进阶路径 · 给已经写过一点 ECS 的人】

先聊《守望先锋》那套架构最常被误读的三个地方。第一,很多人以为它的核心是性能,其实核心是可快照性——性能是附赠的,因为数据连续排布天然快。第二,很多人抄了“组件是纯数据”就收工,把“系统无状态”漏了,结果一遇到回滚或者并行就全乱。第三,很多人以为 ECS 必须配一套多线程作业系统,这也是误读,2016 年那一版在模拟层面主要还是单线程,多线程是后来的事,本系列第 10 篇会把边界摊开讲。

再聊框架的“诞生语境”,这个视角比对比功能表有用得多。你可以这么理解:有的框架诞生于“我要把一个东西做到极致轻”,有的诞生于“我要在这门语言里提供一整套游戏框架”,Flecs 诞生于“我要一个能表达复杂世界结构、又不强迫我换编译流程的东西”。选框架本质上是在选你和作者的痛点像不像。痛点像,用起来就顺手;痛点不像,你会天天觉得这个设计多余。

最后说一个我自己的判断:“关系是一等公民”这件事,对独立开发者的价值远大于对大厂。大厂有钱造工具,关系可以拿普通组件加查询手搓,反正有专人维护;独立开发者一个人干五个人的活,如果框架不帮你把世界结构管起来,你就得自己造一套小型的对象数据库,造完你发现你已经三个月没碰过玩法了。这就是我这几年越来越偏向“框架要替我把结构管住”的原因——不是懒,是时间真的不够。

五个坑,我至少踩过三个

坑一:把 ECS 当性能银弹。说句实话,实体数在一千以下、组件种类二十种以内的项目,ECS 和 OOP 的帧时间差经常落在测量噪声里。你换来的可能是三周的迁移和半年的“这个功能到底在哪个系统里”。先拿性能分析工具测,再决定换不换。凭感觉换架构,是最贵的一种玄学。

坑二:只抄“组件是纯数据”,不抄“系统无状态”。我见过一个项目,组件确实是纯数据,但系统里塞了静态缓存、塞了全局单例、塞了“我上一帧算出来的中间结果”。平时跑得好好的,一做回滚就出鬼,一做并行就崩。判断标准特别简单:这个系统能不能被销毁重建、然后一切照旧?不能,它就带状态,它就不合格。

坑三:世界变成垃圾桶。UI 状态、配置表、贴图句柄、网络连接对象,全往组件里塞。判断标准也很好记:这东西需要被快照吗?需要被网络同步吗?需要被很多系统同时读吗?三个都不需要,它就不该进世界。它该待在一个老老实实的普通对象里。

坑四:为想象中的规模提前上 ECS。这个我亲自翻过车。一个还没验证玩法的原型,我先花两周把 ECS 骨架搭得漂漂亮亮,结果玩法被砍,骨架跟着进坟墓。教训是:架构要为一个你已经验证过的痛点服务,而不是为一个你想象中的未来服务。

坑五:关系滥用,把世界切得稀碎。这是 Flecs 特有的一种疼。每引入一种新的关系,世界的分桶方式就多切一刀;上百种关系、上千种组合叠起来,内存会碎、查询会慢。第 06 篇会讲怎么量化诊断这个问题,你现在先记一句:关系是给“世界上真实存在、且需要被查询的结构”用的,不是给你省一个字段用的。

还有一个不算坑但很多人纠结的问题:我要不要自己造一个 ECS?我的答案是,除非你的核心玩法本身就跟 ECS 强相关,否则别造。造一个能用的 ECS 是三个月,造一个别人愿意用的 ECS 是三年,而你的游戏可能只需要六个月。

FAQ

问:ECS 和 Unity 那套 GameObject-Component 到底差在哪?看起来不都是“往对象上挂东西”吗?

差在两件事。第一是内存:Unity 那套里每个组件还是一个独立对象,散在堆上,遍历时要一个个跳;ECS 把组件按组合排成连续的内存块,遍历时是一条直线走到底。第二是逻辑的位置:Unity 那套里逻辑一般写在组件自己的更新方法里,一个敌人身上可能挂着七八个组件各自跑各自的;ECS 里逻辑全部在系统里,系统按固定顺序跑,可以并行、可以快照、可以热重载。挂东西这件事是一样,但东西挂完之后能不能被整体搬走,完全不一样。

问:我的游戏峰值就两百个实体,上 ECS 是不是杀鸡用牛刀?

看你的实体规模确实没必要。但你可以反过来看另外三个维度:状态组合是不是在爆炸式增长?是不是要做可靠的存档和版本迁移?是不是要频繁试新玩法?三个里中两个,就值得上用,哪怕实体很少。因为你买的是迭代速度和结构清晰度,不是吞吐量。反过来,如果这三个都没有,那你就是拿牛刀杀鸡,还容易切到自己的手。

问:《守望先锋》那套架构我能不能直接照着抄?

能抄的是三条原则:组件纯数据、系统无状态、实体只是编号。抄不了的是它的边界——他们的 ECS 世界里只有游戏逻辑,渲染、音频、网络接口全在外面。你要是把渲染资源也塞进去,那个“整体快照”的能力立刻就废了,因为你没法把一张纹理拷一份再恢复回去。抄架构的人十有八九死在边界上,而不是死在结构上。

问:Flecs 为什么用 C 写?对我有什么影响?

用 C 写的好处是:能被几乎所有语言调用(C++、Rust、C#、Lua、Python 都有绑定),编译产物简单,不依赖某个特定的编译器版本,嵌到别的地方也容易。对你最直接的影响是:你不需要为了用它而把整个项目的语言换掉,第 16 篇专门讲各种语言绑定怎么接。代价是 C 的接口比 C++ 啰嗦一点,有些写法不如模板库那么顺手。

问:DOD 和 ECS 到底是不是一回事?

不是。DOD 是一种思维方式:先问“这份数据会以什么顺序被访问”,再决定代码怎么写。ECS 是这种思维的一种工程约定。你可以 DOD 但不用 ECS,也可以用着 ECS 却写出完全不 DOD 的代码。后者是行业里最普遍的病症,症状是:框架换了,帧时间没变,代码还更难读了。

最后说一个反直觉的观察

这篇聊了一堆历史,但我想留给你一个不带历史感的判断标准。

ECS 最大的好处不是跑得快,是让策划能问出以前不敢问的问题。比如“如果所有敌人都会互相转化阵营会怎样”“如果这栋楼能一边着火一边被冻住会怎样”“如果每个 NPC 都记得自己见过谁”。这些问题在 OOP 项目里通常会被一句“这改起来太麻烦了”提前毙掉,在组合式的架构里,它们可能只需要一个标签加一个系统。

所以你判断自己该不该换范式,别去数实体数量,也别去看别人家的基准测试。你去看你们团队的会议记录,数一数有多少个点子是被“实现成本”这四个字杀掉的。如果一个月里出现过三次以上,那问题就不在性能上,在你的世界长什么样。

下一篇我们接着往下走:Flecs 从 1.x 到 4.x 这几年,到底改了些什么,哪些改动是真正的架构跃迁,哪些改动把老用户坑得骂娘——一个框架的健康度,往往就藏在这些破坏性升级的社区反馈里。

文章标签
Flecs ECSEntity Component SystemC++ ECSArchetype ECSSparse Set ECSEnTTBevy ECSUnity DOTSECS 框架对比Flecs RelationshipsFlecs QueryFlecs Pipeline
更多专题全部专题
觉得有价值?点赞或收藏支持内容持续产出。
← 返回专题:Flecs ECS 游戏开发技术专题