Verse Wiki — 写给蓝图作者的 Verse 手册
第六章 · 第 22 课

specifiers 与 attributes:代码的徽章与便签

你在蓝图 Details 面板里勾的那些复选框——Instance Editable、Pure、Private——引擎一直在背后拿它们给每样东西贴「标签」。Verse 把这些标签摆到了明面上:一类用尖括号 <> 写,是「徽章」——Compile 按钮会逐条检查、不合格就报红的硬规矩;另一类以 @ 开头,是「便签」,不改逻辑,只是对编辑器喊话(你早就会的 Instance Editable 小眼睛就是其中一张)。这一课把两套标签讲清,顺便拆掉那条人人都会撞上的 no_rollback 报红。

一、两套系统:尖括号徽章与 @ 便签

回顾一下你一路见过的写法:游戏开始事件 OnBegin(Verse 版的 Event BeginPlay)后面跟着 <override> 和 <suspends>,可能失败的函数戴着 <decides>,想在编辑器面板里调的数值上方蹲着一行 @editable。这些「挂在名字旁边的小标记」其实来自两套完全不同的机制。

specifier(尖括号徽章)用尖括号 <> 写,紧跟在名字或关键字后面,例如 <public><final>。它管的是「这段逻辑允许怎么被用」的硬规矩,由 Compile 按钮(蓝图左上角那个 Compile,Verse 里对应快捷键 Ctrl+Shift+B)强制执行:说不能继承就不能继承,说可能失败就必须按可能失败的规矩调用。把它想成焊死在装备上的附魔词条——上场之前,系统逐条验装,一条不合格就亮红、不放行。

attribute(@ 便签)以 @ 开头,独占一行,写在某样东西的上方,例如 @editable。它不改逻辑本身,而是告诉UEFN 编辑器或运行环境「请特殊对待它」——@editable 干的就是把一个变量的 Instance Editable 小眼睛点亮,让它出现在 Details 细节面板里,策划不碰一行逻辑就能调。它更像挂在装备上的便签,是写给商店老板(编辑器)看的。

badge_demo_device.verse
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }

badge_demo_device := class(creative_device):

    # attribute:@ 开头,独占一行,写在声明上方
    @editable
    var Prize:int = 100

    # 访问 specifier:紧跟在标识符后面
    var Secret<private>:int = 0

    # 效果 specifiers:在参数表之后、返回类型之前
    IsLucky()<decides><transacts>:void =
        Prize > 50

    # override + suspends:两枚你早就认识的徽章
    OnBegin<override>()<suspends>:void =
        Print("徽章展览开幕")

读图翻译:这段 badge_demo_device 就是一张「徽章陈列柜」——从上到下摆了四样东西。@editable 那行是贴在 Prize 变量上方的便签(点亮它的 Instance Editable 小眼睛);Secret<private> 是一个只有自己能碰的私有变量;IsLucky()<decides><transacts> 是一个「可能失败、失败了还能整体撤销」的判断函数;最后 OnBegin 就是 Verse 版的 Event BeginPlay,游戏一开始自动跑,里面用 Print String 打了句「徽章展览开幕」。每样东西旁边挂的尖括号或 @,就是这一课的主角。

位置也有规矩,记四条:类的徽章贴在 class 后面(class<final>);函数的效果徽章贴在那对小括号后面、冒号前面(F()<decides><transacts>:int);管「谁能碰」的访问徽章贴在名字后面(X<private>:int);@ 便签永远自己独占一行、待在上方。贴错地方——比如把 <public> 挪到行首、或者把 @editable 写成尖括号版 <editable>——一按 Compile 就报红。

二、修饰符总览:一张表看全

Verse 的徽章按「管什么」分三大类:管「谁能看见、谁能碰」的访问徽章、管「这个函数会对世界做什么」的效果徽章、管「这张蓝图长什么样、能不能被继承」的类徽章。一张表看全:

类别 specifier 一句人话 示例
访问 <public> 完全公开,谁都能用 Score<public>:int
<internal> 同一模块内可用——不写就是它(默认值) Helper<internal>():void
<protected> 只有本类和它的子类能碰 var HP<protected>:int
<private> 只有本类自己能碰 var Seed<private>:int
效果 · 独占
(四选一)
<transacts> 做的事可以整体回滚(隐含读、写、新建对象三种效果) AddHit()<transacts>:int
<varies> 同样的输入,可能给出不同结果 多见于官方 API 签名
<computes> 纯计算,无副作用:同输入必同输出 多见于官方 API 签名
<converges> computes 再加一条:保证一定算得完 字段初始化等场合引擎要求它
效果 · 附加
(可叠加)
<decides> 可能失败,调用要用方括号 Foo[] IsReady()<decides><transacts>:void
<suspends> 异步函数,可跨多个模拟帧慢慢跑 OnBegin<override>()<suspends>:void
类 / 成员 <abstract> 不能实例化,留给子类补全 base := class<abstract>:
<final> 到此为止:类不能再被继承,成员不能再被重写 boss := class<final>:
<unique> 实例有唯一身份,可用 = 比较、可当 map 键 pass := class<unique>:
<concrete> 所有字段都有默认值,可用空原型 {} 实例化 @editable 自定义类需要它
<override> 我在重写父类的成员 Tick<override>():void

效果徽章有一条重要的组队规则:transacts / varies / computes / converges 是「独占款」,一个函数只能戴一个;decides 和 suspends 是「附加款」,可以叠在独占款上面,比如 IsPuzzleSolved()<decides><transacts>:void。而什么效果都不写时,函数默认拿到的是 no_rollback——「做过的事覆水难收」。这个不起眼的默认值会在下一节掀起风浪。平时做 UEFN,你会主动写的基本只有 transacts、decides、suspends 三个;computes、converges、varies 你更多是在翻官方现成节点/函数的说明时碰到,认得它们就够了。

访问徽章还有个隐藏玩法:它既能贴在变量名字上,也能单独贴在 var 这个关键字上,把「读」和「写」的权限拆成两半:

score_module.verse
# 谁都能读分数,但只有本模块能改它
var<internal> Score<public>:int = 0

最后敲一下黑板:什么访问徽章都不写时,默认是 internal(只在同一个模块/文件夹里可见),不是 public。想从别的模块引用一个忘了标 <public> 的东西,Compile 会甩来一句 unknown identifier(找不到这个名字)——东西没丢,只是被门禁拦在外面了。

三、decides 与失败上下文:方括号背后的契约

之前的课程说过:带 <decides> 的函数,调用时要用方括号 [] 而不是小括号。现在可以把背后的道理讲完整了。

<decides> 把一个函数变成「可能走不通」的判断——它跑到一半可能当场宣布「没过」。Verse 对「没过」的态度和别处不一样:它不是报错、不是崩溃,而是像掷骰没过判定一样的正常结果,但它必须有地方接住。谁来接?就是那些「允许一条线走不通」的地方——最典型的是 if(蓝图里的 Branch)。不过注意:蓝图的 Branch 问的是 True/False,Verse 的 if 问的却是「这条线到底能不能走通」,走不通就转去走 False(else)那一路。所以 decides 函数只能在这种地方调用,方括号 Foo[] 就是明摆着的路标:「注意,这一步可能走不通」。

接住「没过」还不够,还得善后:一个判断走到一半,可能已经动了几个变量、做了半件事。Verse 的做法很硬核——这种地方里的一切都是推测执行:就像先在一张「影子图」上把线试连一遍,全走通了才把改动落到真图;只要中途走不通,影子图整个作废,所有改动一笔勾销,像没连过。这正是 decides 总拉着 transacts 一起出场的原因:transacts 就是那份「我干的每件事都能撤回」的承诺。

反过来,这也解释了那条著名的报红「This invocation calls a function that has the no_rollback effect, which is not allowed by its context」:没戴效果徽章的函数默认是 no_rollback,它做的事(比如 Print String 打一行调试字)撤不回来,Compile 自然不准它进这种「可能要反悔」的地方。解法也简单:把这个调用挪到 Branch 外面先跑,把结果存进一个常量,再让 Branch 只拿常量做判断。

下面这台「金库门」把回滚过程演给你看:只有一把钥匙,却要开两扇门。点「运行下一步」,盯紧 Keys 的值。

vault_device.verse
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }

vault_device := class(creative_device):

    # 只有一把钥匙
    var Keys:int = 1

    # 先扣钥匙再验收:失败时 set 会被整体回滚
    TrySpendKey()<decides><transacts>:void =
        set Keys -= 1
        Keys >= 0

    OnBegin<override>()<suspends>:void =
        if (TrySpendKey[]):
            Print("第一扇门开了,剩余钥匙:{Keys}")
        if (TrySpendKey[]):
            Print("第二扇门开了")
        else:
            Print("开门失败,Keys 被回滚,还是 {Keys}")
输出日志

点「运行下一步」,看代码怎么一行行执行。

注意那句 set Keys -= 1(第 11 行)被跑了两次:同一个 Set 节点,第一次被提交、第二次被撤销——决定命运的是紧跟其后的那句判定。这套「先斩后奏但可以反悔」的机制,本质就是把「事务」(一整笔操作要么全成、要么全撤)做进了语言里;想再深挖一层,文末拓展页《效果系统与事务回滚:no_rollback 全解》等着你。

四、@editable:最常用的便签

说完 Compile 管的徽章,轮到编辑器的便签。@editable 是你最常用的便签:把它写在设备(creative_device)某个变量的上一行,这个变量就冒到 UEFN 编辑器的细节面板里——这正是蓝图里那个 Instance Editable 小眼睛干的事,策划和关卡设计师不碰一行逻辑,就能拖着滑条调你的数值。

用它时记三件事:一,这个变量必须有默认值;二,变量类型得在支持清单里(基本类型、设备引用、agent(就是那个「玩家引脚」)、数组,以及标了 <concrete> 的自定义类);三,想要鼠标悬停说明(ToolTip)、面板分组、带范围的滑条这些高级体验,@editable 家族还有更进阶的写法——见文末拓展页。

最后澄清一个高频误会:persistable 是尖括号徽章(specifier),不是 @ 便签!想让一个类能存档,要写成 class<final><persistable>,写成 @persistable 一按 Compile 直接摇头。分辨两套系统就一条准绳:要 Compile 强制执行的硬规矩,用 <> 徽章;只是给编辑器 / 运行环境的悄悄话,才用 @ 便签。

动手时间:下面这台设备被挖掉了三个关键词——一张便签、一枚访问徽章、一个 decides 的黄金搭档。把它们填回去,再点「检查答案」。

drill_device.verse
using { /Fortnite.com/Devices }

drill_device := class(creative_device):

    # 便签:把奖金字段挂到编辑器细节面板
    @____
    var Prize:int = 100

    # 只有本类和子类能碰的护盾
    var Shield<____>:int = 50

    # 可失败函数:decides 的黄金搭档是谁?
    BossAlive()<decides><____>:void =
        Shield > 0

五、常见坑

跟徽章有关的 Compile 报红,大概是全 Verse 报错榜的前排常客。提前踩点,真遇到时不慌:

蓝图对照

这一课几乎每个标记,在蓝图里都是一个复选框或一个下拉。把它们摆在一起看,差异会很扎眼。

蓝图里的做法 Verse 里的写法 差异
变量 Details 面板勾 Instance Editable(那只小眼睛)/ Expose on Spawn @editable,独占一行写在变量上方 作用相同:让变量冒到细节面板里给策划调。但 @editable 挑食——变量必须有默认值,类型必须在支持清单里,自定义类还得挂 <concrete>
变量 / 函数的 Private、Protected、Public 三档访问 <private> / <protected> / <public> 三个词一模一样,默认值不一样:蓝图变量默认 Public,Verse 什么都不写时默认是 internal(只在同模块可见)——跨模块引用报 unknown identifier 的元凶
函数勾上 BlueprintPure(执行引脚消失,变成绿色纯节点) <computes> Pure 只是「没有执行引脚」,引擎并不阻止你在里面搞事;<computes> 是 Compile 强制的承诺:同输入必同输出,想在里面改状态直接报红
Latent 节点(右下角带小钟表,如 Delay、Retriggerable Delay) <suspends> 蓝图的 latent 只能放在事件图表里、不能放进函数;Verse 把它变成了一枚可以贴在任何函数上的徽章,代价是调用方也必须是 <suspends>,或者用 spawn{} 岔出一条线
Const 引脚 / 不给变量拖 Set 节点 不写 var —— Verse 默认就不可变 蓝图默认可改、想锁死要另外设置;Verse 反过来,默认锁死、想改要主动写 var。默认值调了个个儿
Class Settings 勾 Abstract / 函数不允许被子类重写 class<abstract> / <final> 位置从设置面板搬到了名字旁边,语义一致。差别只是 Verse 里这些信息跟代码同屏,不用切窗口去确认
Cast To 节点的 Cast Failed 引脚 / Branch 的 False 口 <decides> + 方括号调用 Foo[] 蓝图的失败口你可以不接,后果自负;Verse 的 <decides> 必须待在能接住失败的地方,而且失败时整笔改动全部回滚——蓝图没有这回事,Cast 失败前做过的 Set 一个都不会撤
SaveGame 对象 / 变量勾 SaveGame class<final><persistable> 都是「这份数据要跨对局活下来」。Verse 的限制更硬:可存档的类里只能放常量、不能放 var,而且它是尖括号徽章——写成 @persistable 直接报红
变量 Details 里的 Tooltip、Category、Slider Range @editable 的块级元数据(见本课拓展) 蓝图是几个输入框;Verse 要手写一小段元数据。功能覆盖不完全对等,且随版本变动——写完最好在编辑器里实测一眼

这张表里最该记住的是「默认值反过来了」这件事。蓝图的默认是宽松的:变量默认 Public、默认可改、Cast 失败口不接也能编译过。Verse 的默认是收紧的:默认 internal、默认不可变、失败必须被接住。原因不难理解——蓝图是给「一边连线一边试」的工作流设计的,能跑起来最重要;Verse 假设你的代码会被别人读、被别人改、会跨模块引用,所以把「你没明说的部分」一律按最保守的方向解释。刚上手时,你有一半的 Compile 报红都来自这个方向差。

另一半差异来自 <decides> 背后那套回滚机制,蓝图里完全没有对应物。蓝图的 Cast Failed 只是换一条执行线走,失败之前你已经改过的变量原封不动留在那儿;Verse 的失败上下文会把整笔改动一起撕掉,像什么都没发生过。这个多出来的保证,正是 no_rollback 那条天书报红的来源:一个撤不回来的操作(比如 Print String),不许进一个随时可能反悔的地方。

关卡挑战

徽章展览闭馆前,来闯三道关卡验收战利品。答错零惩罚,可以一直重试。

一个函数什么访问徽章都没写,它的默认访问级别是?

为什么 <decides> 函数必须用方括号、并且只能在「可能走不通」的地方(比如 Branch)里调用?

想让一个类可以持久化(玩家数据跨局保存),下面哪个写法是对的?

拓展阅读

技巧 · EXTRA

@editable 进阶:ToolTip、Categories 与滑条

用块级元数据把 Verse 设备的细节面板做出接近原生设备的体验:分组、悬浮提示、数值范围滑条。

进入拓展 →

拔高 · EXTRA

效果系统与事务回滚:no_rollback 全解

把那条天书报错拆到原子级:推测执行、事务内存,以及为什么 Print 进不了失败上下文。

进入拓展 →

拓展 · EXTRA

<persistable>:让玩家数据跨局存活

排行榜、货币、通关进度——用 persistable + weak_map 搭出你的第一个存档系统。

进入拓展 →