纯节点(Pure)与 Verse 的效果说明符
主课说「节点 → 函数调用」,但漏了一件事:蓝图的节点分两种长相,有白色执行引脚的和没有的。这个区别在 Verse 里没有消失,它被搬到了一个更严格的地方——函数签名。
一、先把 Pure 节点看清楚
蓝图里有些节点接不上白线:Add、Get、Make Vector、Greater Than、Get Player Count。它们只有数据引脚,没有执行引脚,这类节点叫 Pure(纯)节点,图标是绿色的圆角小方块。它们有三个特征:
- 不排队:不挂在白线上,所以没有「什么时候执行」这回事。
- 用一次算一次:输出引脚拉出去三根线,就被算三次(主课第五节说过)。
- 不该改状态:这是个约定——Pure 节点被假定为「只算不改」。
关键在第三条的那两个字:约定。在蓝图里勾选一个函数的 Pure 选项,引擎不会去验证你在函数体里到底有没有偷偷 Set 一个变量。你勾了 Pure,它就长成 Pure 的样子,仅此而已。C++ 侧的 BlueprintPure 标记同理:它是节点作者写下的一句承诺,不是一份被检查的合同。所以蓝图里「这个节点纯不纯」这个信息,准确度取决于写它的人有多自觉。
二、Verse 把它写进了函数签名
Verse 里对应的机制叫 效果说明符(effect specifier),写在函数名之后、返回类型之前的尖括号里。你其实一直在看它——每节课都出现的 OnBegin<override>()<suspends>:void,末尾那个 <suspends> 就是。
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
score_device := class(creative_device):
var Score:int = 100
# 纯计算:只吃参数、只吐结果,不碰任何会变的状态
Doubled(X:int)<computes>:int = X * 2
# 可能失败:买得起才走通,买不起就算「失败」
Afford(Price:int)<decides><transacts>:void =
Score >= Price
# 不写说明符 = <transacts>:能读能写,和挂在白线上的普通节点一样
AddScore(N:int):void =
set Score += N
OnBegin<override>()<suspends>:void =
Print("{Doubled(21)}")
if (Afford(30)):
AddScore(-30)
Print("买了,剩 {Score}")
三个函数,三种身份。Doubled 是彻头彻尾的 Pure 节点;AddScore 是挂在白线上的普通节点(它 set 了变量);Afford 则是蓝图里没有的第三种东西——一个「可能走不通」的函数,调用它必须放在 if 这类允许失败的位置。
三、蓝图作者最先撞上的五个
说明符不止五个,但下面这五个覆盖了你迁移时 95% 的场景。这里只写「大致管什么」,精确语义以官方文档为准:
| 说明符 | 大致管什么 | 蓝图里的类比 |
|---|---|---|
<computes> |
纯计算:同样的输入永远给同样的输出,不读也不写会变的状态 | 最纯粹的那种 Pure 节点(Add、Max 这类数学节点) |
<converges> |
「最小」的效果集:保证在有限时间内算出一个值,不会陷进去出不来 | 没有对应物——蓝图不关心你的函数会不会死循环 |
<transacts> |
默认档:可以读、写、分配;这些动作被放进一个事务,配合失败时可以整体回滚 | 挂在白线上的普通(Impure)节点 |
<decides> |
这个函数可能失败;调用它就是一个「可能走不通」的表达式,必须写在允许失败的地方 | 多加一个 bool 输出引脚,再手动接一个 Branch 去判断 |
<suspends> |
这个函数可以中途暂停、把控制权让给别的并发任务,跨多次模拟更新才跑完 | Latent 节点(Delay、Timeline 那种右下角带时钟图标的) |
还有一条组合规则值得记:说明符分成独占和叠加两类。<converges> / <computes> / <transacts> 是独占的,一个函数最多挑一个;<decides> / <suspends> / <reads> / <writes> / <allocates> 是叠加的,可以和独占的写在一起,比如上面的 <decides><transacts>。有一条硬限制:<decides> 和 <suspends> 不能同时用——「可以回滚」和「跨帧等待」这两件事凑不到一起。
什么都不写会怎样?按官方文档,不写说明符的函数默认就是 <transacts>——相当于蓝图里默认给你一个 Impure 节点:能读能写,老老实实排在白线上。你需要主动申请的,是比默认更纯(<computes>)或者更特别(<decides>、<suspends>)的那些。
顺手澄清一个容易误会的地方:<computes> 不代表结果会被缓存。主课第五节讲过,Verse 是写几遍算几遍;说明符描述的是「这个函数能做什么」,不是「引擎会怎么优化它」。同一个值要用两次,该起名字还是得起名字。
四、最大的差异:这是合同,不是注释
说明符的价值不在于「多写几个词」,而在于编译器会替你检查。三个具体后果:
- 在一个
<computes>函数里试图set一个变量?一按 Compile 就被拦下——你承诺过它不碰状态。而蓝图里勾了 Pure 却偷偷 Set 变量,引擎连眉头都不皱。 - 调用一个
<decides>函数却没把它放在if这类允许失败的位置?同样过不了。第 8 课那条看不懂的「failure context」报错,根子就在这里。 - 效果会往上传染:一个函数不能调用比自己「效果更大」的函数。这就是为什么
OnBegin挂着<suspends>——因为它里面可能会调用需要跨帧等待的东西,而这件事必须在签名上说明白。
换个说法:蓝图把「这个节点会干什么」画在节点长相上,靠人自觉、靠人眼看;Verse 把它写进签名,靠编译器强制。你打开一个陌生的 .verse 文件,不用读函数体,光看一行签名就知道这个函数会不会改状态、会不会失败、会不会卡住等几帧——这份信息在蓝图里要么得点开函数图翻一遍,要么根本无从得知。文档变成了合同。
代价当然也有:多了一套要记的词,而且刚开始写的时候,你会被一堆「这里不允许有这个效果」的报错拦住。好消息是这些报错几乎都指向真实的设计问题——它在问你「你到底想让这个函数干什么」,而你在蓝图里从来不用回答这个问题。
五、小测验
蓝图里的 Pure 节点(没有执行引脚、每拉一根线就重算一次、只算不改),在 Verse 里最接近哪个说明符?
来源
本文整理自 Epic 官方文档与官方语言手册:Verse Glossary(官方文档)↗ · Specifiers and Attributes in Verse(官方文档)↗ · Book of Verse — Effects(官方语言手册)↗。说明符的完整清单与精确语义以官方文档为准。