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

Flecs 性能调优实战:从基准测试到生产优化的完整方法论

· 0 分钟阅读
Flecs 性能调优实战:从基准测试到生产优化的完整方法论 Flecs Performance Tuning in Production — Flecs ECS 技术专题

Flecs 性能调优实战:从基准测试到生产优化的完整方法论 Flecs Performance Tuning in Production

为什么性能调优要在你写第一个组件之前就开始

先说个我亲眼见过的场面。一个三人小队做了两年半的动作游戏,内容齐了,美术也过了,Demo 拿去展会。结果在笔记本上稳 60 帧的 Boss 战,换到一台中端安卓机上,进入二阶段的那一刻直接掉到 31 帧,之后就在 40 上下抖。制作人当场脸就白了。

回去查,渲染不是瓶颈,物理也不是。最贵的是"实体搬家"——Boss 每进入一个新状态,就给一堆实体挂上或摘掉状态标记;团队当初觉得这样写特别优雅,一个状态一个标记,加一个就是开一个新功能。结果战斗一激烈,每帧几千次组件增删,数据在两个表之间来回倒腾。要改?整个战斗系统的写法都得翻一遍。这不是"优化",这是"返工"。

所以我一直跟人说:帧率是玩家唯一能看见的代码质量。策划写的剧情、美术画的贴图、音频做的混音,最后都要经过你那 16.7 毫秒的窗口才能抵达玩家。窗口一挤,前面所有功夫打七折。

这个系列从 01 篇讲到 22 篇,讲的是"怎么把房子搭起来":范式、内存模型、查询、关系、调度、模块化、大规模架构。这一篇讲的是另一件事——别把房子盖在沼泽上。性能不是上线前的一道工序,而是从你决定"这个状态用标记还是用字段"那一刻就开始的决策链。它排在第 23 篇,不是因为它不重要,恰恰因为它太重要了——你得先知道地基长什么样,才听得懂我在说什么。

这篇会给你四层机制、五张能贴到墙上的参数表、一条初级同学今天下午就能跑完的自检路线,以及一堆我自己踩过的坑。我不保证你照着做完就能翻倍,但我保证你能知道自己的帧时间花在哪儿了——这已经比八成团队强。

第 1 层:先学会量——把调优从玄学变成一条流水线

大部分团队的"性能优化"是这样的:某天卡了,抓个人过来看一眼,改改这里改改那里,好像快了点,收工。两周后又卡了,换个人,再来一遍。这不叫优化,这叫跳大神。

先把一件事说清楚:玩家感知的不是平均帧率,是帧时间的分布,尤其是最差的那 1%。你平均 58 帧,但每三秒来一帧 80 毫秒的尖刺,玩家的评价就是"这游戏卡"。反过来平均 55 帧但极其均匀,大多数人会说"挺顺的"。

单帧耗时换算帧率玩家的真实感受
16.7ms60顺滑,动作游戏的及格线
20–25ms40–50能玩,但输入发飘,格挡和闪避开始"不跟手"
33ms30动作游戏明显难受,射击游戏基本判死刑
单帧 50ms 尖刺—哪怕只有一帧,玩家也会说"顿了一下";判定类玩法直接失败
200ms 卡顿—玩家会以为游戏崩了,去按 Home 键

看懂这张表,你就明白为什么"平均帧率"这个指标经常骗人:它把尖刺稀释掉了。你的验收指标应该是第 99 百分位的帧时间,而不是平均值。

微基准为什么会骗你

很多人喜欢把某个系统单独拎出来跑十万次,看它多快。这么测当然快——数据全在 CPU 的高速缓存里,分支全部预测成功,内存访问全是最优路径。可真实场景里,这批实体的数据散落在几百张表、跨好几兆内存,每一次访问都是一次缓存未命中。

我自己的规矩是:微基准只能用来比较两个实现谁更快,绝不用来预测上线帧率。它是天平,不是尺子。

还有个更隐蔽的坑:桌面 CPU 有大缓存和乱序执行帮你兜底,同一个逻辑在 PC 上测快三倍,到了手机上可能反而更慢——因为手机的瓶颈根本不是算力,是内存带宽和散热。你在实验室里得到的每一个结论,都得拿到真机上重新验一遍。

基准测试的四道防线

我把一套能长期用的基准体系拆成四件事,缺一件都会翻车:

  • 可复现:固定场景、固定随机种子、先空跑 60 帧预热再开始计时,锁死机型和频率(笔记本插电、关掉省电模式,手机开性能模式并记录温度)。做不到这条,你后面所有对比都是噪音。
  • 可归因:分层计时。总帧时间 → 各阶段 → 各系统 → 可疑子系统。只测总量等于只知道"人生病了",不知道是哪个器官。
  • 可对比:一次只改一个变量,记录提交版本、机型、场景。我以前同时改了三处,帧率涨了,高兴了三天,后来发现是其中一处拖后腿、另两处救回来的——白高兴。
  • 可回滚:把性能预算写进自动构建,超过阈值就报警甚至拦截。人肉记忆不可靠,制度才可靠。

场景基准才是真基准

合成基准测上限,场景基准测现实。"十万个空实体转一圈"这种测试毫无意义,它测的是你的内存带宽上限,跟你的游戏没关系。有意义的场景是什么?是弹幕游戏里屏幕上同时飞 5000 发子弹、是兽群模拟里 800 只动物同时寻路、是建造游戏里玩家一口气框选 300 栋建筑下指令。基准要长得像你的游戏最难看的那一帧。

两个真实案例,一个共同结论

Unity 的 DOTS 演示项目 Megacity 是这套思路的教科书。它把十万量级的车流、行人、建筑塞进 ECS,并且要在中端安卓机上跑起来。他们做的事,本质上就是本文这四层:先有极其明确的"目标场景 + 目标设备"基准(在真机上跑,不是编辑器里跑),再用时间轴把开销归因到具体系统,然后靠数据布局(连续内存、批处理)和分级更新(远处的东西少算)把预算压回去。这里面有个容易被忽略的细节:他们关心的是"每个数据块装得多满",而不是"实体总数"。因为装得不满,就等于你花了内存带宽去搬运空位。这跟我们后面讲的碎片化是同一件事的两面。代价也很实在——为了这套东西,团队必须全盘接受 DOTS 的写法、烘焙管线和组件设计纪律,用不了传统对象式的那套灵活性。性能从来不是免费的,它是拿开发自由度换的。

另一个是《守望先锋》的服务端。Tim Ford 在 GDC 上那场著名分享里讲过,他们把游戏逻辑重构成 ECS 之后,遍历开销掉了一两个数量级。很多人记住的是"ECS 好快",但真正的原因是:数据从"指针追指针、跳一次算一次"变成了"一整条连续内存顺序扫过去"。加速来自布局,语言和框架只是顺带。他们同时还用了固定大小的回滚缓冲窗口——所有状态变更都在一个有界的帧区间里被确定地处理,不让不可控的即时副作用渗进来。这条设计后来喂给了无数做网络同步的团队,也正好是我们第 3 层要讲的东西。官方给的是量级上的口径,具体倍数别当圣旨,不同机器测出来能差好几倍。

第 2 层:Archetype 碎片化——最贵的一笔隐形税,和怎么把它量出来

Archetype(原型表)这个词听着吓人,说白了就是:组件组合完全相同的实体,住在同一张表里。好处很直白——你要遍历所有"有位置、有速度、没有睡眠"的实体,引擎直接把这几个字段排成连续的几大块内存,一口气扫过去,缓存吃满,快得像坐高铁。

代价也很直白:任何实体只要改了组件组合——加一个、删一个、换一个——它就得从这张表搬到那张表。农贸市场换摊位,东西得自己搬。

抽屉的比喻

你以为你在做收纳:抽屉按"常用"和"不常用"分,两大类,找东西飞快。结果你给每支笔都贴了一个标签(每个状态一个标记),钢笔一个抽屉、圆珠笔一个抽屉、红笔一个抽屉、红壳圆珠笔又是另一个抽屉……最后变成 300 个抽屉,每个抽屉里躺着三支笔。你要找"所有笔",得开 300 次抽屉,每次只拿三支。

更糟的是,玩家一进战斗,这些笔还在抽屉之间来回搬家。这就是碎片化:表多、每表实体少、迁移还频繁。三件套凑齐,性能断崖。

为什么它贵

遍历一张表是有固定开销的:定位表、建立批处理边界、让预取器重新找准方向。实体多的时候,这点固定开销摊薄了,几乎看不见;实体少的时候,固定开销盖过数据本身。极端情况下表比实体还多,你花在"翻抽屉"上的时间比"拿东西"还多。这不是线性退化,是断崖式的。

玩家实际体验到什么

平时 200 个敌人跑得好好的,一波刷 800 个,同时各种增益减益一起上身——就在这一瞬间掉帧。玩家不会说"我的状态标记组合爆炸了",他会说"这游戏优化真差"。而真正的原因,是你在设计组件时把"临时状态"当成了"永久身份"。

量化诊断:三步链路

诊断这件事最怕凭感觉。我用的是一条三步链路,任何人都能在一个下午跑完:

  • 第一步,抓快照。运行目标场景到最激烈的那一帧,统计三个数:表的总数量、每张表里的实体数(看中位数和最少的那个)、这一帧里实体搬了多少次家(也就是组件增删的总次数)。Flecs Explorer 这类工具能直接看到表的分布,不用自己造轮子。
  • 第二步,找祸首。按"贡献了多少张表"排序,你会惊讶于它有多集中——通常就是那么两三个设计决策造成的。常见的元凶:每个技能给一个标记、每个增益给一个标记、每个阵营给一个标记,然后它们在玩家身上自由组合,组合数直接起飞。
  • 第三步,定策略。四种手法,按性价比排序:合并互斥状态、用关系表达归属、预热常用组合、冷热分离。

四种收敛手法,具体到能落地的程度

合并互斥状态。如果"中毒/燃烧/流血"这几种在一定规则下互斥,或者它们本来就可以用一个字段描述伤害类型和层数,那就别做成一堆标记,做成一个带枚举和层数的组件。玩家体验完全一样,表数量少一个数量级。

用关系表达归属。"这颗子弹属于那把枪""这个护盾挂在那个角色身上"——这是关系该干的事,别用"给每个实体挂一个'属于某某'的标记"来解决。关系是稳定的、结构性的;标记组合是爆炸的、临时的。这条线在第 08 篇讲透了,这里只提醒一句:关系本身也会造成表分裂,所以关系要用在结构稳定的归属上,别用在每帧都变的临时关联上。

预热常用组合。在加载关卡的时候,把已知会出现的组件组合先创建好一批实体再销毁,让表提前存在。这样进入战斗时就不需要现场建表,省掉的是运行时的抖动。

冷热分离。把"每帧都要读"的字段(位置、速度、血量)和"三个月才读一次"的字段(掉落表、描述文本、成就标记)拆成不同组件。前者塞进一张紧凑的表里被高频扫,后者躺在角落里没人打扰。这一招在移动端上收益最大,因为它省的是内存带宽。

三个指标和三个误解

表多本身不是罪。五百张表、每张两百个实体,挺健康的。真正危险的是组合:表多、每表实体少、迁移频繁。我自己的经验区间是:每张表平均实体数在 20 以上算舒服,50 以上算健康,低于 5 就要动手了;表数量跟峰值实体数的比值超过五分之一,得警惕;每帧的实体迁移次数超过实体峰值的百分之五,说明你的组件设计在跟你打架。这些数不是教条,是体检指标,跟你体重血压一个性质。

第 3 层:批量操作的时序经济学——什么时候攒着干,什么时候立刻干

上一层讲的是空间上的税,这一层讲时间上的税。先记住一张成本阶梯,这是所有性能直觉的源头:

操作类型相对代价(量级)为什么
读一个组件字段1连续内存里挪一格,最快的情况
写一个组件字段2–3多一次内存写入,可能影响缓存行
给实体加/删一个组件10–30换表、搬数据、打断正在进行的遍历
创建/销毁一个实体20–50换表 + 分配/回收 + 可能建新表

看到没有,结构性变更比读写贵一个半到两个数量级。这就是为什么"我这个系统只写了一行"的直觉经常错得离谱——那一行可能是加组件。

延迟命令是什么,为什么它有用

延迟命令(deferred command)说白了就是"先记账,等下一起结"。你在遍历所有子弹的时候发现 300 颗命中了,别当场销毁它们,先记在本子上;遍历走完,一次性把 300 个销毁掉。好处有三:不在遍历过程中改动容器(安全)、把分散的搬家合并成一次连续的搬家(快)、顺便让顺序变得确定(对网络同步友好)。

代价也真实:内存上要留一块队列空间、语义上有延迟(你这一帧删掉的实体,在这帧剩下的时间里其实还在)、合并本身也有开销。所以我见过不少团队"加了批量之后反而更卡"——他们把一个每帧只有三次增删的小系统也改成了延迟命令,纯属给自己加了一层账本。

玩家能感知到的差别

场景:弹幕射击或者大范围 AOE。你屏幕上同时有 4000 发子弹,其中 800 发在一帧内命中消失。即时销毁的写法,就是这 800 次搬家随机散落在各个系统的执行中间,帧时间被拉成一根刺;攒到帧末批量处理,就是一次整整齐齐的搬家,帧时间是一条平线。玩家的感受差别就一句话:"这爆炸挺带感的"对"这爆炸卡了一下"。同样的画面,两种评价。

再举个反向的例子:碰撞检测发现角色踩到了陷阱,必须立刻把它标记为已触发。如果你把这个标记也攒到帧末,那这一帧剩下的系统都会看到"还没触发"的状态,可能导致二次扣血、音效播两次。这种地方就得立刻生效,一毫秒都不能攒。判断标准不是"能不能攒",而是"攒了之后,还有谁会看到旧状态"。

一段我自己的翻车史

早些年做一个塔防,我给每种增益减益都做了独立的添加系统和移除系统,一个 buff 一个系统,写得那叫一个整齐。结果后期关卡敌人一多,每个敌人身上五六种状态,每帧几万次组件增删,帧时间从 9 毫秒涨到 45 毫秒。我盯着分析器看了半天,还以为是渲染的问题。

后来怎么改的?把所有临时增益塞进一个组件里的定长数组,记录类型、剩余时间、层数;再写一个系统统一推进时间、统一清理到期项。加一次增益,变成了写一个字段。帧时间回到 9 毫秒,而且代码少了一半。那次之后我信了一句话:临时状态不是身份,别用身份的方式去表达它。

批量该给多少预算

我的做法是给结构性变更设一个每帧预算,超了就分帧。经验值:每帧的结构性变更总数控制在实体总量的百分之五以内;单次批量合并的上限设一个数(比如一次最多处理五百个),超出的留到下一帧;延迟队列预留足够的容量,别在峰值时动态扩容——那一下也是尖刺。这些参数怎么调,下面参数表里有。

第 4 层:移动平台的最后一公里——瓶颈换了地方,方法也得换

前三层是通用的。这一层专门说手机,因为这是绝大多数独立团队真正会翻车的地方。

桌面上的性能瓶颈通常在三处:分支预测失败、缓存延迟、单核性能。手机上瓶颈换了:内存带宽、大小核调度、热降频。手机的内存跟 GPU 是共享的,你多搬一个字节,渲染那边就少一点带宽;手机的散热是被动散热,跑满三分钟就开始降频砍你频率。

这带来一个非常阴险的玩家体验:前两分钟满帧,六分钟后掉到 45。这比"一开始就慢"糟糕得多。因为一开始慢,玩家会说"我手机不行";先快后慢,玩家会说"这游戏越玩越卡,做得真烂"。而且场景很具体:夏天户外、边充电边玩、手机壳没摘——温度一上来,全完。

四个具体手段

第一,砍每帧访问的字节数。注意不是砍功能,是砍"每帧要走一遍的数据"。冷热分离在这里的收益比桌面上大得多,因为它省的是带宽。我曾经把角色身上一个两KB的配置数据从高频组件里挪出去,帧时间直接少了 1.8 毫秒——那数据三年没变过,但每帧都被扫了一遍。

第二,数据类型瘦身,但要小心。能用 16 位就别 32 位,但物理相关的量要小心。我踩过一次:为了省内存把坐标精度砍了,结果射弹飞到六十米开外开始抖,玩家截图发论坛说"你们的箭会抽搐"。精度这事儿,位置和时间可以省,旋转和速度最好别乱动。

第三,活跃实体上限加分级更新。远处的东西不需要每帧算。屏幕外三十米的敌人,AI 每 0.3 秒更新一次,中间靠插值表现,玩家根本看不出来。这一招在大量 NPC 的场景里,收益是几倍级的。

第四,分帧。把不紧急的工作摊到不同帧去做:掉落物回收、成就检查、路径重算、音频距离衰减更新。人眼对"每帧做一点点"极其宽容,对"某一帧做一大堆"极其敏感。

案例拆到底:Megacity 在手机上做了什么

还是 Megacity,从移动端角度看它更有意思。十万量级的交通模拟要跑在中端安卓上,单靠"ECS 快"是绝对不够的——它靠的是一整套分级策略:离摄像机远的车辆只更新位置、不跑完整的行驶决策;不可见的区域整块剔除;更新频率按距离分档;内存布局按访问频率组织。

这里有个很值得学的取舍:远处的车偶尔会"顿一下"或者轻微跳变。严格来说这是瑕疵,但玩家永远注意不到,因为注意力在近处。性能优化的本质,是决定哪些不完美可以被观众忽略。这话听着像妥协,其实是设计。

他们还反复强调一件事:必须在真机上测,必须在真实的温度下测。这一点我要加粗强调:模拟器和桌面永远骗你,编辑器里跑出来的数字连参考价值都有限。我做过的每个项目,第一次上真机都能挖出三五个桌面上根本不存在的问题。

真机验收怎么做

我的清单很土,但管用:找一台三年前的中端机(不是旗舰),电充满,壳摘了,跑目标场景连续十五分钟,每三十秒记一次帧时间和电池温度。前五分钟看起步,后十分钟看衰减。曲线应该是平的,如果是一条往下走的斜线,你就有热降频问题,回去做上面那四件事。这十五分钟的测试,能救你上线后一半的差评。

把上面这些变成能贴到墙上的五张表

方法论听完容易忘,数字留下来才有用。下面这几张表建议直接抄进你们项目的性能文档,每次评审都过一遍。

表一:帧时间预算分配(60 帧目标)

预算项PC 建议移动端建议说明
逻辑(含物理、AI)6–9ms4–6ms移动端优先砍这项,靠降频和分帧
渲染提交与剔除4–6ms8–10ms移动端要给 GPU 留够带宽
UI 与动画1–2ms2ms复杂 UI 是最容易被低估的黑洞
余量2–3ms2–3ms没有余量,第一个尖刺就会击穿

表二:碎片化体检表

指标健康警戒危险该做什么
每表平均实体数> 5020–50< 5合并互斥状态、砍临时标记
表数量 / 峰值实体数< 1/201/20 – 1/5> 1/5查组合爆炸源头,按贡献排序
每帧实体迁移次数 / 峰值实体数< 2%2%–5%> 5%临时状态改字段、增删攒到帧末
单帧表数量增长(长跑)平稳缓慢上升持续上涨找泄漏:谁在造新组合,为什么

表三:结构性变更预算

参数建议值超了怎么办
每帧增删组件总次数< 实体峰值的 5%攒到帧末批量执行
单次批量合并上限300–500 个拆成多帧分批处理
延迟队列预留容量峰值需求 × 1.5一开始就分配够,别动态扩容
临时状态存活上限按玩法定,通常 30 秒内做成带到期时间的字段,不用标记

表四:降频与分帧参数

系统近处/可见中距离/屏幕外远处/不可见
AI 决策每帧每 0.2 秒每 0.5 秒
寻路重算变化时立即每 0.5 秒每 2 秒或暂停
动画更新每帧每帧(但降骨骼)降频或冻结
掉落物回收分帧执行分帧执行分帧执行

这张表最关键的用法是:它是跟策划谈的筹码,不是技术细节。你拿着它去说"远处敌人 0.3 秒想一次,玩家看不出来,能省 4 毫秒,我们要不要用在同屏敌人数量上",这才是对话。而不是一句"性能不够了,砍点东西吧"。

表五:真机验收清单

项标准
设备三年前中端机一台,当年旗舰一台
时长连续 15 分钟目标场景
记录频率每 30 秒一次帧时间与电池温度
合格线第 99 百分位帧时间 < 20ms,末段不高于初段 20%
场景覆盖刷怪峰值、大范围特效、UI 弹窗、切场景

两条路:今天下午就能做的五件事,和把它变成团队制度

初级路径:一个下午的自检

如果你是单人开发或者小团队刚开始,别搞什么体系,先做这五件事,大概四小时:

一,造一个能复现的最难场景。不是整个游戏,就是"最难看的那一帧"——刷怪峰值、子弹最多、UI 最满的那三秒。能用固定种子一按就跑最好。

二,抓一次表的分布。用 Flecs Explorer 或者你自己打印,看表数量和每表实体数。如果发现几百张表每张躺三五个实体,恭喜你,找到金矿了。

三,把所有"每帧增删组件"的代码列出来。不用改,先列。你会被这个清单的长度吓到。

四,挑清单里最频繁的三个,改成攒到帧末处理。就改三个,别贪多。改完立刻对比同一个场景的百分之九十九帧时间。

五,拿真机热跑十五分钟。看曲线平不平。

这五步做完,多数项目能拿回 20% 到 40% 的帧时间。剩下的,等你有闲了再说——不是所有项目都值得做完整的性能工程,一个两小时流程的叙事游戏,帧率稳住 60 就够了。

进阶路径:把性能变成一条产品指标

如果你的项目要长期运营、要跨五六种机型、团队超过五个人,那就得制度化,四件事:

性能预算是产品指标,不是技术指标。写进设计文档,跟帧率、内存占用、包体大小并列。新功能上线前要"报预算":这个功能大概吃几毫秒逻辑时间、几个实体、几张表。听起来夸张,但做过两个项目之后你会发现这是最省事的做法——它把返工成本从上线前挪到了写之前。

自动化回归。把目标场景做成可以无人值守跑的任务,每次合并到主干就跑一遍,出图和上周对比。哪条曲线抬头了,谁改的,一目了然。这一条是整套体系里性价比最高的,因为它解决的是"人记不住"的问题。

性能债台账。跟第 22 篇讲的技术债是一个东西。每次为了赶工期妥协了性能(比如"这个状态先用标记顶着"),记一条,写清原因、影响的场景、预估的修复成本。三个月后回头看,你会感谢自己。

灰度验证。上线后收集真实机型的帧时间分布,重点看低端机的尾部数据。实验室测不出的问题,永远藏在真实玩家的设备里。

几个我听过太多次、但大多站不住脚的说法

坑一:"用了 ECS 就自动变快。"这是最流行也最害人的误解。ECS 快的是"批量、连续、同构"的访问模式。如果你的游戏是两百个实体、逻辑里全是分支、系统之间频繁互相查询,ECS 完全可能比一个朴素的对象数组还慢——因为你在为用不上的批处理能力付复杂度成本。选型要看你的实体规模和访问模式,不是看热度。这条线第 25 篇会展开骂,我就不抢戏了。

坑二:把微基准当圣旨。跑分软件里快 30%,上线后慢 10%,这种事我见过不止一次。微基准只能回答"A 和 B 谁更快",回答不了"我的游戏会不会卡"。

坑三:以为多线程是免费午餐。多线程能把物理和 AI 摊到不同核上,但合并结果、同步状态、延迟命令的线程安全边界,全都要你亲手管。四核上收益 1.6 倍、八核上收益 1.7 倍,这种曲线是常态——因为瓶颈最后总是落在那个不能并行的合并步骤上。第 10 篇专门讲这个。

坑四:只看平均值,不看尖刺。你的平均帧率可能是 58,玩家的体感可能是"一直卡"。验收指标必须是尾部分位数,不是平均值。

坑五:把框架当替罪羊。"Flecs 太慢了"这句话,我听过十次,九次半是用法问题——碎片化、每帧增删、查询没缓存、在遍历里改结构。换框架救不了坏的数据布局。

坑六:过早优化和过晚优化,两头都是坑。原型阶段就抠微秒,你会花三个月做出一个没人想玩的完美 Demo;内容全部做完才想起性能,你会面对一个"改一处崩三处"的烂摊子。我的建议很朴素:原型期只守住一条线——别让临时状态变成实体身份;上线前三个月开始做完整的性能工程。中间那段时间,让它先跑起来。

还有个更微妙的争议:性能优化会不会伤害玩法?我的答案是,会——如果你把它当成砍功能的话。但真正的优化从来不砍功能,它砍的是浪费。把"每次状态变化都搬家"改成"一个字段写一下",玩家体验到的东西一点没少,帧数多了一倍。这中间的差别,是"优化"和"阉割"的差别,也是技术总监和"性能杀手"的差别。

FAQ

问:我的游戏峰值才两千个实体,还要做碎片化治理吗?

看你卡不卡。两千个实体如果每帧增删五六百次组件,一样能把你打到 30 帧,因为成本在迁移次数不在实体数。反过来,两万个实体但组合稳定、几乎不增删,那可能一点事没有。所以别用实体数判断,用我上面那三个指标判断:每表平均实体数、表数比例、每帧迁移比例。三个都在健康区,你就安心做玩法。

问:延迟命令越多越好吗?为什么我批量之后反而更卡了?

不是。延迟命令本身就是一层账本,有内存开销和合并开销。每帧只有三五次增删的系统,直接做反而更快。它真正发光的地方是峰值——几百上千次结构性变更挤在一帧里的时候。另外检查两件事:你是不是把需要立刻生效的标记也攒了(导致下一帧重复处理),以及延迟队列是不是在运行时动态扩容(那一下本身就是尖刺)。

问:同一个逻辑,桌面上很快,手机上一卡一卡的,最可能是什么?

按概率排:一,内存带宽,你每帧扫过的数据太多了(冷热没分离);二,热降频,跑几分钟才出现的话基本就是这个;三,单帧尖刺被手机的调度放大了,比如一个 5 毫秒的任务在大小核切换时变成 12 毫秒;四,渲染那边的带宽被逻辑抢了。先看曲线形状:一开始就卡是前三之一,越玩越卡多半是降频。

问:要不要把所有状态标记都改成位标记或者枚举,一劳永逸?

别走极端。判断标准是"这个标记会不会影响实体的组件组合"。如果它只是数据、不影响系统筛选,做成字段完全没问题;如果它需要被查询筛选("所有处于眩晕且有护甲的敌人"),那它就得是组件或者标记——但这样每一组新的组合就是一张新表,你得接受这个代价,并且把组合数量控住。真正的原则是:影响筛选的,尽量少;不影响筛选的,别做成标记。

问:性能预算这套东西,怎么跟策划和美术谈才不吵架?

别用毫秒谈,用"同屏能出现多少个"谈。"这 4 毫秒如果省下来,同屏敌人能从 60 个提到 120 个,你要哪个?"——这个问法策划听得懂,也答得上来。我合作过的最好的策划,自己就会问"这个特效大概吃多少预算"。把性能翻译成玩法单位,是技术岗最重要的沟通技能之一。

最后一个问题留给你:如果明天你的游戏在目标机型上掉到 40 帧,你能在半小时内说清楚是哪三件事造成的吗?如果不能,那说明缺的不是优化技巧,是本文第 1 层那条流水线。搭起来不难,难的是从今天开始就搭。

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