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

Flecs 观察者(Observer)与响应式系统设计

· 0 分钟阅读
Flecs 观察者与响应式系统设计:把「变化」变成一等公民 Observer & Reactive System Design — Flecs ECS 技术专题

Flecs 观察者与响应式系统设计:把「变化」变成一等公民 Observer & Reactive System Design

为什么「变化通知」是响应式的第一块基石

先说个我自己的翻车现场。

几年前做一个俯视角战斗原型,我给生命值挂了一个「值变了就通知我」的回调:飘伤害数字、更新血条、播音效、镜头抖一下,顺手把仇恨表也刷了。代码短到什么程度?以前写一个系统要拆三个文件,那会儿一个回调全办完。我当时还挺得意,跟主程说这架构真香。

三个月后,一波精英怪的自爆炸进人堆。同屏两百多个单位,每只身上挂三四层持续伤害,一帧之内血量被写了三千多次,帧时间从 4 毫秒窜到 22 毫秒。翻性能面板,最贵的那个函数叫「刷新 UI 文本」——被叫了三千次,而屏幕上只有一块血条。

这就是响应式在 ECS 里的双面性。它把「变化」变成一等公民,你不用再问自己「我该在哪个系统里检查这个值变没变」,事件自己会找上门。可一旦你让它接管主流程,那条精心设计的数据导向管线就会退化成一团互相调用的回调网——那玩意儿有个更熟悉的名字,叫面向对象。

所以这篇要聊的不是「Observer 怎么注册」,而是「什么时候该让它开口、什么时候该让它闭嘴」。具体会拆四层:Observer 的同步回调本质、Observer 与 Trigger 的语义分家、用脏标记把「推」降噪成「拉」、以及关系变化那条最容易失控的传播链路。看完你至少能回答一个问题:项目里那些噼里啪啦响的回调,有几个是真该响的。

第一层:Observer 是「此刻的回调」,不是「另一个系统」

先把机制说清楚,不然后面全是空谈。

Flecs 的世界默认是拉模型:你写系统,系统里带查询,每帧扫一遍匹配的实体,一次处理一批。这是数据导向的根,好处是内存连续、可批处理、可并行、顺序可控。Observer 是另一条路——推模型:当某个操作发生的瞬间,Flecs 立刻同步回调你注册的处理器。注意「同步」两个字,它不是排队等下一帧,是在世界正被改动的那一刻就插进去执行。

这个差别不是风格问题,是状态可见性的问题。系统跑的时候,世界是「上一个阶段全部结束」的稳定快照;Observer 跑的时候,世界是半成品——你要加的组件可能刚加了一半,你要删的实体还没真删干净,同批次的其他改动可能还没落地。

落到玩家能感知的层面,你就能明白为什么这个区分要命。拿一次普攻命中举例:

系统流:碰撞系统算出命中,写一个「命中记录」组件;伤害系统在下一个阶段统一结算,扣血、判定死亡、加死亡标记;死亡系统再统一处理掉落和经验。链条长,但每一步都可预测、可断点、可并行,出问题你能一眼定位到是哪个环节。

Observer 流:命中记录一写进去,回调立刻结算伤害;掉血触发了血量回调,血量回调里判定死亡加了死亡标记;死亡标记的回调又去发掉落、发经验、发成就。一秒钟之内四跳走完。你感觉爽,其实你已经把顺序控制权交出去了——这四个环节谁先谁后,取决于你注册的顺序和当时世界的状态。

那是不是就该全用系统?也不是。有些东西晚一帧就出戏:受击音效晚 16 毫秒,玩家立刻觉得「不跟手」;粒子要在命中点炸开;网络包要即时入队。这类反馈的共性是——它对玩家来说是手感,不是结算。

我常用的一句判断标准:玩家能「感觉到延迟」的,用 Observer;玩家只能「看到结果」的,用系统。震屏、拖尾、顿帧、数字跳出来,这是手感;掉多少血、死没死、掉什么装备、算不算成就,这是结算。手感可以稍微乱一点,结算必须一步不错。

顺便说一句横向对比,帮你把「推」和「拉」这两极立住。EnTT 的 observer 是个「变更收集器」:你把它接到注册表上,它把符合条件的加入和移除记在列表里,你在合适的时候自己去遍历这个列表。它不回调你,它记账。这是个很聪明的折中——延迟一帧,但顺序永远在你手里。而 Flecs 选了更激进的那条:立刻回调,性能更好、手感更紧,代价就是上面说的那些半成品状态。

方案触发时机成本模型顺序可控性适合的事件密度典型翻车点
系统 + 查询管线里的固定位置全量扫描,成本随匹配实体数线性增长完全可控高频、大面积变化扫了一堆没变的实体,白烧 CPU
Observer 直连操作发生的那一瞬间每次触发一次回调,成本随「变化次数」增长基本不可控低频、稀疏事件事件风暴、递归触发、读到半更新状态
Observer 打标记 + 系统批量处理变化时盖章,帧末统一处理盖章极便宜,处理成本可预算完全可控中高频、需要合并忘记清标记,脏数据累积

第二层:Observer 与 Trigger 的语义分家——「状态」不是「事件」

如果你翻过几年前的 Flecs 教程,会看到两套写法:Trigger 和 Observer。别慌,你现在只需要记 Observer 一条路。历史上的分工是:Trigger 是底层的事件匹配器,负责「当这个事件配上这个组件时通知我」;Observer 是它的高阶封装,能一次监听多种事件、能拿到组件值、能配通配符。

从 3.x 开始官方把 Trigger 标记为弃用,4.x 直接把它收掉,能力合进 Observer。这个演进本身就是个信号:框架在帮你减少概念数量。我们这行有个迷信,觉得概念越多越专业,其实正相反——每多一个概念,团队里就多一批人会写错。

但 Trigger 留下的那点语义差异值得懂,因为它对应两种完全不同的脑内模型:

事件订阅模型(Trigger 的思路):我订一个事件,事件来了我处理,处理完世界继续跑。适合表达「回合结束」「关卡加载完成」「技能释放」「网络包收到」这种瞬时发生、过期不候的东西。

条件监控模型(更早期还有过 Monitor 那一层):当「满足某条件的实体超过 N 个」时通知我。适合表达「警戒值满了」「连杀提示」「资源见底」。这类需求本质上是「持续成立的状态」,不是「一瞬间的事件」。

Flecs 最后用 Observer 统一了这些,代价是条件判断要你自己在回调里写,好处是你脑子里只剩一个开关。

接下来是这一层真正的重点,也是我这几年见人踩得最多的坑——把状态当事件用。

看一个能救命的故事。我自己给 Boss 做了个阶段标记:进二阶段就加一个「狂暴」标签,标签的回调里播变身动画、换 BGM、拉一次全场冲击波。上线测试,Boss 被打回一阶段再进二阶段,第二次变身没播。为什么?因为标签的「加」是幂等的——同一个标签已经在身上,再加一次不产生结构变化,回调根本不响。

于是有人(我)开始自作聪明:进阶段前先 remove 再 add。这回响是响了,但两次操作会被合并进同一个批次,某些情况下你又看不到你想要的那一次。最后老老实实改成一个「阶段变更」组件,里面塞个序号,每次进阶段序号加一——这种「值更新」的语义天然每次都会响,问题解决。

把这件事抽象成一句话就是:标签是状态,不是事件。你把状态当事件用,迟早要还。

事件类型语义触发次数正确的表达对象典型误用
OnAdd结构变化:这个东西被挂上来了只响一次,重复挂不响进入某个状态:中毒、眩晕、死亡标记拿它当「每次都通知我」的事件
OnSet值写入:这个数据被改了每次写都响,值不变也响属性变化、阶段切换、带序号的信号每帧无脑写一遍,制造事件风暴
OnRemove结构变化:这个东西被摘掉了只响一次状态退出:解控、buff 到期、清理引用在回调里访问已被移走的目标
自定义事件业务语义:这件事发生了你发几次响几次回合结束、伤害结算完毕、技能命中什么都用自定义事件,绕过组件模型
删除 / 删除目标实体或关系目标被销毁一次弱引用清理、UI 卡片销毁在这做重活,拖慢销毁流程

这里插一个我很佩服的公开案例:《守望先锋》的自研 ECS。他们在 GDC 那场讲架构和网络同步的分享里,把组件明确分成两类——普通组件,和一种叫「状态(state)」的特殊组件。状态是什么?就是「变了之后外面必须知道」的那些数据,因为网络同步要靠它做差异比对。而普通组件改了就改了,没人需要被通知。

这个分类的狠的地方在于:状态是被严格限制、且基本不允许随意增删的(要保证网络层能预测内存布局)。说白了,他们在设计层面上做了一次「事件预算」——全游戏只有很小一部分数据有资格触发通知,剩下的全是哑巴数据。你想想,如果他们把每个组件都做成会喊话的状态,那套 60Hz 的回滚同步早就崩了,带宽和确定性两项都撑不住。

玩家那边感受到的是什么?是老生常谈的「跟手」。但「跟手」不是玄学,它是分层的产物:必须立刻反馈的(命中顿帧、技能前摇、准星反馈)走高频通道,可以晚一点结算的(伤害数字、击杀播报、成就)走合并通道。这个分层你完全可以照抄进 Flecs——先问自己「这个变化,外部真的有人关心吗」,再决定它配不配拥有一个 Observer。

第三层:脏标记——把「推」降噪成「拉」的重构示范

这一层讲实操。场景选最常见也最容易翻车的:UI 面板。

角色身上 20 个属性,一个 AOE 让 200 个怪掉血,每个怪的血量变化都直连 UI。理论上屏幕刷新率 60,一帧能画 60 次;实际上一帧能叫醒 UI 系统 4000 次,而屏幕上只有一块血条、一个数字。这就是我开头那个 22 毫秒的来源。

重构分三步,我个人管它叫「盖章—收网—擦桌子」:

盖章:Observer 里只干一件事——给需要刷新的东西打个脏标记。可以是一个空标签,也可以是个带版本号的组件(版本号的好处是能顺带解决「刷新过程中又变了」的问题)。回调里不要碰 UI、不要碰文本、不要做任何有成本的事。

收网:一个专门的刷新系统放在管线末尾,扫一遍带脏标记的实体,去重、合并,一次性把结果推给 UI。

擦桌子:刷完清标记,否则下一帧你还在刷上一帧的东西。

这三步换来的不只是性能。更值钱的是顺序可控——所有 UI 更新发生在同一时刻,玩家永远不会看到「血条已经变了但数字还没变」的中间态。做过血条动画的人都懂,那种中间态比卡顿更让人出戏。

那到底哪些东西该立刻响、哪些该合并、哪些可以拖?我给团队定的三层分级,用了三年还在用:

级别典型对象延迟容忍实现方式为什么
L0 立即音效、粒子、震屏、网络包入队、顿帧一帧都不能等Observer 里直接干,但只干这一件事人眼对人耳对动作的延迟感知在十几毫秒量级,晚一帧就「不跟手」
L1 帧末合并血条、数字、小地图图标、仇恨表、任务进度100 毫秒以内都无感打脏标记,帧末统一刷合并后成本从 N 次降到 1 次,N 往往是几百上千
L2 惰性成就检查、自动存档、寻路重算、光照更新秒级都行进队列,按预算每帧做几个这些活单次成本高,集中做会撞帧,摊开做才平滑

再说一个反方向的案例,帮你把这套思路钉死:《城市:天际线 2》。这款游戏跑在 Unity DOTS 上,几十万市民和车辆在 ECS 里跑模拟。DOTS 里压根没有 Flecs 这种回调式观察者,它的变更检测是「版本号 + 过滤器」——系统遍历时,只挑本帧变过的实体处理。

这就是纯粹的拉模型。好处太明显了:批量、可并行、没有回调栈、没有半更新状态;代价是延迟至少一帧,而且你必须在数据里留版本号这些「记账」字段。

为什么一个几十万实体规模的模拟必须这么干?你想想如果每个市民的心情变化都要通知 UI——几十万条通知涌向一个只显示当前选中市民的面板。荒谬吧?所以他们的做法天然就是「UI 想看的时候自己去取快照」,模拟层根本不知道 UI 的存在。

我对这件事的判断很明确:Flecs 给了你推的能力,但你的默认选项应该是拉。推是给那些「晚一帧就出戏」的东西准备的,是稀缺资源,不是基础设施。如果你的项目里 Observer 的触发量已经超过了系统处理量的一成,说句不好听的,你已经在写事件驱动的 OOP 了,只是恰好用了 ECS 的容器。

第四层:关系变化的传播链路——ChildOf 与 IsA 的级联地狱

关系是 Flecs 最有辨识度的东西(这块 [08] 篇拆过底层),而它跟观察者一碰,就是本篇最容易失控的地方。

先看看玩家能感知到什么。角色骑马,马跑人跟着跑;换武器,武器出现在手上对应的挂点;给角色换个阵营,全城的守卫态度瞬间翻转;给 Boss 挂个「精英模板」,它所有属性按模板放大——最后这个是 IsA,模板继承。

这些体验在数据层是什么样?本质上都是「一条关系边被加上了或者被删掉了」。ChildOf 加一条边,意味着这个实体的世界坐标要跟着父节点重算,而且它的所有子孙都要重算。IsA 加一条边,意味着继承链上的属性要重新合并一遍。

问题就在这:这些操作天然会触发结构变化事件,而结构变化的传播是乘法的,不是加法。

两个具体的坑,我都亲身踩过:

坑一,通配关系的乘法效应。一个父节点下面挂 5000 个子实体,你用一个监听「任意父节点」的通配观察者去收挂载事件。你以为是「一次层级变化」,实际是 5000 次回调,每次都要算一遍匹配。批量创建 UI 列表的时候这个数还会更难看。

坑二,级联深度。你在回调里顺手递归更新子节点的变换,一条 20 层深的骨骼链就是 20 层回调嵌套。栈深还是小事,要命的是顺序取决于插入顺序——同一帧里你先挂了 A 后挂了 B,A 的子节点可能用着上一帧的父节点坐标。这种 bug 表现为「偶尔抖一下」,最难查。

正确姿势跟第三层一样,只是把对象从 UI 换成变换和属性:关系变化只盖章,传播交给固定顺序的系统。挂上父节点?打一个「层级脏」标记。摘掉父节点?打一个「层级脏」标记。然后一个变换系统在渲染之前,自顶向下跑一次,把整棵子树算完。IsA 同理:属性脏标记 + 一个属性重算系统。

我画给你看这条链路长什么样。玩家换上一把 +10 力量的剑:剑的模板关系被加上(第一跳,Observer)→ 角色属性打脏(第二跳,盖章)→ 属性系统在帧末重算一次并发表(第三跳,系统)→ 面板打脏(第四跳,盖章)→ UI 系统刷新(第五跳,系统)。五跳里只有第一跳是推,剩下四跳都是拉。

这个模式我叫它「短推长拉」:用一次推,把变化送进拉的世界,然后让拉的世界按自己的节奏跑完。反过来写成「一路推到底」,这条链路就会变成一张网——每个节点都认识别的节点,加一个新系统要改五个回调,三个月后没人敢动。

关系变化需要传播什么传播方式建议上限超限降级
ChildOf 挂载世界变换、渲染排序、可见性打层级脏标记,帧末自顶向下重算单帧重算子树上限几千节点量级超出则拆帧,一帧算一层
ChildOf 摘除同上 + 引用清理打标记,不要递归清理同上进延迟销毁队列
IsA 模板切换属性合并、技能表刷新打属性脏标记,帧末重算一次单帧重算实体数按预算排队队列化,按重要性排序
阵营 / 好感等自定义关系AI 态度表、UI 提示打标记;只重算「与目标有关」的子集别全量重扫分帧 + 空间分区

呼应一下渲染那篇([12])的结论:变换层级千万别用递归回调去推,那等于把 O(节点数) 的活干成了 O(节点数 × 深度),还顺带把顺序搞乱。这块我吃过亏,别学我。

参数化设计工具:几张能直接贴进设计文档的表

下面这些数字都是经验刻度,不是物理定律。你拿去当起点,然后自己测。我最怕看到有人把别人的经验值当圣旨——包括我的。

表 A:事件方式选型表(按事件密度)

这里的「事件密度」指「每帧发生变化的实体数 ÷ 该组件涉及的总实体数」。这个比值决定了你的成本曲线长什么样。

密度典型场景推荐方案理由
< 0.5%死亡、升级、成就解锁、关卡事件Observer 直连,随便用稀疏,回调开销淹没在噪声里
0.5% ~ 5%受击、拾取、状态异常Observer 只打标记,帧末批量处理回调不算贵,但处理逻辑贵,必须合并
5% ~ 30%每帧移动的单位、附近的敌人别用 Observer,写系统扫回调的固定开销开始反超批量遍历
> 30%全体单位每帧的坐标、动画时间系统 + 批量操作,禁止逐实体回调你在做的事本质就是一遍全量扫描,别绕路

表 B:三层响应分级的参数建议

参数L0 立即L1 帧末合并L2 惰性队列
延迟容忍0 帧1 帧(约 16ms),最多 100ms 内可接受几百毫秒到几秒
合并窗口不合并同帧内多次变化合并为一次同队列内按优先级排序
每帧预算单次回调建议控制在几微秒量级整个刷新阶段建议不超过总帧预算的 10%~15%每帧固定做 N 个,N 可配
失败姿势宁可少做一些,不可做重活超预算则丢标记、下帧再刷超预算则继续排队,但要报警

表 C:观察者上线前自查清单

问题不合格的样子改法
这个回调里有几个副作用?播动画 + 改数据 + 刷 UI + 发网络包拆成多个观察者,每个只干一件事
它会不会触发别的观察者?链式触发,无终止条件加深度计数器,开发期超过 4 层直接打日志报警
有没有用标签当事件?靠加同一个标签来反复触发换成带序号的组件 + 值更新语义
会不会每帧无脑写值?每帧把血量写成它原本的值写之前比一下,或者用版本号门控
有没有挂通配关系观察者?「任意父节点」全场合监听限定到具体父实体,或者只监听关键方向
能不能被一键关掉?没开关,出问题只能改代码给观察者加启停开关,线上可紧急关闭
每帧触发多少次?没人知道回调里埋计数器,按来源统计 Top10
它在 defer 里吗?以为立刻响,其实延后了搞清批量操作的边界,需要立刻响的别塞进批里

双路径:五条铁律,和五个「什么时候该反悔」

初级路径:先记住五条,够你撑过第一个项目

一,手感用推,结算用拉。玩家能感觉到延迟的走观察者,只有结果重要的走系统。拿不准就归到「结算」,因为结算写错了是 bug,反馈慢了只是不够爽。

二,一个观察者只干一件事。看到回调里有三个以上副作用,立刻拆。这条比任何性能优化都管用。

三,进入状态用 OnAdd,数值更新用 OnSet,退出状态用 OnRemove。这三句话能解决九成的「怎么选」式提问。

四,关系变化只盖章,绝不递归。挂父节点、换模板,都只打脏标记,让帧末的传播系统去干脏活。

五,上线前埋一个计数器。按事件来源统计每帧触发次数,跑一次压力场景看 Top10。你会惊讶于那个第一名是谁——我的记录保持者是「鼠标悬停高亮」,一帧能响两百多次。

进阶路径:五个「什么时候该反悔」

一,当你的观察者开始互相认识了。如果 A 观察者的回调里在等 B 的结果,说明这条链应该合成一个系统。系统天生有顺序,回调没有。

二,当你查一个 bug 要打八个断点。这是可读性崩溃的信号。ECS 有个很真实的批评是「代码跳来跳去」(这事儿 [25] 篇会聊),观察者就是那把最快的刀。

三,当 defer 边界开始咬人。批量操作为了性能会把改动攒起来,观察者的事件也一起攒着,等批量结束才一块儿放出来。这时候「我明明刚加了它怎么没反应」会成为团队日常。要么别在批里期待立即反馈,要么把这条链改成系统。

四,当多线程把顺序打乱。工作线程上发的事件在哪个线程回调、能不能碰主线程的东西,这些边界在 [10] 篇里讲透了,但结论要在这里重复一次:观察者 + 并行处理 = 你自己给自己造的竞态。

五,当你想用它做「事件总线」。这是我见过最普遍的死法。用 ECS 的观察者去搭一套全局消息系统,谁都能发谁都能收,三个月后没有任何人能说出「这一帧发生了什么」。说句实话,你要真需要总线,外面挂一个正经的消息队列,别寄生在 ECS 里。

争议与常见坑:那把最顺手的刀,最容易割到自己

「观察者让 ECS 变回 OOP」这个批评,一半是对的。它本身不是坏事——推模型解决的是拉模型解决不了的问题。坏事是拿它当主循环。给你一条量化红线:如果观察者的触发量超过系统处理量的 10%,你已经不在写数据导向的代码了,你只是恰好用了 ECS 的容器。

坑一:值没变也响。OnSet 的语义是「被写了」,不是「变了」。每帧无脑把当前血量写回去,等于每帧给自己发一次事件。改法很土但很有效:回调里比一下新值旧值,或者用一个版本号门控写入。

坑二:递归触发。A 的回调改 B,B 的回调改 A。Flecs 不会帮你检测环,它只会忠实地一直调下去,直到栈炸或者你发现帧率不对。开发期加个深度计数器,超过四层就报警——这个便宜到不行的保险,救过我两次。

坑三:defer 期间的时序幻觉。批量操作里,事件会被攒到批量结束才放出来。你以为「加完就响」,实际上是「这批全加完才响」。这不叫 bug,叫设计,但你的直觉会认为它是 bug。

坑四:通配关系观察者的乘法。批量为 5000 个子实体挂父节点,回调不是一次,是 5000 次。而且你通常是在加载关卡的时候干这事,正好撞上「加载慢」这个最容易被骂的点。

坑五:半更新状态下读数据。OnRemove 的回调里,实体还在,但别指望它还在查询结果里;OnAdd 的回调里,同批次要加的其他组件可能还没落地。这条我在项目里立了规矩:回调里只信你自己收到的那个东西,别东张西望。

坑六:销毁时机的直觉错误。你在回调里销毁一个实体,它的清理又触发了别的观察者,那个观察者又去访问已经处于销毁流程中的目标。这类问题往往表现为「偶发崩溃,无法复现」,能查到你怀疑人生。

最后说个反直觉的观察:越强大的 ECS 框架,越容易写出不像 ECS 的代码。观察者就是最典型的那把刀——它太好用了,好用到你会忘了自己为什么选 ECS。

FAQ:几个真问题

问:观察者能替代系统吗?我能不能整个游戏都用观察者写?

技术上能跑,工程上不建议。判断标准很朴素:这个逻辑是「对所有匹配实体做同一件事」,还是「某个特定变化发生后做一件事」?前者写系统,后者写观察者。全用观察者的项目,性能通常比全用系统的差,因为回调是逐实体的,批量遍历是连续内存一次扫完——这是 [06] 篇讲的架构决定的,不是风格偏好。

问:OnAdd 和 OnSet 到底怎么选?

一句话口诀:进入状态用 OnAdd,数值更新用 OnSet,退出用 OnRemove。再加一条判断:如果这个变化发生两次你需要被通知两次,那就不该用 OnAdd。因为结构变化是幂等的,第二次加同一个东西不产生变化,回调不响。这条规则救过无数「Boss 二阶段不播动画」的 bug。

问:我在观察者回调里加了组件,为什么下一个观察者没立刻触发?

八成是 defer 边界。批量操作(或者调度器内部的延迟模式)会把改动攒起来,事件也跟着攒,等批量结束才一起放。想验证很简单:把这段代码从批量操作里挪出来单独跑,如果立刻就响了,那就不是观察者的问题,是时序的问题。

问:我的观察者就是不触发,常见原因有哪些?

按出现频率排:一,事件类型选错了,用 OnAdd 去等值更新;二,你以为的「变化」其实是幂等操作,压根没产生结构变化;三,通配写法写窄了或写宽了,实际匹配不到你以为的那些实体;四,它被禁用了(这个最容易被忘,尤其你在做热重载或者调试开关的时候);五,发生在 defer 里,你测的时候还没到放出来的时机。排查顺序就按这个来,能省你半天。

问:多线程下观察者安全吗?

「安全」这个词在这儿太含糊。准确的说法是:你得清楚每个观察者会在哪个线程回调,以及回调里碰的数据归谁。工作线程上发出的变化,可能就在工作线程上回调你,你在里面去改主线程的 UI 状态,那就是自己给自己埋雷。稳妥的做法是把跨线程的观察者砍到只剩「收集数据、打标记」两件事,所有真正干活的处理统一回到主循环做。这块的边界 [10] 篇写得很细,建议对着看。

问:Observer 写多了以后,怎么定位是哪个在拖性能?

别上重型工具,先在每个回调里埋一个计数器,按「来源 + 事件类型」分类。跑一遍最重的战斗场景,把 Top10 打出来。九成情况下问题出在两三个高频回调上,而且通常都是「每帧无脑写值」和「UI 直连」这两种。修完再看下一轮,比一上来就上完整剖析链路快得多(工具链的事另开一篇讲)。

最后留个问题给你,不用现在回答:如果你把项目里所有观察者全部删掉、只留系统,游戏会坏几处?如果只坏三处,恭喜你,那三处就是真正需要「推」的地方,剩下的都是你在用事件给自己找安全感。如果坏了两百处——那你其实写的不是 ECS,是个穿着 ECS 外套的消息中间件,这也没问题,但别骗自己说这是数据导向。

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