拔高 · EXTRA
执行线 vs 表达式:为什么 Verse 里没有「白线」
蓝图用白线和彩线把「做一件事」和「算一个值」分成两类。Verse 把这条界线拆了:一切都是表达式,连 if 和代码块都有值。这对你意味着什么?
进入拓展 →上一课解决「东西叫什么」,这一课解决「东西怎么摆」。蓝图图表是一张摊在二维平面上的图:你靠眼睛顺着线走。Verse 是一段从上往下读的文本:顺序和缩进替你把线画好了。这一课把图表里的每一种线、每一种框,逐个翻译成文本结构。
打开任何一张蓝图图表,不管它有多乱,视野里能看见的元素就四类:白色执行线、彩色数据线、节点本身、注释框。把这四样各自翻译一遍,你就掌握了整套结构转换。
白色执行线 → 顺序 + 缩进。这是最反直觉的一条:在 Verse 里,执行线不存在对应物。没有关键字、没有符号、没有箭头。原因很简单——顺序本身就是执行线。上一行跑完跑下一行,这就是那根白线。蓝图之所以需要画线,是因为节点在平面上可以摆在任何位置,不画线你就不知道谁先谁后;文本天然只有一个方向,线就没必要了。你在图上花在「理线」上的那份力气,在 Verse 里直接省掉。
彩色数据线 → 表达式嵌套,或者一个变量名。把 A 节点的输出引脚拉一根线接到 B 节点的输入引脚,写成文本就是两种形态:要么把 A 直接塞进 B 的括号里——B(A()),一层括号就是一根线;要么先给这个中间结果起个名字——X := A(),再写 B(X)。前者短,后者好读,两种都对,选哪种全看这根线有多长、要不要复用。
节点 → 函数调用。图上那个带引脚的方块,在文本里就是 名字(参数)。Print String 变成 Print("Hi"),Get Player Count 变成 GetPlayerCount()。引脚的名字变成参数的位置或名字,仅此而已。
注释框 → # 注释。蓝图的注释框(选中一堆节点按 C)能把一片区域框起来、涂个颜色、起个标题。Verse 的 # 只能注释掉一行,做不到「框住一片」——但它也不太需要:Verse 里「这几行属于同一块」这件事,已经由缩进本身表达清楚了,注释框的框选功能被缩进接管了,只剩下「起个标题」的功能留给 #。
四条合起来,一张三节点的小图就长这样:
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
score_board_device := class(creative_device):
# 注释框 → # 注释:给下面这一小段起个标题
Score:int = 40
OnBegin<override>()<suspends>:void =
# 数据线 → 表达式嵌套:一层括号就是一根线
Print("总分:{Score + 10}")
# 也可以先给中间结果起个名字,再用它
Total := Score + 10
Print("总分:{Total}")
读图翻译:Score 是变量面板里的一个整数;OnBegin 是 Event BeginPlay 那个红色事件节点;它下面缩进的四行,是顺着白线依次排开的四个节点。Score + 10 是一个 Add 节点,它的输出用一根数据线接到 Print String 的输入——写成文本,那根线就是把 Score + 10 写进大括号里。下面两行是同一根线的另一种画法:先拉出来落成 Total,再接进去。
上面说「执行线就是顺序」,马上会遇到一个问题:有些节点不止一个输出执行引脚。Branch 有 True 和 False,Sequence 有 Then 0 / Then 1 / Then 2,ForEach 有 Loop Body 和 Completed。一条直上直下的文本,怎么表达「兵分两路」?
答案就是缩进块:一个出口 = 一个缩进块。节点在图上朝右分出几条线,在文本里就朝里缩进几块。
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
turret_device := class(creative_device):
var Ammo:int = 1
OnBegin<override>()<suspends>:void =
# Branch 的 True 引脚 → 冒号后缩进进去的这一块
if (Ammo > 0):
set Ammo -= 1
Print("开火")
# Branch 的 False 引脚 → else 后缩进进去的这一块
else:
Print("空仓")
# Sequence 的 Then 0 / Then 1 / Then 2 → 同一层缩进的三行
Print("一")
Print("二")
Print("三")
| 蓝图节点 | 输出执行引脚 | Verse 里的写法 |
|---|---|---|
| Branch | True / False | if (…): 下面的块 / else: 下面的块 |
| Sequence | Then 0 / Then 1 / Then 2 | 同一层缩进的第 1 / 2 / 3 行(Sequence 没有对应物) |
| ForEach / For Loop | Loop Body / Completed | for (…): 下面的块 / 退回缩进后的下一行 |
| Switch on Int / String | 每个 case 一个引脚 | if 一层层接下去,或后面章节的 case |
表里最值得盯着看的是 Sequence 那一行:它在 Verse 里压根不存在。你之所以在蓝图里需要 Sequence 节点,是因为一个执行引脚只能接一根线,想连着做三件事就得请一个专门的节点来分线。文本里「连着做三件事」就是写三行,不需要任何节点。同理消失的还有 Reroute 节点(拖线整理用的中转点)——它在 Verse 里的等价物是给中间结果起个名字,而那件事你是为了好读才做的,不是为了让线不打架。
另一件事:蓝图里 True 和 False 那两条线画在图上一上一下,Verse 里 if 块和 else 块也是一上一下地写。方向感完全一样,只是从二维压成了一维。你原本用眼睛顺线走的那套本能,在这里依然管用。
把规则完整说一遍,只有三条:
: 开一个块。if (…):、for (…):、else:、类名 := class(父类):,后面跟的都是一个块。using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
vault_device := class(creative_device):
OnBegin<override>()<suspends>:void =
Print("开始")
for (Round := 1..2):
Print("第 {Round} 轮开始")
if (Round > 1):
Print("这是最后一轮")
Print("第 {Round} 轮结束")
Print("全部结束")
数一数缩进,这段有四层:缩进 0 是模块最外层(using 和类名那一行);缩进 4 是类的身体;缩进 8 是函数的身体;缩进 12 是 for 的身体;缩进 16 是 if 的身体。这在蓝图里就是三层嵌套的连线:类图 → Event BeginPlay 的执行线 → ForLoop 的 Loop Body 引脚 → Branch 的 True 引脚。区别只在于蓝图把这三层摊在同一张平面上、靠线连着,你得顺着线走才知道自己在第几层;Verse 把它们摞成阶梯,缩进几格就是第几层,一眼可数。
再看两处「退回缩进」的地方。Print("第 {Round} 轮结束") 退回到缩进 12,意思是它跳出了 if——不管刚才那个 Branch 走的是 True 还是 False,这一行都会跑,它接的是 Branch 两条线汇合之后的那根线。最后一行 Print("全部结束") 退回到缩进 8,意思是它跳出了 for——它接的正是 ForLoop 的 Completed 引脚。在蓝图里,「两条分支汇合」和「循环跑完」需要你把线拉回来重新接上;在 Verse 里,你只是少按了四下空格。
光看缩进还不够,要看它跑起来怎么跳。下面这台门禁设备有 2 把钥匙、要开 3 轮门,循环里套着分支——正好把上面两节的东西全用上。点「运行下一步」,每一步的说明都会告诉你:如果这是一张蓝图图表,此刻高亮的是哪个节点、走的是哪根线。
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
gate_device := class(creative_device):
var Keys:int = 2
OnBegin<override>()<suspends>:void =
for (Round := 1..3):
if (Keys > 0):
set Keys -= 1
Print("第 {Round} 轮:开门,剩余 {Keys}")
else:
Print("第 {Round} 轮:没钥匙了")
点「运行下一步」,看代码怎么一行行执行。
回头看整段:代码只有 14 行,可执行的部分只占 6 行,却完整表达了「一个循环套一个双出口分支」。同一件事画成蓝图,你需要 Event BeginPlay、ForLoop、Branch、Set、两个 Print String 六个节点,外加至少七根线,而且为了不让线交叉,你多半还得挪半天位置。这不是说蓝图差——图有图的好处(下一课会讲哪些好处是 Verse 给不了的)——但「结构」这一项,文本确实赢得干净。
结构翻译完了,还剩一个隐形的差异:值是什么时候算出来的。这一条不写在图上,也不写在代码里,但它能让蓝图作者结结实实踩一脚。
蓝图这边是「按需拉取」(pull)。数据线上的节点不挂在白线上,它们不会主动跑;只有当下游某个节点需要那个值的时候,才顺着数据线往回拉一次、算一次。所以同一个 Pure 节点,输出引脚拉出去三根线,就会被算三次——图上你只看见一个方块,心里以为只算一次。
Verse 这边是「从左到右、从里到外」(push)。执行到某一行时,这一行里的表达式就地求值:最里层的括号先算,算完把值交给外面一层;同一层的多个参数从左往右依次算。没有延迟,没有缓存,写几遍就算几遍。
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
shop_device := class(creative_device):
var Coins:int = 100
# 一个「算价格」的小函数,顺手打一行日志暴露自己被调用了
# (函数怎么写,第 19 课细讲,这里只数它跑了几次)
Price():int =
Print("—— 算了一次价格")
30
OnBegin<override>()<suspends>:void =
# 写了两遍 Price(),就调用两次,日志会打两行
if (Coins > Price()):
Print("买得起,剩 {Coins - Price()}")
# 只想算一次?先给它起个名字
P := Price()
if (Coins > P):
Print("买得起,剩 {Coins - P}")
上半段的日志会出现两次「算了一次价格」,下半段只出现一次。Coins - Price() 这一句的求值顺序也值得念一遍:先算最里面的 Price() 得到 30,再算减法得到 70,最后才把 70 交给字符串插值——从里到外,和你在蓝图里从最左边的节点顺着数据线往右传是同一个方向。
坑在哪。蓝图养出来的直觉是「Pure 节点是免费的,画一个随便拉」——因为它长得像一个值,不像一次调用。搬到 Verse 之后,这个直觉会让你在一个函数里反复写 GetSomething(),而那个函数背后可能每次都在遍历一整个数组。反过来说,这个坑在蓝图里其实一直存在(三根线就是三次求值),只是图把它藏起来了;Verse 把它摊在字面上——你写了两遍,它就在那儿明摆着。看得见的坑不算坑。规矩很简单:同一个值要用两次以上,就先给它起个名字。
下面这段挖掉了三个开块的关键字。注释里写了它在图表上对应哪个引脚,照着填:
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
arena_gate_device := class(creative_device):
var Lives:int = 2
OnBegin<override>()<suspends>:void =
# 跑三轮:下面缩进的这一块是 ForLoop 的 Loop Body 引脚
____ (Round := 1..3):
# 一个 Branch:冒号后缩进进去的是 True 那条线
____ (Lives > 0):
set Lives -= 1
Print("第 {Round} 轮:进场")
# False 那条线:和上面那个块并排,缩进相同
____:
Print("第 {Round} 轮:没命了")
这一课没有新语法要背,只有一张对照表要记熟。左边是你在图上看得见的东西,右边是它在文本里的形状,差异那一列才是重点。
| 蓝图图表里的元素 | Verse 里的文本结构 | 差异 |
|---|---|---|
| 白色执行线(Exec) | 书写顺序 + 缩进(没有任何符号) | 接不错、断不了、也不会出现「忘了连线的孤岛节点」;代价是不能画成任意形状 |
| 彩色数据线(数据引脚之间的连线) | 表达式嵌套 B(A()),或先落成名字 X := A() |
蓝图的数据线能跨半张图拉过去;Verse 的名字只在自己那个缩进块里有效 |
| 节点(函数调用节点) | 函数调用 Print("Hi") |
蓝图节点的「纯不纯」写在节点长相上;Verse 写在函数签名的说明符里,编译器会查(见拓展 x2) |
| 注释框(选中节点按 C) | # 行注释 |
# 框不住一片区域——但「这几行是一块」已经由缩进说清楚了 |
| Branch 的 True / False 引脚 | if (…): 块与 else: 块 |
Verse 的 if 条件不止能填布尔,还能填「可能走不通的表达式」(第 12 课) |
| Sequence 节点 | 同一层缩进的连续几行 | 没有对应物:文本天然能连着写几件事,不需要专门的分线节点 |
| Reroute 节点(理线用的中转点) | 中间变量 X := … |
蓝图起中转是为了线不打架,Verse 起名字是为了人好读、值只算一次 |
| ForEach 的 Loop Body / Completed | for (…): 块 / 退回缩进后的下一行 |
「循环跑完接着干什么」在蓝图里要把线拉回来,在 Verse 里只是少缩进一层 |
| 数据线的「按需拉取」求值 | 执行到那一行时,从左到右、从里到外求值 | 两边都是「用几次算几次」,但蓝图把重复求值藏在图里,Verse 摊在字面上 |
这些差异有一个共同的来源:蓝图是二维的,Verse 是一维的。二维给了你自由摆放的空间,代价是「谁先谁后」「谁属于谁」这两件事必须靠画线额外说明;一维本来就只有一个方向和一个深度,顺序和归属自动就有了答案,代价是你没法把两块逻辑并排放在一起对着看。
所以迁移时真正要改的不是手,是眼睛:以前你读图是「找到红色事件节点,顺着白线走」,以后你读代码是「找到函数名,从上往下读,遇到缩进就知道进了一层」。这个切换比记语法快得多——大多数人半天就能过去。
三道小关卡,验收一下这张对照表。答错可以一直重试,零惩罚。
蓝图里那根白色执行线,在 Verse 里对应什么?
Sequence 节点的 Then 0 / Then 1 / Then 2,在 Verse 里怎么写?
一段代码里,条件写了 Price(),下面那行的插值里又写了一次 Price()。Price 被调用了几次?
拔高 · EXTRA
蓝图用白线和彩线把「做一件事」和「算一个值」分成两类。Verse 把这条界线拆了:一切都是表达式,连 if 和代码块都有值。这对你意味着什么?
进入拓展 →拓展 · EXTRA
Pure 节点没有执行引脚、每用一次重算一次。Verse 把「纯不纯」写进了函数签名:computes、transacts、decides、suspends 各管什么。
进入拓展 →技巧 · EXTRA
同一段代码,冒号加缩进能写,大括号加分号也能写。什么时候用哪种,单行 if (X) {} 的惯用法,以及团队里怎么定规矩。
进入拓展 →