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

logic 与 enum:两面硬币与状态铭牌

蓝图的 Branch 节点吃的是一个 True/False 布尔;Verse 的 if 偏不——它吃的是「这条线走不走得通」。背包里那个 logic 格子,这一课翻出来看个仔细:打没打勾(true/false)是一个「值」,而「这条线走不走得通」是另一回事——这是 Verse 最核心的观念之一。我们还会解锁 enum,给游戏状态挂上专属铭牌,从此告别魔法数字。

一、logic:一枚只有两面的硬币

讲变量那一课,logic 作为「四款格子」之一露过脸:它就是蓝图里的 Boolean(布尔)——那种只能打勾或不打勾的开关,能填的值只有 truefalse 两个,专门记录非黑即白的事实——门开没开、钥匙拿没拿、玩家准备好了没。开格子、当常量、做比较,它都跟 int、string 一样规矩:

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

switch_room_device := class(creative_device):

    # logic 格子:只装 true 或 false
    var IsDoorOpen:logic = false
    var HasKey:logic = true

    # 不写 var 就是常量:这盏灯永远亮着
    LightAlwaysOn:logic = true

▸ 读图翻译:这段相当于在变量面板里新建两个 Boolean 变量——IsDoorOpen(默认不打勾)和 HasKey(默认打勾);最后那个 LightAlwaysOn 没写 var,相当于一个不给 Set 节点、锁死为 true 的常量。

比较两个 logic 用 =<>,例如 if (A = B):。注意:这两个比较不是在算一个 true/false 塞给你,而是在问「这条线能不能走通」——走得通就往下走,走不通这一步就没有结果。这种「能不能走通」的判断,官方名叫「可失败表达式」,下一课的主角就是它,先把名字记在小本本上。

现在敲响本课最重要的黑板:在 Verse 里,「打没打勾」和「这条线走不走得通」是两码事。你在蓝图里连惯的 Branch 节点,吃的是一个 True/False 布尔;Verse 的分叉偏不——它吃的是「这一步能不能走通」。false 是一个值,好端端待在那个没打勾的开关里;而「走不通」是执行线撞了墙、这一步没能跑出结果。俩人只是长得像,其实八竿子打不着。所以你要是像连 Branch 那样,直接把 HasKey 这个开关喂给 if,它当场亮红报错(就是蓝图点 Compile 时亮红那种):分叉要的是「走没走通」,你却塞给它一个「打没打勾的值」。这是新手撞得最响的一堵墙,也是 Verse 的地基观念——第 12 课我们专门盖这座楼,这一课先学会在墙上开门。

二、问号 ?:真假与成败之间的旋转门

想按这个开关的状态分出两条线,办法是在它后面加一个问号 ?,把「打没打勾的值」当场变成「这条线走不走得通」:HasKey?HasKey 打了勾(true)时走得通,没打勾(false)时走不通。于是 if (HasKey?): 就相当于一个普通的 Branch,可以正常连了。可以把 ? 读成一次盘问——「你真打勾了吗?」——答不上来就算走不通。点「运行下一步」,看同一个问号如何走出两条不同的线:

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

key_gate_device := class(creative_device):

    # logic 格子:钥匙拿没拿
    var HasKey:logic = false

    OnBegin<override>()<suspends>:void =
        if (HasKey?):
            Print("咔哒——门开了!")
        else:
            Print("门纹丝不动……先去找钥匙吧。")
        set HasKey = true
        if (HasKey?):
            Print("咔哒——门开了!")
输出日志

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

旋转门是双向的。反着走的写法是 logic{…}:把大括号里那段「走不走得通」的判断跑一遍,走通了得 true、走不通得 false——等于把一次「线通不通」的结果封存成一个能存进变量的开关值。另外,Verse 里 if 本身也会「吐出一个值」,所以 if (条件) then true else false 也能当场造出一个 logic 值(有点像蓝图里的 Select 节点,按条件挑一个值输出):

coin_device.verse
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /Verse.org/Random }

coin_device := class(creative_device):

    var IsReady:logic = true
    var HasKey:logic = true

    OnBegin<override>()<suspends>:void =
        # logic{}:把「成败」翻译成 true/false 存起来
        CoinFlip := logic{GetRandomInt(0, 1) = 1}
        if (CoinFlip?):
            Print("硬币:正面!")

        # if 也是表达式,同样能造出 logic
        Score := 150
        HasWon := if (Score > 100) then true else false
        if (HasWon?):
            Print("战绩达标!")

        # 组合多个 logic:先各自加 ?,再用 and 串起来
        if (IsReady? and HasKey?):
            Print("万事俱备,出发!")

▸ 读图翻译:第一段把「随机数等于 1 吗」这条判断塞进 logic{},封成开关 CoinFlip,再用 ? 展开成一条线——正面就打印。第二段用「按条件挑值」的写法(≈蓝图 Select 节点)算出 HasWon 开关,达标就打印。最后一段把两个开关各自加 ? 展开成线,用 and 串成「两条都得走通」——相当于蓝图里两个判断先进一个 AND 节点、再进 Branch。

最后看结尾两行:把多个条件并起来,用 andornot 这三个单词就行(它们就是蓝图里的 AND / OR / NOT 节点)。要留神的是:它们串的也是「走不走得通」的线,所以每个 logic 开关都得先自己加上 ? 变成线,再用 and 接起来。写成 IsReady and HasKey(没加问号)Compile 会亮红报错。

三、enum:给状态挂铭牌

换个场景。你的地图有三个阶段:大厅等人、战斗进行、颁奖谢幕。用 int 记(0、1、2)?三个月后没人记得 2 是什么意思,「魔法数字」是祖传大坑。用 string 记("Lobby")?手一抖打成 "Loby",编译器不闻不问,bug 直接上线。这时就轮到 enum(枚举)出场了——就是你在蓝图里右键新建的那个 Enumeration 资产:它造出一份专属的下拉清单,值只能从你点名的那几个里选,名字拼错编译时当场亮红。

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

# enum 定义在模块层,与设备类平起平坐,不能塞进函数体
game_phase := enum{Lobby, Battle, Podium}

phase_device := class(creative_device):

    # 用 enum 类型开格子,初始停在大厅
    var CurrentPhase:game_phase = game_phase.Lobby

    OnBegin<override>()<suspends>:void =
        set CurrentPhase = game_phase.Battle

        # enum 只支持 = 和 <> 两种比较(都是可失败表达式)
        if (CurrentPhase = game_phase.Battle):
            Print("战斗阶段,全员集合!")
        if (CurrentPhase <> game_phase.Podium):
            Print("还没到领奖时间。")

▸ 读图翻译:第一行相当于右键新建了一个名叫 game_phase 的 Enumeration 资产,里面填了 Lobby、Battle、Podium 三个条目。设备里开一个类型是这个枚举的变量 CurrentPhase,默认停在 Lobby。开局(Event BeginPlay)用 Set 节点把它切到 Battle,再用两个 Branch 分别问「现在等于 Battle 吗」「现在不等于 Podium 吗」——注意枚举只认「等于 / 不等于」两种比较。

三个要点:定义写成 game_phase := enum{Lobby, Battle, Podium},而且它得待在最外层——跟设备蓝图平级,就像 Enumeration 是一份独立资产,你不会把它建在某个事件图里头;真写进函数或事件里,编译时会被打回。命名照 Verse 的规矩来:类型名用 lower_snake_case,条目名用 PascalCase。用到某个值必须带全名 game_phase.Lobby(带上枚举名前缀),这样永远不会跟别的枚举里的同名条目搞混。

enum 的本事被刻意收窄:只能用 =<> 判断「是不是这一项」;不能比大小、不能加减;每个条目背后没有藏数字,也不能自动变成整数或一段文字;想用 Print String 直接把它打到屏幕上?打不出来。要按当前是哪个条目分线,除了一串 Branch,还有一个专门的 case 写法(像一个多出口的分流器,按条目一次挑一条),先睹为快:

phase_label.verse
game_phase := enum{Lobby, Battle, Podium}

# case 表达式:挨个核对成员,命中哪个走哪支
PhaseLabel(Phase:game_phase):string =
    case (Phase):
        game_phase.Lobby => "大厅"
        game_phase.Battle => "战斗"
        game_phase.Podium => "颁奖台"

▸ 读图翻译:这个 PhaseLabel 是一个蓝图函数(输入一个 game_phase 条目,返回一段文字)。里头的 case 就是那个多出口分流器:传进来的条目是 Lobby 就吐「大厅」,是 Battle 就吐「战斗」,是 Podium 就吐「颁奖台」——每个出口对应一条线,一次只走中的那一条。

对默认(closed)的 enum,只要 case 把所有条目都写全,就不用留「兜底」出口(就像蓝图里 Switch on Enum 那个 Default 引脚,可以不接);更妙的是,将来你给枚举加了新条目却忘了更新 case,Compile 时会直接点名提醒你漏了谁——这张安全网是 enum 最值钱的福利之一。想知道 closed 是什么、还能不能改成 open,文末拓展页里细聊。先做个小练习,把两个关键空格填回去:

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

# 挖空 1:定义枚举用的关键字
game_phase := ____{Lobby, Battle, Podium}

ready_check_device := class(creative_device):

    var CurrentPhase:game_phase = game_phase.Lobby
    var IsReady:logic = true

    OnBegin<override>()<suspends>:void =
        set CurrentPhase = game_phase.Battle
        if (CurrentPhase = game_phase.Battle):
            Print("进入战斗阶段!")
        # 挖空 2:把 logic 递给 if 要加的符号
        if (IsReady____):
            Print("玩家已就位。")

▸ 读图翻译:上面就是把前面两样拼在一起——先造出 game_phase 枚举(空 1 填造枚举用的那个关键字),开局切到 Battle、用 Branch 判断阶段;最后那个 IsReady 开关,得先加上把它变成一条线的那个符号(空 2),才能喂给 if

四、选型指南:什么时候 logic,什么时候 enum

两条经验法则,足够覆盖日常九成场景:

其一,问题的答案天然是「是/否」,而且永远只有这两种 → 用 logic。拿没拿钥匙、开没开门、活着没,都是这一类。其二,状态有三种以上;或者虽然只有两种,但语义不是「是/否」而是「甲/乙」(比如红队/蓝队)→ 用 enum。Team:team = team.RedIsRedTeam:logic 可读得多,而且将来加个「观战者」阵营只需添一个成员。

还有一个进阶理由:用多个 logic 拼状态,会拼出「鬼状态」。想给门做「开着 / 关着 / 锁着」三种状态,如果用 IsOpenIsLocked 两个 logic,组合出四种情况——其中「又开着又锁着」物理上不存在,却能在代码里堂而皇之地出现,等着某天半夜坑你。改成 door_state := enum{Open, Closed, Locked},非法状态从类型上就无法被表示。「让非法状态无法表示」是类型设计的黄金准则,enum 是它最便宜的实现。

场景 推荐 理由
拿没拿钥匙、准备好了没 logic 天然的是/否,两种取值封顶
游戏阶段:大厅/战斗/颁奖 enum 三种以上状态,名字自带文档
阵营:红队/蓝队 enum 语义是「甲/乙」而非「是/否」,且便于扩展
门:开/关/锁 enum 两个 logic 会拼出「又开又锁」的鬼状态

五、常见坑:先替你踩一遍

这一课的坑,几乎全长在「打没打勾的值 ≠ 线走不走得通」这条裂缝上。对号入座,提前免疫:

蓝图对照

这一课的每个概念在蓝图里都有对应物,但对应得最不整齐的一对——Branch 与 if——恰恰是全课的地基。

蓝图里的做法 Verse 里的写法 差异
变量面板新建一个 Boolean(那个勾选框) var IsDoorOpen:logic = false 只是换了名字:蓝图叫 Boolean,Verse 叫 logic,取值同样只有 true / false
Branch 节点:吃一个 True/False,分出两条执行线 if (HasKey?): 最大的一处不对应。Verse 的 if 吃的是「走不走得通」,logic 值必须加 ? 才进得去
AND / OR / NOT 节点 and / or / not 它们串的也是「走不走得通」,所以每个 logic 都得先各自加 ?,再串起来
Select 节点:按条件挑一个值输出 if (Score > 100) then true else false Verse 里 if 本身就是表达式,不用换一个节点就能吐出一个值
Branch 的两条线各接一个 Set,把某个 Boolean 设成 true / false B := logic{条件} 三步并一步:把一次「走没走通」直接封成开关值,连变量都不用先开
右键新建一份 Enumeration 资产 game_phase := enum{Lobby, Battle, Podium} 位置也对应:枚举得写在最外层、跟设备类平级,像独立资产一样,不能塞进函数体
Switch on Enum 节点 case (Phase): 条目写全就不用接 Default 引脚;将来给枚举加了条目却忘了改 case,Compile 会直接点名
枚举成员背后的整数序号,以及自动的枚举名字符串 两样都没有 Verse 的 enum 不能转 int、不能直接 Print;要文字得自己写一个 case 映射函数

差异集中在第二行,而它的根子在语言内核:蓝图的执行线只有「走」这一种状态,所以分叉必须由一个外来的布尔值来指挥,Branch 才需要一个 True/False 输入。Verse 的每一步表达式本身就带着「走通 / 走不通」两种结局,分叉是这个结局的直接后果,不需要谁来指挥。logic 在这个体系里只是一个普通的数据类型——和 int、string 一样,是拿来的,不是拿来指挥的。?logic{} 就是这两个世界之间的两道旋转门。

enum 那几行则是另一种落差:蓝图的 Enumeration 骨子里是一个带名字的整数,所以它能转 int、能打印、能比大小。Verse 的 enum 什么都不是——它只是一块铭牌,只能回答「是不是这一项」。能力被砍掉的部分,换来的是 case 的穷尽性检查:漏了一个条目,编译期就点名,而不是等某天玩家撞出一个没人处理的分支。

六、关卡挑战

硬币和铭牌都到手了,闯三道小关卡验收。答错零惩罚,可以一直重试。

HasKey 是一个 logic 变量,想在它为 true 时开门。哪种写法既能编译通过、又是官方推荐的惯用写法?

关于 enum,下面哪个说法是对的?

Ok := logic{Score > 100},当 Score 为 50 时,Ok 里装的是什么?

拓展阅读

技巧 · EXTRA

给 enum 手写 ToString:case 映射模式

enum 不能直接 Print?社区的标准解法只要一个 case 函数,还附赠字符串插值支持。

进入拓展 →

拓展 · EXTRA

open 与 closed enum:发布之后还能不能改

项目上线后 enum 还能加成员吗?版本兼容性如何写进类型系统,长期运营的项目必读。

进入拓展 →

拔高 · EXTRA

Verse Calculus:失败为什么不是布尔值

「if 不吃 logic」的理论答案,藏在 Epic 的 ICFP 2023 论文里——一窥 Verse 的语言学内核。

进入拓展 →