技巧 · EXTRA
给 enum 手写 ToString:case 映射模式
enum 不能直接 Print?社区的标准解法只要一个 case 函数,还附赠字符串插值支持。
进入拓展 →背包里那个 logic 格子,这一课翻出来看个仔细:true/false 是「值」,而「成功/失败」是另一回事打没打勾(true/false)是一个「值」,而「这条线走不走得通」是另一回事——这是 Verse 最核心的观念之一。我们还会解锁 enum,给游戏状态挂上专属铭牌,从此告别魔法数字。
讲变量那一课,logic 作为「四款格子」之一露过脸:它是 Verse 的布尔类型,字面量只有 true 和 false 两个它就是蓝图里的 Boolean(布尔)——那种只能打勾或不打勾的开关,能填的值只有 true 和 false 两个,专门记录非黑即白的事实——门开没开、钥匙拿没拿、玩家准备好了没。开格子、当常量、做比较,它都跟 int、string 一样规矩:
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 里,「真假」和「成败」是两码事。很多语言里 if 吃的是布尔值,大家便默认「条件就是 bool」;Verse 偏不。Verse 的 if 吃的是可失败表达式——一段要么成功给出结果、要么失败一无所获的代码。false 是一个值,好端端躺在格子里;「失败」是一次没有结果的求值。俩人只是长得像,户口本上毫无关系。所以 if (MyLogic) 直接编译不过:if 想要「成败」,你却塞给它一个「值」。这是新手撞得最响的一堵墙,也是 Verse 的地基观念——第 8 课我们专门盖这座楼,这一课先学会在墙上开门。
现在敲响本课最重要的黑板:在 Verse 里,「打没打勾」和「这条线走不走得通」是两码事。你在蓝图里连惯的 Branch 节点,吃的是一个 True/False 布尔;Verse 的分叉偏不——它吃的是「这一步能不能走通」。false 是一个值,好端端待在那个没打勾的开关里;而「走不通」是执行线撞了墙、这一步没能跑出结果。俩人只是长得像,其实八竿子打不着。所以你要是像连 Branch 那样,直接把 HasKey 这个开关喂给 if,它当场亮红报错(就是蓝图点 Compile 时亮红那种):分叉要的是「走没走通」,你却塞给它一个「打没打勾的值」。这是新手撞得最响的一堵墙,也是 Verse 的地基观念——第 8 课我们专门盖这座楼,这一课先学会在墙上开门。
想按 logic 的值分岔,办法是给它加一个后缀问号 ?,把「值」现场翻译成「成败」:HasKey? 在 HasKey 为 true 时成功,为 false 时失败。于是 if (HasKey?): 就能编译了。可以把 ? 读成一次质询——「你真的是 true 吗?」——答不上来就算失败。点「运行下一步」,看同一个问号如何走出两种结局:
想按这个开关的状态分出两条线,办法是在它后面加一个问号 ?,把「打没打勾的值」当场变成「这条线走不走得通」:HasKey? 在 HasKey 打了勾(true)时走得通,没打勾(false)时走不通。于是 if (HasKey?): 就相当于一个正常的 Branch,可以正常连了。可以把 ? 读成一次盘问——「你真打勾了吗?」——答不上来就算走不通。点「运行下一步」,看同一个问号如何走出两条不同的线:
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——把一次「成败」封存成可以放进背包的「值」。另外,if 在 Verse 里本身就是表达式,所以 if (条件) then true else false 也能当场造出一个 logic:
旋转门是双向的。反着走的写法是 logic{…}:把大括号里那段「走不走得通」的判断跑一遍,走通了得 true、走不通得 false——等于把一次「线通不通」的结果封存成一个能存进变量的开关值。另外,Verse 里 if 本身也会「吐出一个值」,所以 if (条件) then true else false 也能当场造出一个 logic 值(有点像蓝图里的 Select 节点,按条件挑一个值输出):
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。
最后看结尾两行:组合条件用 and、or、not 这三个单词——Verse 没有 &&、||、!。而且它们组合的同样是可失败表达式,所以 logic 值要先各自带上 ?,再用 and 串起来。写 IsReady and HasKey(不带问号)会吃编译错误。
最后看结尾两行:把多个条件并起来,用 and、or、not 这三个单词就行(它们就是蓝图里的 AND / OR / NOT 节点)。要留神的是:它们串的也是「走不走得通」的线,所以每个 logic 开关都得先自己加上 ? 变成线,再用 and 接起来。写成 IsReady and HasKey(没加问号)Compile 会亮红报错。
换个场景。你的地图有三个阶段:大厅等人、战斗进行、颁奖谢幕。用 int 记(0、1、2)?三个月后没人记得 2 是什么意思,「魔法数字」是祖传大坑。用 string 记("Lobby")?手一抖打成 "Loby",编译器不闻不问,bug 直接上线。enum(枚举)专治这两个毛病:它创造一个全新的类型,值只能从你点名的那几位里选,拼错名字编译器当场翻脸。这时就轮到 enum(枚举)出场了——就是你在蓝图里右键新建的那个 Enumeration 资产:它造出一份专属的下拉清单,值只能从你点名的那几个里选,名字拼错编译时当场亮红。
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},位置在模块层——想在函数体里临时定义一个 enum?编译器拒收。命名沿用 Verse 惯例:类型名 lower_snake_case,成员名 PascalCase。取值必须报全名:game_phase.Lobby,带着类型前缀,永远不会跟别的枚举撞名。
三个要点:定义写成 game_phase := enum{Lobby, Battle, Podium},而且它得待在最外层——跟设备蓝图平级,就像 Enumeration 是一份独立资产,你不会把它建在某个事件图里头;真写进函数或事件里,编译时会被打回。命名照 Verse 的规矩来:类型名用 lower_snake_case,条目名用 PascalCase。用到某个值必须带全名 game_phase.Lobby(带上枚举名前缀),这样永远不会跟别的枚举里的同名条目搞混。
enum 的能力被刻意收窄:只有 = 和 <> 两种比较;不能比大小、不能加减;成员背后没有整数,不能与 int 或 string 互转;Print(CurrentPhase) 也不认识它。想按成员分岔,除了 if 链,还有一个专门的 case 表达式,先睹为快:
enum 的本事被刻意收窄:只能用 = 和 <> 判断「是不是这一项」;不能比大小、不能加减;每个条目背后没有藏数字,也不能自动变成整数或一段文字;想用 Print String 直接把它打到屏幕上?打不出来。要按当前是哪个条目分线,除了一串 Branch,还有一个专门的 case 写法(像一个多出口的分流器,按条目一次挑一条),先睹为快:
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 写全所有成员就不需要「兜底」分支;将来你给 enum 添了新成员而忘了改 case,编译器还会点名提醒——这张安全网是 enum 最值钱的福利之一。想知道 closed 是什么、还能不能 open,文末拓展页里细聊。先做个小练习,把两个关键空格填回去:
对默认(closed)的 enum,只要 case 把所有条目都写全,就不用留「兜底」出口(就像蓝图里 Switch on Enum 那个 Default 引脚,可以不接);更妙的是,将来你给枚举加了新条目却忘了更新 case,Compile 时会直接点名提醒你漏了谁——这张安全网是 enum 最值钱的福利之一。想知道 closed 是什么、还能不能改成 open,文末拓展页里细聊。先做个小练习,把两个关键空格填回去:
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。Team:team = team.Red 比 IsRedTeam:logic 可读得多,而且将来加个「观战者」阵营只需添一个成员。
还有一个进阶理由:用多个 logic 拼状态,会拼出「鬼状态」。想给门做「开着 / 关着 / 锁着」三种状态,如果用 IsOpen 加 IsLocked 两个 logic,组合出四种情况——其中「又开着又锁着」物理上不存在,却能在代码里堂而皇之地出现,等着某天半夜坑你。改成 door_state := enum{Open, Closed, Locked},非法状态从类型上就无法被表示。「让非法状态无法表示」是类型设计的黄金准则,enum 是它最便宜的实现。
| 场景 | 推荐 | 理由 |
|---|---|---|
| 拿没拿钥匙、准备好了没 | logic |
天然的是/否,两种取值封顶 |
| 游戏阶段:大厅/战斗/颁奖 | enum |
三种以上状态,名字自带文档 |
| 阵营:红队/蓝队 | enum |
语义是「甲/乙」而非「是/否」,且便于扩展 |
| 门:开/关/锁 | enum |
两个 logic 会拼出「又开又锁」的鬼状态 |
这一课的坑,几乎全都长在「真假 ≠ 成败」这条裂缝上。对号入座,提前免疫:
这一课的坑,几乎全长在「打没打勾的值 ≠ 线走不走得通」这条裂缝上。对号入座,提前免疫:
if (HasKey): —— 本课最高频错误,没有之一。编译器会抱怨这里需要的是可失败表达式。修法:补上问号,if (HasKey?):。if (HasKey): —— 本课最高频错误,没有之一。Compile 会亮红,抱怨这里要的是「走不走得通」,不是一个现成的开关值。修法:补上问号,if (HasKey?):。IsOpen && IsLocked —— Verse 没有 &&、||、!,请写 and、or、not;而且 logic 值要先各自加 ?,再组合:IsOpen? and IsLocked?。IsOpen and IsLocked(两个开关直接用 and 串)—— 组合条件请用 and、or、not(蓝图里的 AND / OR / NOT 节点);而且每个 logic 开关都得先自己加 ? 变成线,再组合:IsOpen? and IsLocked?。if (HasKey = true): —— 能编译,但绕了远路;惯用写法是 if (HasKey?):,官方文档和风格指南都这么写。Print(CurrentPhase) 或 "{CurrentPhase}" —— enum 没有内置的文字形态,直接打印编译不过;标准解法是手写一个 case 映射函数,拓展页有完整方案。Print(CurrentPhase) 或把枚举塞进 "{CurrentPhase}" —— 枚举没有自带的文字形态,想用 Print String 直接打出来会亮红;标准解法是手写一个 case 映射函数,把每个条目翻成一段字,拓展页有完整方案。game_phase.Lobby 等于 0 —— enum 成员背后没有整数,enum 与 int 之间没有任何原生互转,别按 C 语言的直觉来。game_phase.Lobby 等于 0 —— 枚举条目背后没有藏数字,也不能自动转成整数;它就是一块铭牌,不是一个编号。set B = true / set B = false —— 三行变一行:B := logic{条件},又短又不用 var。B 设成 true / false —— 其实三步并一步:B := logic{条件},直接把「走不走得通」封成开关,又短又不用额外开变量。硬币和铭牌都到手了,闯三道小关卡验收。答错零惩罚,可以一直重试。
HasKey 是一个 logic 变量,想在它为 true 时开门。哪种写法既能编译通过、又是官方推荐的惯用写法?
关于 enum,下面哪个说法是对的?
Ok := logic{Score > 100},当 Score 为 50 时,Ok 里装的是什么?