大规模项目的 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 到 80 | 150 到 400 | 400 到 800 |
| 系统数 | 30 以内 | 40 到 120 | 100 到 250 |
| World 数量 | 1 | 1 到 2 | 1 到 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 | 超过 3000 | Explorer | 把变体标签降级为数据字段 |
| 平均每 Archetype 实体数 | 大于 20 | 小于 5 | 同上 | 同上,或合并组件组合 |
| 组件平均字段数 | 3 到 8 | 超过 12 | 代码走查 | 按消费者切分 |
| 系统总数 | 中型 40 到 120,大型 100 到 250 | 单迭代增长超过 20% | 系统列表 | 合并同频系统 |
| 单系统耗时占比 | 低于 15% | 超过 25% | 性能剖析 | 拆分、降频或异步化 |
| 关系种类数 | 小于 30 | 超过 50 | Explorer 关系视图 | 合并语义相近的关系 |
| 每实体平均组件数 | 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:什么时候该把一个子系统拆成独立的库?
我的判断标准是三条,满足两条就拆:一,它被两个以上项目用到了;二,它的迭代节奏跟主项目明显不同(比如它半年才改一次,主项目天天改);三,它有自己的依赖,而且那些依赖主项目其实不想带。反过来说,如果它只是「看起来很独立」,拆出去只会让你每次改功能都要在两个仓库之间来回跳,那还不如老老实实放在一个模块里。
最后丢一个可能有点反直觉的观察给你:大规模项目里最难管的从来不是代码,是「约定」。命名、边界、巡检节奏,这些东西没有一条是技术难题,但它们是十万行项目能不能活下去的真正分水岭。代码写错了可以改,约定崩了,你会连「哪里错了」都找不到。你要不要现在就打开自己的项目,数一数有几个名字在描述同一个概念?我猜至少三个。