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

大规模项目的 Flecs 架构设计:10 万行代码级别的工程实践

· 0 分钟阅读
大规模项目的 Flecs 架构设计:10 万行代码级别的工程实践 Flecs Architecture at Scale — Flecs ECS 技术专题

大规模项目的 Flecs 架构设计:10 万行代码级别的工程实践 Flecs Architecture at Scale

为什么「规模」是第一块基石

你第一次用 Flecs 那天,多半是这样的:二十个组件,六个系统,一个 World。改一行,跑一遍,五秒钟看到结果,爽得像刚学会骑车。

三年后,你的项目十万行,团队六个人。某天早上你打开 Flecs Explorer 左栏的组件列表,滚了七屏。里面有四个东西都在描述「位置」,两个都在描述「血量」,还有一个叫 GameData 的组件,四十个字段,你完全想不起来是谁加的——你只知道删了它游戏会崩。

这就是规模。它不是一个技术问题,是一种传染病。

这篇文章不讲 Flecs 的 API 怎么调,那种东西文档写得比我好。我想聊的是:当代码从一万行长到十万行,那些第一天就该定、但没人告诉你要定的事。十万行的核心矛盾从来不是「性能不够」,而是变更成本——改一行代码要动几个文件、要问几个人、要跑几轮测试、要担心哪几个老 bug 复活。ECS 把数据摊平了,摊平之后,秩序得有人管。

下面四层,我一层层拆。第一层讲阶段,第二层讲命名,第三层讲多 World,第四层讲技术债。全是我和同行真实踩过的坑,不是从文档抄的。

第 1 层:架构不是设计出来的,是分阶段预支出来的

先扔一个可能让你不舒服的判断:没有人能在项目第一天设计出十万行的架构。所有号称「一开始就搭好架子」的项目,要么架子后来全拆了,要么根本没长到十万行。

真正活下来的架构,都是「预支」的:你在一万行的时候,花两天做一次小重构,换来接下来两万行的顺畅;在五万行的时候再花一周,换来接下来五万行。每次预支一点点,成本是线性的。反过来,如果你一直不管,等到十万行再统一重构,成本是超线性的——你会碰到「改一个组件名要动两百个文件」和「这系统谁也不敢碰」的死亡螺旋。

原型期(0 到 1.5 万行,1 到 3 人)

这个阶段唯一的目标是:验证核心玩法循环能不能跑通。不是「能不能扩展」,是「能不能跑通」。

我的建议很粗暴:一个 World,组件随手加,系统按注册顺序排,允许写两百行的大系统,允许在一个系统里直接改另一个系统的数据。这个阶段最贵的错误是提前抽象。

说个我自己的糗事。四年前做一个卡牌项目,原型期我就兴致勃勃搭了「事件总线 + 模块注册 + 存档版本迁移」三件套,写了大概三千行基础设施,觉得自己特别有远见。两个月后玩法推倒重来,从即时战斗改成回合制,那三件套一行没用上,还挡了我两周的路。

原型期的量化参考:组件数做到 30 到 80 个是健康的。如果你在做原型,组件数已经破 150,那不是「设计得好」,那是你在做中型项目的事——要么砍功能,要么老老实实承认自己已经过了原型期。

什么时候该升级?三个信号,中一个就动:
一,团队来了第三个人。两个人可以靠喊,三个人开始靠记忆,第四个人来了就开始靠猜。
二,连续两周出现「改一个组件要动五个以上文件」。
三,有人问你「这个逻辑在哪儿」,你答不上来。

中型期(1.5 到 5 万行,3 到 8 人)

中型期的核心动作只有一个:切边界。

切法我有一个很土但很管用的标准:按「玩家能用一句话说清的功能」来切。战斗、经济、建造、AI、存档——这些都是一句话能说清的。别按技术层切(「渲染层」「逻辑层」「数据层」),那种切法在项目中期看着很清爽,上线后一点都不好用,因为改一个「技能特效」要同时动三个层。

Flecs 的 Module 系统正好是干这个的(完整做法在 [20] 篇展开,这里只说结论):一个模块一个组件集合,模块对外只暴露少量「公开组件」,其余都是内部实现。这条规矩听着简单,但我见过太多团队败在这里——A 模块的 AI 系统直接读了 B 模块的二十个内部字段,等到 B 要重构,A 就跟着陪葬。这就是没有边界的代价。

中型期还必须做一件事:把隐藏的系统执行顺序显式化。很多中型项目的技术债,第一笔就是「这个系统必须跑在它后面」这种注释。Flecs 有依赖声明机制([09] 篇讲),你把它用起来,改注册顺序就不会莫名其妙炸掉。

量化参考:组件 150 到 400,系统 40 到 120,模块 6 到 12 个。超过 12 个模块,通常说明你切得太碎,或者有些「模块」其实是一个功能的两个面——那就该合并。

大型期(5 万行以上,8 人以上,长期运营)

大型期的关注点从「怎么组织」变成了「怎么不让它腐化」。这个阶段的敌人不是需求,是时间——代码活过三年,必然有人走有人来,必然有「临时方案」悄悄变成永久方案。

三条我见过的、真正能执行下去的铁律:

一,跨模块只通过组件数据通信,不通过系统调用。A 模块的系统不能调用 B 模块的函数,只能读写 B 模块公开的组件。这条不守,模块边界就是纸糊的,一次紧急需求就能捅穿。

二,任何组件的删除都走「废弃、观察、移除」三段。先打废弃标记,让所有引用报警,观察两个迭代,确实没人用了再删。在大项目里直接删组件等于扔炸弹,而且炸的是三个月后的你。

三,每周一次架构巡检,十五分钟。就三件事:组件总数有没有异常增长,Archetype 数量有没有暴涨,系统耗时排行榜有没有新的头名。这个节奏听起来轻得不像正经流程,但它是你发现问题最便宜的方式——比等玩家报 bug 便宜一百倍。

插一个案例:守望先锋的几年赌注

说到「预支」,绕不开《守望先锋》。Tim Ford 在 GDC 上公开讲过他们的迁移过程:搬到 ECS 架构时,项目代码量在几百万行级别,团队几十人,整个过程横跨数年。

他们最看重的其实不是性能,是网络同步。在 ECS 里,「哪些数据需要同步」变成了一个可以枚举的东西:组件就是同步单位,你把一个组件标成「需要同步」,整个系统的同步策略就跟着变了。这在老架构里根本做不到——数据散落在各种对象里,你得靠人工维护一份清单,而且一定有遗漏;一旦遗漏,就是线上那种「两个人看到的位置不一样」的诡异问题。

但你猜代价是什么?不是技术,是人和工具。几百万行代码的整体迁移,意味着工具链要重建(调试器、编辑器、序列化、性能剖析全都得跟上),意味着几十个人要重新学一套思维方式,意味着在迁移完成之前你要同时维护两套东西。这才是大规模项目做架构演进的真实成本。

这就是「预支」的另一面:预支得早,成本可控;预支得晚,成本会以「人」为单位结算。这也是为什么 Flecs 这类框架对独立团队有价值——它把「工具链」这一环替你做了大半(Explorer、序列化、关系系统都是现成的),你至少不用为了一个能看的调试面板再养两个人。

维度原型期中型期大型期
代码规模1.5 万行以下1.5 到 5 万行5 万行以上
团队人数1 到 3 人3 到 8 人8 人以上
组件数30 到 80150 到 400400 到 800
系统数30 以内40 到 120100 到 250
World 数量11 到 21 到 4
模块边界没有强,有内部与公开之分强,且对外 API 冻结
序列化契约随便存开始管,建映射层版本化,兼容旧档
重构节奏随缘每个迭代末一次每周巡检一次

第 2 层:命名是团队协作的底层协议

讲命名之前,得先说清 Flecs 里「名字」到底是什么东西,不然这层就变成喊口号了。

每个实体有一个编号,这是它的真身,全部逻辑都靠它。除此之外,你还可以给实体挂一个名字字符串。这个名字不参与任何逻辑判断,但它在两个地方至关重要:一是在 Explorer 里你找不找得到东西,二是序列化成存档之后数据长什么样——而存档长什么样,直接决定了你明年做兼容有多痛苦([17] 篇专门讲这个)。

问题在于:Flecs 的命名空间基本是平的。不像面向对象那样天然有包和类的嵌套。模块能提供点号前缀,算是有限的分层,但如果你不管,一个六人团队干满一年,组件列表里一定同时存在:位置、Position、pos、transform、Transform、WorldTransform、LocalTransform——七个名字,三个概念。而每个人都会觉得自己的命名是最合理的那个。

三层命名法

第一层,模块前缀,2 到 4 个字符小写加下划线:combat、eco、build、ai。这层的作用是让组件列表能排序、能分组、能在 Explorer 里一眼看出归属。没有这层,你的组件列表就是一锅乱炖。

第二层,语义中段,用名词,不缩写:health、velocity、inventory 配上模块前缀拼起来。别写 trns、vlc 那种东西,你省下的三个字符,将来会被一个新人花三十分钟还回来,而且他还得先问你。

第三层,类型后缀,用来标数据形态。这是最容易被忽略、但收益最大的一层。我的分法很简单:只表示「是/否」、没有数据的是标签;只存一个枚举状态的是状态组件;存数值或结构体的是数据组件;指向另一个实体的是关系。

你想表达的东西该用什么为什么
这个敌人「是精英」标签只有是/否,没数据,不该占内存
这个敌人「处于追击状态」状态组件(枚举)会切换,且需要被查询和统计
这个敌人「血量 320,上限 500」数据组件多个数值,高频读写
这个敌人「正在攻击玩家 X」关系指向另一个实体,还要能反查谁在打我
这个敌人「刚被打中」观察者事件一次性,不该长期占着组合

关系谓词的命名建议用动词或现在分词:Owning、Targeting、Following、ChildOf。因为它读起来像句子——「敌人 Targeting 玩家」,半年后你回来看,不用查文档就懂。这是 Flecs 关系系统([08] 篇)一个被严重低估的好处:它让代码读起来像自然语言。

一份反例清单,照着删

  • 同一个概念多种拼写。这是头号杀手,因为它不报错,只是让你永远找不到东西。
  • 缩写。省字符的成本极低,理解成本极高,亏本买卖。
  • 带数字后缀的东西,比如 enemy2、state3。这是「临时方案泄漏到正式代码」的味道,看到就该警觉。
  • 描述实现细节的,比如 HealthFloat32、PositionVec3。万一以后换成双精度呢?万一以后改成定点数呢?
  • 描述来源的,比如 PlayerHealthFromNetwork。那是接口的事,不该写进组件语义里。

版本化:中型期就该立的法

有语义变更怎么办?不要原地改。加一个 V2 后缀,新旧双写两周到一个月,等所有消费方都迁过去,再删旧的。这条规矩应该在中型期就立法,因为它在早期成本几乎为零,在晚期成本几乎无限——你想想上线之后每改一次核心组件都要写一次存档迁移是什么感受。

一个真实的小故事

我踩过最蠢的一次:一个项目里,战斗系统定义了「当前血量」,UI 系统定义了「血量百分比」,养成系统定义了「最大血量」。三个组件,三个数值,三个人各写各的,谁也没觉得有问题。

某天测出来一个 bug——A 打 B,B 的血量掉了,但血条不动。查了两天,原因是 UI 读的是百分比组件,而百分比只在养成系统结算时才更新。这不是 Flecs 的锅,这是命名的锅:如果当初有一个统一的「生命值账本」概念,三个系统不可能各写各的。命名不只是好看,它是团队对「世界上存在哪些概念」达成的共识。共识没达成,代码就会替你说谎。

第 3 层:多 World 是手术刀,不是锤子

Flecs 的 World 不是一个「命名空间」,它是一个独立的宇宙:实体表、组件注册表、系统管线、关系存储,全部私有。两个 World 之间没有共享实体,没有跨 World 的关系,数据要过去只能手动搬。

这个设计对大规模项目是双刃剑。用来做隔离,它非常干净;用来做分区管理,它是个陷阱。

场景该不该开新 World理由
编辑器运行时 vs 游戏运行时该编辑器实体不该被游戏系统遍历到,隔离彻底
匹配制服务器,一局一房间该销毁房间等于销毁整个 World,没有残留
单元测试该隔离是测试的全部意义
大世界里分区(玩家能串门)不该用空间索引和关系,物理隔离会撕裂世界
只是「逻辑分组」不该标签和关系就够了,World 太重
关卡加载与卸载不该删除实体就够,重开 World 反而更慢
回滚快照不该同一个 World 内序列化更快([15][17] 篇)

代价得说清楚,别以为多开一个 World 是免费的午餐。第一,内存不省反增:每个 World 都有一份自己的组件注册表和 Archetype 表,开两个等于多背一份目录开销。第二,调试变麻烦:Explorer 一次只看一个 World,你得来回切。第三,多 World 不是并行的手段,线程安全是另一套东西([10] 篇讲)。

最危险的坑:编号跨世界误用

我见过最阴的一个 bug:一个团队把编辑器 World 里某个实体的编号存进了配置表,然后运行时在游戏 World 里查这个编号。两个 World 的编号分配是独立递增的,那个编号在游戏 World 里刚好指向了另一个实体——于是配置表里的「boss」变成了一个路灯。

游戏不崩溃,只是行为诡异。这种 bug 平均要查一周,而且查出来之后你会想抽自己。

工程上的对策很土但有效:在调试构建里给实体编号的高位打上世界标记,任何跨 World 使用时立刻报错崩掉,发布构建把这个检查剥掉。宁可让它在开发期崩一次,也不要让它在玩家机器上诡异三个月。这条经验我建议你直接抄,别自己重新发明一遍。

但有一种情况多 World 是真的香

匹配制的服务器。那种「一局开一个房间,打完整个房间消失」的游戏,把每个房间做成一个 World,最大的好处是销毁房间变成了销毁整个 World——没有残留实体、没有没清干净的定时器、没有跨局泄漏的状态。这一条能省掉无数个「为什么第二局会出现第一局的鬼影」的问题。

反过来,如果你的玩家能跨房间串门、能跨服交易、能在一个无缝大世界里跑,那多 World 就是灾难。那时候你该用的是单 World 加空间索引和关系,而不是物理隔离。一刀切永远是错的,得看你的游戏是不是天然分成互不相干的块。

第 4 层:ECS 技术债的五种典型形态

ECS 的技术债跟传统面向对象的技术债长得完全不一样。面向对象的债多半是「继承树太深」和「上帝类」,而 ECS 的债更像是数据秩序的崩坏。我总结了五种最常见形态,基本上覆盖了我见过的八成烂摊子。你可以拿它当体检清单,对着自己项目一条条划。

形态一:万能组件

症状:某个组件字段数超过 20 个,其中一半只在特定状态下有意义。

代价:这条最隐蔽。一个胖组件会被挂到大量实体上,每个实体都背着那二十个字段的内存。更糟的是遍历时的浪费——CPU 需要跳过整块用不上的数据,缓存带宽白烧。这一点 [06] 篇讲 Archetype 时会详细拆,这里你只要记住结论:组件越胖,你浪费的内存带宽越多。

重构动作:按「谁读它」来切。一段数据只有一个系统在读,那它该独立;一段数据被八个系统读,那才是核心组件。切的时候别心疼——你会惊讶于有多少字段其实只有一个消费者,只是当初为了图省事塞在了一起。

形态二:Archetype 爆炸

症状:Explorer 里 Archetype 数量冲到四位数,但平均每个 Archetype 只挂着三五个实体。

代价:内存跟着实体组合走,碎片化严重;遍历时「每种组合一小撮」,批量效率暴跌。这是 ECS 最反直觉的一种债——你以为组件拆得越细越好,结果拆出了一地碎片。

重构动作:把低频的「变体信息」从「组件组合」降级成「数据字段」。比如你有二十种敌人类型,每种一个标签,那就是二十种组合;改成「一个组件里的一个枚举字段」,组合数立刻塌缩。另一条:把「临时标记」(比如「刚被击中」)改成观察者事件([11] 篇),别让它们长期挂在实体上占着位置。

形态三:系统顺序耦合

症状:代码注释里写着「必须跑在某某之后」,或者一个系统读取了「上一个系统刚写进去」的中间状态。

代价:任何人调整注册顺序都会引发连锁崩溃,最后没人敢动,系统列表变成雷区。

重构动作:用依赖声明代替注册顺序([09] 篇)。更根本的一条是读写分离:一个系统只写自己负责的组件,只读别人公开的组件。中间状态通过组件传递,而不是通过调用顺序传递。这条一旦做到位,你会发现很多「必须按顺序」的迷信自动消失了。

形态四:关系深链

症状:每帧都在做多层关系遍历,比如「从这里往上找三层父节点,再拿它的某个属性」。

代价:关系遍历比普通组件查询贵不少,深链更是贵上加贵。十个实体的层级不觉得,十万个实体就是帧时间黑洞。

重构动作:把「每帧要用的层级结果」缓存成组件,只在关系变更时更新——这正好是观察者擅长的活([11] 篇)。缓存一个世界坐标,比每帧爬十层树便宜太多,而且这个改动通常是几个小时的事,回报却是永久的。

形态五:序列化契约失控

症状:存档的字段名和结构直接等于组件名和内存布局,改一次组件就要写一次存档迁移。

代价:一旦上线,你每次重构都背着历史包袱。改得越多,迁移代码越多,最后迁移代码本身成了第二套系统。

重构动作:组件和存档结构解耦,中间加一层映射([17] 篇)。代价是每次加字段要动两处,收益是你随时可以重命名和重组组件。这笔交易对任何打算长期运营的项目都是划算的。

债务形态量化信号重构动作风险
万能组件字段数超过 20按消费者切分低,纯内部重构
Archetype 爆炸组合数超 3000 或每组合实体少于 5标签降级为字段、临时标记改事件中,涉及查询改动
顺序耦合注释里出现「必须跑在之后」改依赖声明、读写分离中,要重跑全部功能测试
关系深链剖析里层级遍历排进前五缓存成组件,观察者更新低
契约失控存档字段与组件字段重合度超 80%引入映射层高,要处理历史存档

案例:城市天际线 2 的预算支票

说到「债」,2023 年 10 月的《城市:天际线 2》是个绕不开的样本。

前作里的市民是「统计意义上的市民」——你看到的是数字,背后没有真实的个体行为。续作 Colossal Order 想做的事,是把每个市民做成一个真实实体:有自己的家、工作、通勤路线,每天真的在路上跑。这个设计目标,用传统对象模型基本免谈——几十万个个体,每个都有状态和行为,对象之间的指针到处乱飞,光内存布局就能把 CPU 拖死。所以他们用了 Unity 的 DOTS,也就是跟 Flecs 同宗的 Archetype ECS 那一套。

从技术上说,ECS 让这个设计成立了。从产品上说,它撞上了预算。发售时的性能争议极大——玩家抱怨帧数低、CPU 吃满、显卡也吃满;官方的回应里提到过模拟负载和渲染的双重压力,之后发了好几轮优化补丁。

我不想在这里评判「这游戏优化好不好」,那是另一个话题。我想说的是它对大规模架构的启示:ECS 给你的是「能做」的能力,不是「做得够快」的保证。当你决定把「统计市民」变成「真实市民」的那一刻,你其实是在签一张性能支票——模拟规模从几千涨到几十万,CPU 预算就那么多,你得先算清楚这笔账,再决定架构怎么落。

这就是我为什么一直强调「先算预算,再选架构」。十万行代码的项目,最贵的技术债往往不是代码写得丑,而是架构承诺超出了硬件预算。丑代码可以慢慢改,承诺收不回来,只能靠优化和降级去还,而且还得在玩家已经骂完一轮之后还。

补一个观察:看不见的大规模用户

Flecs 这个框架,你在独立游戏社区看到的讨论热度,跟它在大公司内部的实际使用量,是不成比例的。作者公开提过有体量很大的公司在用,但具体项目往往不发声——因为它们是内部工具、长期运营的服务端、或者不方便公开的技术中台。

这件事本身值得琢磨:大规模 Flecs 项目大多活在你看不见的地方,而你能看到的热闹,主要是独立开发者和小团队。这两拨人面对的问题根本不是一回事,前者担心「能不能跑通」,后者担心「三年后还能不能改」。这篇文章更关心后者——因为大部分读到这里的人,赌的是三年不是三个月。

一张表给自己项目做体检

下面这张表是我自己用了三年多的版本,每次迭代末花十五分钟对着 Explorer 和性能剖析过一遍。数字不是圣经,是我见过的健康区间,你按自己项目调。但它至少能给你一个「什么时候该警觉」的参照,而不是凭感觉。

指标健康区间告警线怎么看超了怎么办
组件总数中型 150 到 400,大型 400 到 800单迭代增长超过 15%Explorer 组件列表冻结新增一周,合并同义组件
Archetype 数量中型 100 到 600,大型 600 到 2000超过 3000Explorer把变体标签降级为数据字段
平均每 Archetype 实体数大于 20小于 5同上同上,或合并组件组合
组件平均字段数3 到 8超过 12代码走查按消费者切分
系统总数中型 40 到 120,大型 100 到 250单迭代增长超过 20%系统列表合并同频系统
单系统耗时占比低于 15%超过 25%性能剖析拆分、降频或异步化
关系种类数小于 30超过 50Explorer 关系视图合并语义相近的关系
每实体平均组件数8 到 20超过 30统计脚本检查万能组件
World 数量1 到 2超过 4启动日志逐个评审是否真有必要
存档字段与组件字段重合度低于 50%超过 80%人工比对引入映射层
单帧查询次数低于 200超过 500性能剖析合并查询、加缓存

再给一份「迭代末十五分钟巡检清单」,五条,照着做就行:

  • 打开 Explorer,看组件列表新增了多少,有没有两个意思几乎一样的。
  • 看 Archetype 数量和「每个组合平均几个实体」,掉到五以下就要动手。
  • 看系统耗时排行榜,有没有某个系统悄悄从第六名爬到第一。
  • 看这一轮有没有新增「必须跑在之后」的注释,有就当场改成依赖声明。
  • 看这一轮有没有为了赶进度写的临时组件,给它打个标记,下轮必须处理。

你该读到哪一层

初级路径:三万行以下、五人以内

这个规模你只需要做三件事,其余全都可以先不管。

一,定命名规范,模块前缀加语义中段,一个下午就能定完,收益却贯穿整个项目。
二,维护一份组件清单,一页纸,写清楚每个组件是干什么的、谁在用。听起来原始,但它能挡掉一大半「我们是不是已经有一个类似的东西了」的重复造轮子。
三,忍住不要提前抽象。原型期搭基础设施是这个阶段最常见的自杀方式,我自己就干过。

这三件之外的东西,暂时都别碰:多 World、模块 API 冻结、序列化版本化、依赖声明——它们在你的规模上是纯负担,等你长到那个体量再说。

进阶路径:五万行以上、八人以上、打算活三年

这个规模你需要五件事,缺一件都会在半年内找上门。

一,建立阶段评审机制。每次代码量跨越一个量级(比如从三万到六万),强制开一次半天的架构评审,问一个问题:现在的结构还撑得住下一个量级吗?
二,模块边界与 API 冻结。公开组件列表一旦冻结,改动要走评审。内部组件随便动,这给了团队自由度。
三,多 World 决策评审。任何新增 World 都要写一句理由,写在文档里。这一条能挡掉九成的「我觉得分开比较清晰」。
四,技术债台账。上面那五种形态,发现一条记一条,不要指望「有空再改」。台账的意义是让它可见,可见才会被处理。
五,巡检节奏。每周十五分钟,雷打不动,最好固定在同一天同一时间,做成习惯而不是任务。

说句实话,这五件事里最难的不是技术,是让团队接受「花时间在看不见的地方」。你得准备一套说辞,比如「这十五分钟省的是后面三个人各花两天」,而且最好真的拿一次事故去证明它。人是被事故教育出来的,不是被道理。

五个我亲眼看人掉进去的坑

FAQ

Q1:十万行是不是 Flecs 的上限?

不是。十万行是个心理门槛,不是技术门槛。真正决定上限的是「实体规模乘系统复杂度」和「团队对约定的执行力」。我见过两万行就一团糟的项目,也听说过体量大得多的内部项目在稳定跑。判断你还能不能撑的不是代码行数,是「改一个功能要动几个文件、要问几个人」——这个数字开始往上爬的时候,才是真该警觉的时候。

Q2:我们的组件数已经 600 了,要立刻合并吗?

先别急着合并,先看两个数:Archetype 数量和平均每个组合挂几个实体。如果 600 个组件对应的组合数在一千五以内、平均每个组合有二十个以上实体,那其实是健康的,只是名字多。真正需要动手的信号是「组合数超三千」或者「平均每组合不到五个实体」。别为了数字好看去合并语义不同的组件,那才是真的埋雷。

Q3:团队里有人坚持写三百行的大系统,怎么劝?

先别劝,先问一句:「这个系统里哪两段逻辑,将来最可能被不同的需求改动?」如果能答出来,那就按那个缝切开,一切一个准。如果答不出来,说明他其实没想清楚职责,这时候切也是瞎切。大系统本身不是罪,职责混杂才是。我自己写过一个两百多行的生成器系统,三年没动过,因为它职责单一、只干一件事——这种系统再长我也不想拆。

Q4:多 World 能不能用来做多线程?

不能,至少不能当成主要手段。多 World 给的是「隔离」,不是「并行」。你要并行,靠的是系统级的依赖分析和调度([10] 篇讲这个),而不是把数据切开塞进不同的宇宙。而且多 World 之后你会发现同步成本暴涨——数据要跨 World 只能手动搬,搬的时候还得自己保证线程安全,绕了一圈回到原点。

Q5:什么时候该把一个子系统拆成独立的库?

我的判断标准是三条,满足两条就拆:一,它被两个以上项目用到了;二,它的迭代节奏跟主项目明显不同(比如它半年才改一次,主项目天天改);三,它有自己的依赖,而且那些依赖主项目其实不想带。反过来说,如果它只是「看起来很独立」,拆出去只会让你每次改功能都要在两个仓库之间来回跳,那还不如老老实实放在一个模块里。

最后丢一个可能有点反直觉的观察给你:大规模项目里最难管的从来不是代码,是「约定」。命名、边界、巡检节奏,这些东西没有一条是技术难题,但它们是十万行项目能不能活下去的真正分水岭。代码写错了可以改,约定崩了,你会连「哪里错了」都找不到。你要不要现在就打开自己的项目,数一数有几个名字在描述同一个概念?我猜至少三个。

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