技巧 · EXTRA
给 enum 手写 ToString:case 映射模式
enum 不能直接 Print?社区的标准解法只要一个 case 函数,还附赠字符串插值支持。
进入拓展 →蓝图的 Branch 节点吃的是一个 True/False 布尔;Verse 的 if 偏不——它吃的是「这条线走不走得通」。背包里那个 logic 格子,这一课翻出来看个仔细:打没打勾(true/false)是一个「值」,而「这条线走不走得通」是另一回事——这是 Verse 最核心的观念之一。我们还会解锁 enum,给游戏状态挂上专属铭牌,从此告别魔法数字。
讲变量那一课,logic 作为「四款格子」之一露过脸:它就是蓝图里的 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 里,「打没打勾」和「这条线走不走得通」是两码事。你在蓝图里连惯的 Branch 节点,吃的是一个 True/False 布尔;Verse 的分叉偏不——它吃的是「这一步能不能走通」。false 是一个值,好端端待在那个没打勾的开关里;而「走不通」是执行线撞了墙、这一步没能跑出结果。俩人只是长得像,其实八竿子打不着。所以你要是像连 Branch 那样,直接把 HasKey 这个开关喂给 if,它当场亮红报错(就是蓝图点 Compile 时亮红那种):分叉要的是「走没走通」,你却塞给它一个「打没打勾的值」。这是新手撞得最响的一堵墙,也是 Verse 的地基观念——第 12 课我们专门盖这座楼,这一课先学会在墙上开门。
想按这个开关的状态分出两条线,办法是在它后面加一个问号 ?,把「打没打勾的值」当场变成「这条线走不走得通」: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——等于把一次「线通不通」的结果封存成一个能存进变量的开关值。另外,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 这三个单词就行(它们就是蓝图里的 AND / OR / NOT 节点)。要留神的是:它们串的也是「走不走得通」的线,所以每个 logic 开关都得先自己加上 ? 变成线,再用 and 接起来。写成 IsReady and HasKey(没加问号)Compile 会亮红报错。
换个场景。你的地图有三个阶段:大厅等人、战斗进行、颁奖谢幕。用 int 记(0、1、2)?三个月后没人记得 2 是什么意思,「魔法数字」是祖传大坑。用 string 记("Lobby")?手一抖打成 "Loby",编译器不闻不问,bug 直接上线。这时就轮到 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},而且它得待在最外层——跟设备蓝图平级,就像 Enumeration 是一份独立资产,你不会把它建在某个事件图里头;真写进函数或事件里,编译时会被打回。命名照 Verse 的规矩来:类型名用 lower_snake_case,条目名用 PascalCase。用到某个值必须带全名 game_phase.Lobby(带上枚举名前缀),这样永远不会跟别的枚举里的同名条目搞混。
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 把所有条目都写全,就不用留「兜底」出口(就像蓝图里 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): —— 本课最高频错误,没有之一。Compile 会亮红,抱怨这里要的是「走不走得通」,不是一个现成的开关值。修法:补上问号,if (HasKey?):。IsOpen and IsLocked(两个开关直接用 and 串)—— 组合条件请用 and、or、not(蓝图里的 AND / OR / NOT 节点);而且每个 logic 开关都得先自己加 ? 变成线,再组合:IsOpen? and IsLocked?。if (HasKey = true): —— 能编译,但绕了远路;惯用写法是 if (HasKey?):,官方文档和风格指南都这么写。Print(CurrentPhase) 或把枚举塞进 "{CurrentPhase}" —— 枚举没有自带的文字形态,想用 Print String 直接打出来会亮红;标准解法是手写一个 case 映射函数,把每个条目翻成一段字,拓展页有完整方案。game_phase.Lobby 等于 0 —— 枚举条目背后没有藏数字,也不能自动转成整数;它就是一块铭牌,不是一个编号。B 设成 true / false —— 其实三步并一步:B := 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 里装的是什么?