Verse Wiki — 写给蓝图作者的 Verse 手册
技巧 · EXTRA

把一个 Actor 蓝图拆成 components 的思维练习

道理都懂,真拆的时候还是会卡住:这一坨到底算一张卡还是三张卡?这一页拿一个人人都写过的敌人蓝图开刀,四步拆完,顺便说清三条原则,以及「拆过头」和「拆不够」各自长什么样。这是纸上练习——目的是先把手感练出来,代码是第 27 课的事。

一、今天的病人:BP_PatrolEnemy

规格很标准,你八成写过一个几乎一样的:

在 Actor 时代,这些全在一个蓝图里,大概 400 个节点、14 个变量,还继承自某个 BP_CharacterBase。现在把它拆开。

二、四步拆解

第一步:把蓝图翻译成一串动词。不要想「组件」,只问「它在做哪些事」。一句一件,能拆多细拆多细——这一步宁滥勿缺:

显示模型 / 播放动画 / 沿路径移动 / 检测玩家距离 / 切换巡逻与追击 / 存血量 / 扣血 / 判定死亡 / 播死亡特效 / 播死亡音效 / 生成掉落物 / 显示血条 / 计时重生。

十三个动词。注意其中没有一个叫「敌人」——「敌人」是名词,名词属于 entity 和 prefab,不属于 component。

第二步:按「谁会跟谁一起变」归堆。这是整个练习里唯一需要动脑的一步。判据不是「功能像不像」,而是改需求的时候,这几件事会不会一起改

第三步:决定谁在本体、谁下沉到子 entity。规则只有两条,课内都讲过:需要单独运动的部件给自己一个 entity;同一 entity 上同类型 component 只能有一个。所以:武器要能单独挥动 → 子 entity;血条要浮在头顶、跟着但不跟着转 → 子 entity;死亡特效和惨叫都是「一个粒子 + 一个声音」,可以留在本体,但如果你还要一个受击特效,那就是两个粒子系统——同类型只能一个,得再开一个子 entity。

第四步:落成清单。拆完长这样:

P_PatrolEnemy 拆解结果
P_PatrolEnemy                  entity(prefab 根)
   · 网格 component             身体模型
   · 粒子系统 component         死亡特效
   · 声音 component             死亡惨叫
   · health_component           ← 自定义:血量、扣血、死亡事件
   · patrol_ai_component        ← 自定义:巡逻点、追击、移动
   · proximity_component        ← 自定义:进入/离开半径 → 发事件
   · loot_table_component       ← 自定义:死亡事件 → 生成掉落
   · respawn_component          ← 自定义:死亡事件 → 计时 → 复位
   │
   ├─ HealthBar                 entity(子实体:头顶血条)
   │  · 血条 UI component        订阅 health_component 的变更
   │
   └─ WeaponSocket              entity(子实体:手持武器)
      · 网格 component           武器模型

五张自定义卡,加上四五个由资产直接提供的现成 component。对照一下原来那个 400 节点的蓝图:同样的功能,现在每一块的边界都写在面板上。而且——health_componentloot_table_componentrespawn_component 这三张卡,装到宝箱上一样能用。这就是拆的全部回报。

三、拆的三条原则

原则一:单一职责——一张卡只有一个「改它的理由」。判断法很土但很准:念一遍「我什么时候会改这张卡?」如果答案里有「或者」,就该拆了。「我在调数值平衡的时候会改它,或者在美术换特效的时候会改它」——两个不相干的人为了两件不相干的事改同一张卡,那就是两张卡。

原则二:可复用——问「它能不能装到宝箱上?」这是本页最好用的一句话。把每张卡拿起来,想象把它装到一个完全无关的物件上(宝箱、平台、一扇门)。health_component 装宝箱上?可以,宝箱能被打碎。proximity_component 装门上?可以,自动门。patrol_ai_component 装宝箱上?不太行——但那没关系,不是每张卡都必须通用,这个问题的作用是帮你发现「其实可以通用、只是被你写死在敌人身上」的那部分。

原则三:数据与行为分离——谁存状态,谁读状态。血条那张卡不存血量,它 health_component 的血量;掉落卡不存「我死了没」,它订阅死亡事件。让状态只有一个主人,其它卡一律做订阅者。这条做到位,你会发现依赖关系自然变成了一棵单向的树,而不是一张互相拽的网。

顺带说,官方最佳实践里有一条正好落在这里:在开始模拟时把需要的子组件找一次、存下引用或订阅它们的事件,之后靠事件反应,不要在游戏运行中反复到层级里搜。这既是性能建议,也是架构建议——它逼你在启动时就把关系网接好,而不是运行时到处找人。

四、拆过头与拆不够

两边都是坑,而且长得很不一样。

症状后果解法
拆不够 出现一张叫 enemy_component 的卡,里面 300 行,什么都干 你只是把上帝类换了个文件名。组合的好处一个都没拿到,还多了一层间接 念「我什么时候会改它」,凡是答案里有「或者」就切一刀
拆过头 血量、最大血量、扣血、死亡判定被拆成四张卡;或者出现只有 3 行的 set_visible_component 40 张卡互相订阅,谁触发谁全靠猜。调一个 bug 要在五个文件之间跳 把「永远一起改」的几张卡合回去

拆过头有个很好识别的信号:两张卡之间开始出现「必须按顺序初始化」的隐式约定。如果 A 卡必须在 B 卡之后才能工作,而这件事没有写在任何地方——它们大概本来就该是一张卡。

拆不够也有个信号:你在一张卡的 @editable 属性里看到了两组毫不相干的参数。「巡逻速度、巡逻点数量」和「掉落金币数、掉落概率」出现在同一张卡的面板上,说明这张卡在服务两个不同的人。

最后给一个不太浪漫但很有效的收尾:先拆少,不够再拆。把一张卡切成两张,是十分钟的工作;把散在五张卡里的逻辑合回去,通常要半天,还容易漏。第一版按「三到六张自定义卡」的规模起步,等到你第二次为了同一个需求同时改两张卡时,再决定合并;等到你第二次想把一张卡的一半功能装到别的东西上时,再决定切分。

五、随手自查清单

六、小测验

拆敌人时,你写了一张 health_component,里面同时管:血量数值、扣血、死亡判定、播放死亡特效、生成掉落物。按本页的原则,该怎么改?

来源

本页引用的框架规则(entity 与 component 的关系、同类型 component 数量限制、开始模拟时预存引用的最佳实践)来自 Epic 官方文档:Getting Started in Scene Graph in Fortnite(官方文档)↗ · Scene Graph Best Practices in Fortnite(官方文档)↗ · Components in Unreal Editor for Fortnite(官方文档)↗

清单里的 health_componentpatrol_ai_component都是本页为练习虚构的自定义组件名,不是官方 API;真实的内置组件类型与命名以官方文档为准。拆分原则属于通用软件设计经验,非 Epic 官方表述。