技巧 · EXTRA
迁移检查清单:开工前 8 问、翻译中 10 查、翻完后 5 验
一张可打印的清单。打开蓝图之前先问自己 8 个问题,翻译过程中逐项核对 10 条,翻完之后按 5 步验收——把「凭记性」换成「照着划勾」。
进入拓展 →前面 28 课,每一课都在讲「蓝图里的这个东西,在 Verse 里叫什么」。这一课把它们全部串起来:给你一张真实的蓝图,变量面板、事件图、Delay、Actor 引用一样不少,然后逐节点翻译成一份能编译的 Verse 代码。翻完你会拿到三样东西——一套可复用的翻译顺序、一份最容易翻错的清单,以及一个诚实的结论:有些东西根本翻译不过去。
翻译得先有原文。下面这张图叫 BP_PressurePlate,是那种每张地图里都能找到的小机关:踩上去,门开;过几秒,门自己关上。它的父类是 Actor,资产是一个 .uasset 二进制文件,双击打开能看到两块内容——左下角的变量面板,和中间那张事件图表。
变量面板一共三行。「小眼睛」那一列就是 Instance Editable:点亮之后,这个变量会出现在关卡里每个实例的 Details 面板上,策划不碰蓝图也能改。
| 变量名 | 类型 | Instance Editable(小眼睛) | 默认值 |
|---|---|---|---|
IsOpen |
Boolean | 关闭 | false |
OpenDuration |
Float | 点亮 | 5.0 |
TargetDoor |
Actor 引用(BP_Door) | 点亮 | None |
事件图表只有一条执行线,七格节点,从左到右串成一串:
| 序 | 节点 | 这一格在干什么 |
|---|---|---|
| ① | Event ActorBeginOverlap | 有东西踩上压力板,执行线从这里出发 |
| ② | Branch | 条件引脚接了一个 NOT:IsOpen 为 false 才走 True;门已经开着就什么都不做 |
| ③ | Set IsOpen | 把状态记成 true |
| ④ | TargetDoor → Open() | 对着 Actor 引用调用那扇门的开门函数 |
| ⑤ | Delay | Duration 引脚接 OpenDuration,左上角带小钟表的 Latent 节点 |
| ⑥ | TargetDoor → Close() | 关门 |
| ⑦ | Set IsOpen | 把状态记回 false,机关复位 |
连起来读一遍:踩上去 → 判断门是不是关着 → 记状态 → 开门 → 等 OpenDuration 秒 → 关门 → 状态复位。你在脑子里应该已经能看见那张图了——七个方块、一条白线、右上角挂着一个 Delay 的小钟表。接下来的六步,就是把这张图一块一块搬进 .verse 文件。
翻译前先立一条纪律:第一遍要忠实,不要顺手优化。你现在已经知道 Verse 里有 race、有 option、有更漂亮的写法,但一边翻译一边重新设计,出了 bug 你分不清是翻错了还是改错了。先让译本跑出和原图一模一样的行为,再谈重构——什么时候该重构、重构成什么样,留给本课的拔高拓展页。
每一步都是同一个套路:原图的这一块 → Verse 的这几行 → 为什么这样对应。顺序是有讲究的——先骨架、再数据、再引用、最后才是执行线,和你在蓝图里新建一个类的顺序完全一致。
原图的这一块:资产 BP_PressurePlate 本身。Class Settings 里 Parent Class 那一栏写着 Actor。
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
pressure_plate_door := class(creative_device):
为什么这样对应:蓝图里「右键新建 Blueprint Class → 选父类 → 起名字」这三步操作,在 Verse 里合成了一行 子类名 := class(父类名):。三行 using 是蓝图里没有的东西——蓝图的节点面板里什么都能搜到,Verse 得先声明你要用哪些模块的东西(第 7 课)。命名也换了规矩:资产名的 BP_ 前缀和 PascalCase,在 Verse 里变成 snake_case 的 pressure_plate_door(第 4 课)。
父类为什么不是 Actor?因为今天能真正跑 Verse 的地方是 UEFN,那里一段「能被拖进关卡、有 Event BeginPlay 入口」的 Verse 代码,就是 creative_device 的子类(第 6 课)。UE6 把 UE5 与 UEFN 合并之后,Scene Graph 的 entity + component 会是另一套骨架——第 26、27 课讲过,x2 拓展页会把这台设备按那套模型再拆一次。
原图的这一块:变量面板的前两行,IsOpen 和 OpenDuration。
# 原图:OpenDuration,Float,已勾 Instance Editable
@editable
OpenDuration:float = 5.0
# 原图:IsOpen,Boolean,未勾 Instance Editable
var IsOpen:logic = false
为什么这样对应:蓝图里变量只有一种,「会不会被改」不需要提前声明;Verse 反过来,默认不可变,只有会被 Set 的才写 var(第 8 课)。判断方法很机械,不用动脑:在原图里对这个变量按一下「Find References」,接过 Set 节点的就写 var,没接过的就不写。IsOpen 在③⑦两格被 Set 过,所以是 var;OpenDuration 全图只被读,所以不写。
那只小眼睛就是 @editable(第 22 课)。类型对照表也很直接:Boolean → logic、Float → float、Integer → int、String → string(第 9、10、11 课)。注意 Verse 的字段必须给默认值,这一点和蓝图变量面板的 Default Value 那一栏是对得上的。
原图的这一块:变量面板第三行 TargetDoor,类型是 BP_Door 的 Actor 引用,默认 None,放进关卡后要用 Details 面板上那个吸管指向场景里那扇门。另外还有原图没写在变量面板里的一样东西:压力板本体那个碰撞盒——①号节点 Event ActorBeginOverlap 就挂在它身上。
# 原图的碰撞盒:触发源改由关卡里的一台设备承担
@editable
Plate:button_device = button_device{}
# 原图变量面板:TargetDoor,BP_Door 的 Actor 引用
@editable
TargetDoor:barrier_device = barrier_device{}
为什么这样对应:这是整张图最需要动脑的一格。蓝图的 Actor 引用默认值是 None,忘了绑,运行时就是那句人见人烦的 Accessed None;Verse 的 @editable 设备引用必须给一个默认值(barrier_device{} 是个空壳占位),所以它在编译期永远不为空——忘了绑的表现变成「按了没反应」,难查一点,但不会崩。真正表达「可能没有」的类型在 Verse 里叫 option(第 18 课),这是这门语言对 Accessed None 的正面回答。
还有一处必须诚实交代:原图那个「踩上去」的碰撞盒,没有一对一的译文。Verse 里没有「我这个 Actor 被谁 overlap 了」这种事件可以订阅,触发源得是关卡里一台真实存在的设备,由它广播事件。所以这一步把碰撞盒外置成了一个设备插槽。本课用已核实的 button_device.InteractedWithEvent 做演示;如果你用触发器 / 区域一类的设备,事件名请对照官方 Verse API Reference 核实一遍再写。
原图的这一块:①号节点 Event ActorBeginOverlap。
OnBegin<override>()<suspends>:void =
Print("压力板译本上线,等待触发。")
spawn{ PlateLoop() }
为什么这样对应:蓝图的事件节点是引擎替你挂好的——你把 Event ActorBeginOverlap 拖进图,连上线,登记这件事就已经完成了,你甚至没意识到有「登记」这一步。Verse 没有这个自动挂载:你得自己找一个入口,在那里把监听登记上去。入口就是 OnBegin,也就是 Event BeginPlay 的覆写。
登记有两种写法(第 25 课):事件.Subscribe(函数) 完全对应蓝图的 Bind Event,事件响了就调用那个函数;事件.Await() 则是让当前这条执行线停在原地等。原图是「每次触发,把下面那串节点从头跑一遍」,所以译本用 loop + Await() 更贴形状——循环的每一圈就是原图的一次触发。而 loop 会一直占着它所在的那条线,所以要用 spawn 岔一条常驻线专门跑它(第 24 课),OnBegin 登记完就退场。
原图的这一块:②号 Branch 和⑤号 Delay,执行线的两个关节。
PlateLoop()<suspends>:void =
loop:
Plate.InteractedWithEvent.Await()
if (not IsOpen?):
OpenDoor()
Sleep(OpenDuration)
CloseDoor()
Branch → if:蓝图的 Branch 有一个 Boolean 条件引脚、两个出口。Verse 的 if 括号里要的不是一个布尔值,而是一段「走不走得通」的判断——官方叫失败上下文(第 12 课)。所以 logic 变量得先加一个 ? 变成一次盘问:IsOpen? 在 true 时走得通、false 时走不通(第 11 课)。原图的条件引脚上挂了个 NOT,取反就是 not IsOpen?。True 引脚往下那一串节点 = 缩进进去的那一块;原图 False 引脚空着,译本就不写 else。
Delay → Sleep:蓝图的 Delay 是 Latent 节点,左上角挂着小钟表。Verse 里它叫 Sleep(秒数),秒数直接喂 OpenDuration,和原图把变量拖到 Duration 引脚上是同一件事。
但这里有一条蓝图作者最容易忽略的规则:能等的东西,只能待在挂了 <suspends> 的函数里。原图能把 Delay 随便丢进任意一张事件图,是因为蓝图的事件图天生就允许 Latent 节点;Verse 把这件事写进了函数签名——PlateLoop()<suspends>:void 里那个 <suspends>,就是「这张函数图允许放带小钟表的节点」的书面许可(第 23 课)。删掉它,Sleep 和 Await() 当场编不过。反过来,下一步那两个小函数不写 <suspends>,因为它们不等任何东西,一口气跑完就返回。
原图的这一块:③④两格和⑥⑦两格。
OpenDoor():void =
set IsOpen = true
TargetDoor.Disable()
Print("门已打开,倒计时开始。")
CloseDoor():void =
set IsOpen = false
TargetDoor.Enable()
Print("门已关闭,压力板复位。")
为什么这样对应:③④和⑥⑦是同一种形状(记状态 + 拨那扇门)出现了两次,所以打包成两个小函数——蓝图里你也会把重复的一串节点折叠成 Collapse to Function,是同一个动作。set 就是 Set 节点,一个都不能省(第 8 课)。
还有一处极性陷阱:barrier 设备的 Enable() 意思是「屏障存在」,也就是门关;Disable() 是「屏障消失」,也就是门开——和原图 Open() / Close() 的字面意思正好反着。逐节点翻译时这一格翻反的概率极高。把它封进函数名里,图上从此只出现「开门」「关门」,再也不用在脑子里做一次取反。
六步走完,零件齐了。下一节把它们拼成完整的 .verse 文件,并且一行一行跑给你看。
下面就是 BP_PressurePlate 的完整 Verse 译本,43 行。右边运行面板里的每一步,都会告诉你这一行对应原图里的哪个节点——点「运行下一步」,一路对回去。
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# BP_PressurePlate 的 Verse 译本——逐节点对照见本课「蓝图对照」
pressure_plate_door := class(creative_device):
# 原图的碰撞盒:触发源改由关卡里的一台设备承担
@editable
Plate:button_device = button_device{}
# 原图变量面板:TargetDoor,BP_Door 的 Actor 引用,已勾 Instance Editable
@editable
TargetDoor:barrier_device = barrier_device{}
# 原图变量面板:OpenDuration,Float,已勾 Instance Editable
@editable
OpenDuration:float = 5.0
# 原图变量面板:IsOpen,Boolean,未勾 Instance Editable
var IsOpen:logic = false
OnBegin<override>()<suspends>:void =
Print("压力板译本上线,等待触发。")
spawn{ PlateLoop() }
PlateLoop()<suspends>:void =
loop:
Plate.InteractedWithEvent.Await()
if (not IsOpen?):
OpenDoor()
Sleep(OpenDuration)
CloseDoor()
OpenDoor():void =
set IsOpen = true
TargetDoor.Disable()
Print("门已打开,倒计时开始。")
CloseDoor():void =
set IsOpen = false
TargetDoor.Enable()
Print("门已关闭,压力板复位。")
点「运行下一步」,每一步都标出它在原图里是哪个节点。
跑完你会注意到一个语义差异,值得记一笔:原图里 Event ActorBeginOverlap 每次都会触发,②号 Branch 才是那个把重复触发挡回去的门卫;译本里 loop 没绕回第 29 行之前,新的触发根本不会被接住——结构本身就防重入了,IsOpen 这个门卫其实已经冗余。先照搬,别急着删:第一遍忠实,第二遍再决定要不要重新设计。这正是拔高拓展页要谈的事。
这五条不是随手列的,是「蓝图作者第一次翻译」几乎必踩的五格。每条都标了回链课号——踩到了就回去补一课。
原图里每个 Set 节点,译本里都必须有一个 set。蓝图里「改变量」是拖一个节点、连一根线,动作很具体所以不会忘;Verse 里它只是行首两个小写字母,抄代码时最容易丢。丢了不会静默出错,而是当场编不过——只是报错文案不会说「你忘了 set」。回去看第 8 课。
接着上一条:漏了 set 写成 IsOpen = true,Verse 不会当成赋值,而是当成比较——「IsOpen 等于 true 吗」。而且 Verse 里根本没有 ==,单个 = 就是比较。于是报错会拐一个大弯,抱怨这个判断没放在允许失败的地方。看到「failure context」四个字,先回头找漏掉的 set。回去看第 13 课。
把 Branch 直译成 if (IsOpen): 是本课最高频的错。蓝图的条件引脚吃的是一个布尔值,Verse 的 if 括号里吃的是一段能不能走通的判断。logic 变量要加 ?:if (IsOpen?):;取反是 if (not IsOpen?):。同理,那些名字带方括号的可失败函数,也只能写在 if 这类地方。回去看第 11、12 课。
蓝图里 Delay 想放哪就放哪,只要那张图允许 Latent 节点——而事件图默认就允许,所以你从没为此操过心。Verse 把这条许可写进了函数签名:Sleep 和 Await() 只能出现在挂了 <suspends> 的函数里。翻译时凡是遇到带小钟表的节点,先回头给宿主函数补上那块牌子。回去看第 23 课。
蓝图里一个没绑定的 Actor 引用是 None,访问它就是运行时 Accessed None——每个蓝图作者都被它咬过。Verse 里「可能没有」是一个独立的类型 option,取值必须先过一道 if,取不到就走不通。代价是写法啰嗦一点,回报是这类崩溃在编译期就被堵死。回去看第 18 课。
不算「语言坑」,但翻车率同样高:Enable / Disable 的语义和原图 Open / Close 正好反着;BP_MyDoor 到 my_door 还要换一套命名规范。两件事都靠同一个办法解决——先封装成语义明确的小函数,再往图上摆。
一门诚实的迁移课必须讲这一节。下面这几样东西,不是「Verse 里写法不同」,而是不在 Verse 的职责范围内——你去找对应写法,只会浪费一个下午。
| 蓝图里的东西 | Verse 里的状况 | 那该怎么办 |
|---|---|---|
| Timeline 节点(曲线编辑器、Alpha 输出、Play / Reverse 引脚) | 没有对应物。Verse 没有内建的曲线资产,也没有随时间吐出 Alpha 的节点 | 「随时间变化」这一半可以自己写:loop + Sleep + 自己算插值。但曲线编辑器那一半搬不过来——那是编辑器工具,不是语言特性 |
| Construction Script(摆下去就跑一遍、在编辑器里实时生成东西) | 没有对应物。Verse 代码在游戏运行时才执行 | @editable 只负责把参数暴露到面板上,不负责在编辑器视口里实时预览。要「摆下去就看见效果」,那是编辑器工具的活 |
| 纯美术类节点:材质参数、Anim Blueprint 状态机、Niagara 参数、Sequencer 轨道 | 不在 Verse 的职责范围内。它们属于引擎的资产系统,不属于玩法逻辑层 | 继续用它们各自的编辑器做。Verse 负责的是「什么时候该发生什么」,不负责「它长什么样」 |
| Event Tick 里的每帧逻辑 | 写得出来(loop + Sleep(0.0)),但几乎总是错的答案 |
Verse 的时间模型是「停下来等事件」,不是「每帧问一遍」。大多数 Tick 逻辑翻译过来应该变成事件订阅或并发结构——拔高拓展页专门讲这个 |
把这几条摆出来,不是为了泼冷水,而是为了让你在真实项目里划对边界:一张蓝图里通常只有一部分是玩法逻辑,剩下的是美术、动画、编辑器工具。迁移的对象是前者。
关于时间线,把事实再摆一次(措辞按官方口径):Actor 与 Blueprint 在 UE6 Early Access(目标 2027 年底)及早期版本中完整支持;弃用要等 Scene Graph 足够成熟,时间未定。Epic 已承诺在弃用前提供转换工具,但尚未发布。所以这一课教的手工翻译,短期内是「读懂两边」的训练,长期看是「审阅转换工具的产出」的底气——工具能搬结构,搬不动设计意图,你得看得出来哪一段该重写。
整张 BP_PressurePlate,逐节点、逐变量的对照总表。差异那一列才是重点——它解释了为什么有些格子不是简单换个写法,而是换了思路。
| 蓝图里的做法 | Verse 里的写法 | 差异 |
|---|---|---|
| 资产 BP_PressurePlate,Class Settings 里 Parent Class = Actor | pressure_plate_door := class(creative_device): |
蓝图类是二进制 .uasset,Verse 类是纯文本 .verse;命名从 BP_ + PascalCase 换成 snake_case |
变量面板新建 IsOpen(Boolean,小眼睛关闭) |
var IsOpen:logic = false |
蓝图变量默认可变;Verse 默认不可变,接过 Set 节点的才写 var |
变量面板新建 OpenDuration(Float,小眼睛点亮) |
@editable + OpenDuration:float = 5.0 |
小眼睛 = @editable;Verse 字段必须给默认值,不存在「空着以后再填」 |
变量面板新建 TargetDoor(Actor 引用,默认 None) |
@editable + TargetDoor:barrier_device = barrier_device{} |
蓝图引用可以是 None,忘绑 = 运行时 Accessed None;Verse 的 @editable 引用必须有默认值,忘绑 = 指着空壳「没反应」 |
压力板身上的碰撞盒 + Event ActorBeginOverlap |
Plate.InteractedWithEvent.Await()(触发源外置成一台设备) |
没有一对一译文:Verse 里订阅不到「我这个 Actor 被 overlap 了」,触发必须由关卡里的设备广播 |
| 事件节点由引擎自动挂载,拖进图即生效 | OnBegin<override>() 里 Subscribe 或 Await 手工登记 |
蓝图里「登记监听」这一步是隐形的;Verse 里它是你必须自己写的一行 |
| 事件被反复触发(每次 overlap 都跑一遍图) | spawn{ PlateLoop() } + loop: |
循环每圈 = 原图一次触发;但循环没绕回等待点时,新触发会被漏掉,而蓝图会照常触发再被 Branch 挡回去 |
Branch 节点,条件引脚接 NOT + IsOpen |
if (not IsOpen?): |
蓝图条件引脚吃布尔值;Verse 的 if 吃「走不走得通」的判断,logic 必须加 ? |
| Branch 的 True 引脚往下那一串节点 | 冒号之后往里缩进的那一块 | 蓝图靠连线表达从属,Verse 靠缩进——缩错了直接编译不过,连不上线这种 bug 不存在 |
Set IsOpen 节点 |
set IsOpen = true |
省掉 set 不是赋值,而是变成比较;Verse 没有 == |
TargetDoor → Open() / Close() |
TargetDoor.Disable() / TargetDoor.Enable() |
极性反了:barrier 的 Disable = 屏障消失 = 门开。建议封进 OpenDoor() / CloseDoor() 消除歧义 |
Delay 节点(Latent,左上角小钟表) |
Sleep(OpenDuration) |
蓝图事件图默认允许 Latent 节点;Verse 要求宿主函数显式挂 <suspends>,否则编不过 |
| Collapse to Function(把重复的一串节点折叠) | OpenDoor():void = / CloseDoor():void = |
思路完全一致;Verse 里还要多做一个决定:这个函数要不要挂 <suspends> |
| Timeline / Construction Script / 材质与动画节点 | —— | 翻译不过去,也不该翻:它们不在 Verse 的职责范围内(见上一节) |
通读这张表,你会发现差异集中在三处:可变性要声明(var)、判断要能失败(? 与失败上下文)、等待要有许可(<suspends>)。这三条不是 Verse 在为难你,而是它把蓝图里三类「运行时才炸」的问题——改错了状态、判断了个空、在不该等的地方等——统统提到了编译期。代价是多写几个关键字,回报是那三类崩溃在你按下 Build 的那一刻就被拦下。
还有一层差异不在语法上:蓝图是二进制资产,两个人同时改一张事件图,冲突基本只能二选一;.verse 是文本,能 diff、能 review、能逐行合并,也能直接交给 AI 读写——这正是下一课的主题。
先把译本最关键的三块补回去,再答三道题。答对拿星星 ★,答错可以一直重试。
# 补全三个空:让 Delay 合法的那块牌子、Set 节点的关键字、Delay 本身
PlateLoop()<____>:void =
loop:
Plate.InteractedWithEvent.Await()
if (not IsOpen?):
____ IsOpen = true
TargetDoor.Disable()
____(OpenDuration)
set IsOpen = false
TargetDoor.Enable()
原图⑤号 Delay 翻成 Sleep(OpenDuration) 之后,为什么宿主函数 PlateLoop 必须写 <suspends>?
原图②号 Branch 的条件是「IsOpen 为 false」。下面哪一种才是正确的译文?
如果原图里还挂着一个 Timeline 节点做开门动画,迁移时该怎么处理?
回头看看这一课发生了什么:你拿到一张陌生的蓝图,没有一行现成代码,靠一套固定顺序——骨架、字段、引用、事件、执行线、收尾——把它变成了一份能编译、行为对得上的 .verse 文件。这套顺序对任何一张蓝图都成立;换一张更大的图,只是重复的次数变多,步骤一格不变。
你同时拿到了两样更值钱的东西。一是判断力:哪些格子能直译、哪些格子要换思路、哪些格子根本不该翻。二是词汇量:你现在能用 Verse 的术语描述一段蓝图逻辑,也能用蓝图的术语解释一段 Verse 代码——这是双向的。
下一课把这个流程再提一次速。.verse 是纯文本,能 diff、能 review、能整份贴给 AI;而 UE6 编辑器已经集成了 Claude / Gemini 这类生成式 AI 助手。当你能把「原图长这样,请翻成 Verse」讲清楚——而这一课刚好教会了你怎么讲清楚——翻译的体力活就可以交出去,你留下判断力那一半。第 30 课见。
技巧 · EXTRA
一张可打印的清单。打开蓝图之前先问自己 8 个问题,翻译过程中逐项核对 10 条,翻完之后按 5 步验收——把「凭记性」换成「照着划勾」。
进入拓展 →拓展 · EXTRA
同一段逻辑,如果放进 Scene Graph 的组合模型里该怎么拆?一台 device 拆成三个 component 的思路对照,呼应第 26、27 课。
进入拓展 →拔高 · EXTRA
逐节点翻译能搬结构,搬不动设计意图。轮询、Tick 状态机、一堆布尔旗标——这三类蓝图模式在 Verse 里都有明显更好的写法。
进入拓展 →