拓展 · EXTRA
问号的两种身份:?type 与 ?Name
SpawnProp 的返回类型为什么写成 ?creative_prop?把问号的三张面孔一次看清。
SpawnProp 生成道具后交回来的结果为什么写成 ?creative_prop?把问号的三张面孔一次看清。
进入拓展 →手柄上每个键位各管一件事,Verse 的运算符也一样:+ - * / 管算术,= 和 <> 在失败上下文里当裁判,and / or / not 负责组合条件,? 负责查询,:=、set、= 各司其职。这一课把整张键位表配齐,再附一份优先级速查表。
手柄上每个键位各管一件事,Verse 的运算符也一样:+ - * / 管算术,= 和 <> 在 Branch 判断里当裁判(问的不是「真假」,而是「这条线走不走得通」),and / or / not 负责组合条件,? 负责查询,:=、set、= 各司其职。这一课把整张键位表配齐,再附一份优先级速查表。
+ - * / 这四个键没有悬念,int 和 float 都能用。但 Verse 有条铁律:两边必须同款。3 + 1.5 这种 int 混 float 的写法直接编译报错——Verse 不做任何隐式数值转换,该转就得你自己显式转(转换函数的确切写法以官方 Verse API Reference 为准)。这份较真和 set 一脉相承:类型悄悄变来变去,正是别的语言里 bug 的温床。
+ - * / 这四个键没有悬念,int(整数)和 float(小数)都能用。但 Verse 有条铁律:两边必须同款。3 + 1.5 这种整数混小数的写法直接编译报错——蓝图里你把一个 Float 引脚硬拖进 Integer 引脚,UE 会自动塞个转换节点替你圆场;Verse 偏不惯着你,该转就得自己显式转(转换函数的确切写法以官方 Verse API Reference 为准)。这份较真背后是同一个信条:类型在暗处偷偷变来变去,正是最难抓的一类 bug。
真正有脾气的是 int 除法。Coins / 2 藏着两个惊喜:第一,除以 0 会失败——所以 int 除法是可失败表达式,必须住在失败上下文里(还记得上一课的掷骰子吗);第二,两个 int 相除的结果不是 int,而是 rational(有理数),想拿回 int 得用 Floor(向下取整)或 Ceil(向上取整)收个尾。
真正有脾气的是 int(整数)除法。Coins / 2 藏着两个惊喜:第一,除以 0 会走不通——所以整数除法得挂在一个 Branch 判断上,走不通就从 else 引脚溜走(还记得上一课的掷骰子吗);第二,两个整数相除的结果不是整数,而是 rational(有理数),想拿回整数得用 Floor(向下取整)或 Ceil(向上取整)收个尾。
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
wallet_device := class(creative_device):
var Coins:int = 17
var Speed:float = 3.5
OnBegin<override>()<suspends>:void =
set Coins += 3
set Speed *= 2.0
Print("Coins: {Coins}")
# int 除法自带失败可能(除以 0 就失败),必须住在失败上下文里;
# 结果是 rational,用 Floor 转回 int
if (Half := Floor(Coins / 2)): # TODO: 以 Verse API Reference 为准
Print("平分成两份,每份 {Half} 枚")
读图翻译:OnBegin 就是 Event BeginPlay,游戏一开始自动点火。头两行是两个 Set 节点——+= 3 把 Coins 加 3、*= 2.0 把 Speed 翻倍;接着 Print String 把金币数打到屏幕左上角。最后一行是一个 Branch:先把 Coins / 2 向下取整算出 Half,这一步走通了(没除以 0)才从成功引脚进门,再 Print 出每份几枚。
顺带记一笔:+ 不只会算数。string 拼接、array 合并也归它管——set Names += array{"泡泡"} 就是往数组末尾入队一名新玩家。一个键位,多种连招。
顺带记一笔:+ 不只会算数。字符串拼接(蓝图里的 Append 节点)、数组合并(往 Array 里 Add)也归它管——set Names += array{"泡泡"} 就是一个 Set 节点,把「泡泡」入队到数组末尾。一个键位,多种连招。
比较家族六名成员:相等 =、不等 <>、大小 < <= > >=。先把最颠覆的一条说透:它们不返回 bool。上一课说过,Verse 的 if 测试的是成功/失败——而比较运算符正是「可失败表达式」的主力军:比较成立,表达式成功,继续往下走;不成立,表达式失败,走失败的那条路。裁判不举牌子,只放行或拦人。
比较家族六名成员:相等 =、不等 <>、大小 < <= > >=。先把最颠覆的一条说透:它们不吐出一个 Boolean。上一课说过,Verse 的 Branch 判断的不是 True/False,而是「这条线走不走得通」——而比较运算符正是这类判断的主力:比较成立,线就走通,继续往下;不成立,线走不通,拐进 else 那条路。裁判不举牌子,只放行或拦人。
由此推出一条硬规矩:比较只能出现在失败上下文里(if 条件、for 子句、not 内部……)。在普通位置单独写一行 Score = 100,编译器立刻报错——可失败表达式没地方「失败」了。这也是本课头号大坑,一会儿常见坑里还要敲一遍黑板。
由此推出一条硬规矩:比较只能待在「允许走不通」的位置(Branch 的条件、ForEach 的筛选子句、not 内部……)。在普通位置单独写一行 Score = 100,一按 Compile 就亮红报错——这种「可能走不通」的式子没地方安放它的「走不通」。这也是本课头号大坑,一会儿常见坑里还要敲一遍黑板。
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
referee_device := class(creative_device):
var Score:int = 72
var PlayerName:string = "泡泡"
OnBegin<override>()<suspends>:void =
if (Score >= 60):
Print("及格,裁判放行")
else:
Print("没过线,再来一局")
# 相等就是一个 =,不等是 <>(Verse 没有 == 和 !=)
if (PlayerName = "泡泡"):
Print("欢迎回来,泡泡")
if (Score <> 100):
Print("还不是满分,继续练")
读图翻译:这段全是 Branch 节点。第一个 Branch 问「Score 有没有 ≥ 60」,走通了从成功引脚 Print「及格」,走不通就从 else 引脚 Print「没过线」。后面两个 Branch 同理:一个问「名字是不是等于泡泡」(判等只用一个 =),一个问「分数是不是不等于 100」(不等号是一对尖括号 <>)。
还有一条容易忽略的规则:= 只对「可比较」(comparable)的类型开放——int、float、logic、string、char、枚举,以及元素本身可比较的 option / array / map / tuple。普通类的实例默认不可用 = 比较,写了就是编译错误;要么去比较它内部的字段,要么给类标上 <unique>,让它按「是不是同一个」的身份来比。
还有一条容易忽略的规则:= 只对「可比较」(comparable)的类型开放——整数、小数、logic(就是 Boolean)、字符串、字符、枚举(Enumeration 资产),以及装的东西本身可比较的 option / 数组 / map / 元组。普通蓝图实例默认不可用 = 直接比较,硬写就是一按 Compile 就红;要么去比它内部的某个字段,要么给这个类标上 <unique>,让它按「是不是同一个实例」的身份来比(有点像蓝图里比两个对象引用是不是指向同一个 Actor)。
逻辑运算符是三个英文单词:and、or、not——Verse 里没有 &&、||、!。既然比较给出的是成功/失败,这三位组合的也是成功/失败:A and B 两边都成功才算成功;A or B 先试 A,A 失败就回滚它的效果再试 B;not A 则在 A 失败时成功。上一课学过的「if 条件里逗号即 and」在这里对上号了:if (A, B): 和 if (A and B): 是一回事。
逻辑运算符是三个英文单词:and、or、not——正好对上蓝图里的 AND、OR、NOT 布尔节点。既然比较给出的是「走通 / 走不通」,这三位组合出来的也是「走通 / 走不通」:A and B 两条都走通才算走通;A or B 先试 A,A 走不通就把它试连时的改动抹掉、再试 B;not A 则在 A 走不通时反而走通。上一课学过的「Branch 条件里用逗号并列就是 and」在这里对上号了:if (A, B): 和 if (A and B): 是一回事。
not 有个冷知识:它的操作数永远被回滚——不管 not 整体成功还是失败,里面做过的 set 都会被撤销,像没发生过一样。所以别在 not 里偷偷改东西,改了也留不下痕迹。另外,A and B 成功时整个表达式其实有个值(B 的值),A or B 的值是先成功那一边的——但实践中几乎总是只用它们的成功/失败来走分支,别把它们当布尔代数用。
not 有个冷知识:它里面那一段永远被回滚——不管 not 整体走没走通,里面做过的 Set 都会被撤销,像没连过一样。所以别在 not 里偷偷改变量,改了也留不下痕迹。另外,A and B 走通时整个式子其实还带个值(B 的值),A or B 带的是先走通那一边的值——但实践中几乎总是只拿它们的「走通 / 走不通」去决定 Branch 往哪走,别把它们当数学公式来求值。
最后是查询运算符,后缀 ?:跟在 logic 后面是问「你是 true 吗」,跟在 option 后面是问「你有货吗」——是 true / 有货就成功(顺便把货交出来),否则失败。它也是可失败表达式,同样要待在失败上下文里。
最后是查询运算符,后缀 ?:跟在 logic(Boolean)后面是问「你是 true 吗」,跟在 option(那种可能装了东西、也可能是空的盒子)后面是问「你有货吗」——是 true / 有货就走通(顺便把货递出来),否则走不通。它也是那种「可能走不通」的式子,同样得待在 Branch 这类判断位置里。
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
combo_gate_device := class(creative_device):
var HasKey:logic = false
var Score:int = 80
var MaybeBonus:?int = option{50}
OnBegin<override>()<suspends>:void =
# ? 问 logic:「你是 true 吗?」
if (HasKey?):
Print("钥匙已到手")
else:
Print("还没钥匙")
# 组合技:比较 + and + not
if (Score > 60 and not HasKey?):
Print("分数够了,就差一把钥匙")
# ? 问 option:「你有货吗?」有就解包给 Bonus
if (Bonus := MaybeBonus?):
Print("拆到彩蛋:{Bonus} 分")
读图翻译:三个 Branch。第一个用 HasKey? 当条件——问这个 Boolean 是不是 true,是就 Print「钥匙已到手」,否则走 else。第二个把「Score > 60」和「not HasKey?」用 and 串成一个条件,两个都成立才 Print。第三个是拆盒子:MaybeBonus? 把盒子里的值取出来命名成 Bonus,取得到(盒子非空)才进门 Print 出彩蛋分数。
C 系语言里 = 赋值、== 比较,一个手滑就是经典 bug。Verse 的解法是把三件事拆给三个写法,谁也别想冒充谁:
在很多代码语言里,一个 = 既能赋值又能比较,手一滑就是经典 bug(蓝图里倒没这烦恼——Set 变量和比较 Equal 本来就是两个不同的节点)。Verse 索性把三件事拆给三个写法,谁也别想冒充谁:
| 写法 | 身份 | 什么时候用 |
|---|---|---|
Reward:int = 100 |
带类型定义 | 第一次介绍这个名字,类型自己写明 |
Reward := 100 |
类型推断定义 | 第一次介绍这个名字,类型让 Verse 猜 |
set Combo = 3 |
赋值 | 给已存在的 var 换值 |
set Combo += 2 |
复合赋值 | 在原值基础上加减乘除(-=、*=、/= 同理) |
Combo = 5(表达式位置) |
相等比较,可失败可能走不通 | 只能待在失败上下文里只能待在 Branch 这类判断位置里 |
划重点::= 只负责「第一次介绍」,同一个名字再 := 一次就是重复定义,编译器翻脸;改 var 只认 set;表达式位置的 = 一律当比较处理。还要分清 if 里的两副面孔:if (X := Items[0]): 是绑定(右边成功就把值介绍给 X),if (X = 100): 是比较——一个冒号之差,含义完全不同。下面这台连击机把三兄弟凑齐了,点「运行下一步」,亲眼确认每个 = 都在干什么。
划重点::= 只负责「第一次介绍」,同一个名字再 := 一次就是重复定义,一按 Compile 就翻脸报红;改已有变量只认 set(Set 节点);放在普通判断位置的 = 一律当比较处理。还要分清 Branch 里的两副面孔:if (X := Items[0]): 是取值绑定(把数组第 0 个元素取出来、成功就命名给 X,取不到就走 else),if (X = 100): 是比较(问 X 是不是等于 100)——一个冒号之差,含义完全不同。下面这台连击机把三兄弟凑齐了,点「运行下一步」,亲眼确认每个 = 都在干什么。
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
equals_family_device := class(creative_device):
var Combo:int = 0
OnBegin<override>()<suspends>:void =
Reward := 100
set Combo = 3
set Combo += 2
if (Combo = 5):
Print("连击 {Combo} 段,领取 {Reward} 金币")
点「运行下一步」,看代码怎么一行行执行。
多个键位连按,谁先生效?和数学课一样,Verse 的运算符有优先级。下表从高到低排一遍(同档位从左到右结合):
| 档位(高 → 低) | 运算符 | 记忆点 |
|---|---|---|
| 1 | ( ) 分组 |
括号插队,永远最先算 |
| 2 | 后缀 ?、成员访问 . |
贴在名字后面的小尾巴,贴得最紧 |
| 3 | not、一元 - |
单个操作数的前缀 |
| 4 | * / |
先乘除 |
| 5 | + - |
后加减 |
| 6 | = <> < <= > >= |
比较全家一个档位 |
| 7 | and |
比 or 先动手 |
| 8 | or |
压轴最后算 |
两个直接能用的推论:not A and B 解析成 (not A) and B,not 只管住紧随其后的那一个;比较档位高于 and / or,所以 X > 0 and Y > 0 不用加括号也能按直觉走。相邻档位(比如 ? 与 not)的精确次序,以官方《Verse Language Quick Reference》的完整优先级表为准——拿不准就加括号,括号永远不会错。
接下来是本课的排雷手册,每一条都是真实的编译错误现场:
Score = 100,编译器直接拦下——比较是可失败表达式,只能住在 if / for / not 等失败上下文里。想赋值?写 set Score = 100。Score = 100,一按 Compile 就被拦下——比较是「可能走不通」的式子,只能住在 Branch 条件、ForEach 筛选、not 内部这些「允许走不通」的位置。想改值?写 set Score = 100(一个 Set 节点)。== 存在:Verse 的判等就是一个 =,== 不是合法运算符;不等也不是 !=,而是 <>。==、不等写 !=——Verse 都不认。这里判等就一个 =,不等是一对尖括号 <>。Result := A or B 拿到的是「先成功那一边的值」,不是 true / false。组合条件请让它们留在失败上下文里走分支。Result := A or B 拿到的是「先走通那一边的值」,不是 true / false。组合条件请让它们留在 Branch 里决定往哪走。set 不会留下任何痕迹。set(Set 节点)不会留下任何痕迹。X / Y 可失败(除零失败),结果还是 rational 而非 int——要 Floor / Ceil 收尾,并且整个式子要待在失败上下文里。X / Y 可能走不通(除以 0 就走不通),结果还是 rational 而不是整数——要用 Floor / Ceil 收尾,而且整个式子得挂在 Branch 这类判断上。3 + 1.5 编译错误,Verse 不做隐式转换,两边必须同款。3 + 1.5 一按 Compile 就红,Verse 不像蓝图那样自动加转换节点,两边必须同款。<unique>。<unique> 按「同一个实例」来比。set X += 1,二选一写 if (C) then A else B 表达式。想重载运算符?目前也不开放,拓展页有完整的替代方案。++(加一)、条件 ? A : B(二选一)这些简写,Verse 都没有。加一写 set X += 1;二选一写 if (C) then A else B(像蓝图的 Select 节点)。想给自定义类型「重载运算符」?目前也不开放,拓展页有完整的替代方案。光认不练记不牢。下面这道能量闸机被挖掉了三个运算符:改 var 的口令、只有一个等号的相等比较、还有那对「尖括号不等号」。把它们填回去,点「检查答案」。
光认不练记不牢。下面这道能量闸机被挖掉了三个运算符:改变量的口令(Set)、只有一个等号的相等比较、还有那对「尖括号不等号」。把它们填回去,点「检查答案」。
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
energy_gate_device := class(creative_device):
var Energy:int = 10
OnBegin<override>()<suspends>:void =
____ Energy += 5
# 相等比较只有一个等号
if (Energy ____ 15):
Print("能量刚好 15")
# 不等号是一对尖括号
if (Energy ____ 0):
Print("能量不为 0")
键位表配齐了,进训练场实战三回合。答错零惩罚,可以一直重试。
已有 var Coins:int = 5,想把它改成 6,正确写法是?
Verse 里表示「不等于」的运算符是?
not A and B 会被 Verse 解析成哪种?
拓展 · EXTRA
SpawnProp 的返回类型为什么写成 ?creative_prop?把问号的三张面孔一次看清。
SpawnProp 生成道具后交回来的结果为什么写成 ?creative_prop?把问号的三张面孔一次看清。
进入拓展 →技巧 · EXTRA
从一条 transacts no_rollback 报错出发,学会自定义比较器的正确姿势。
从一条 transacts no_rollback 报红出发,学会给自定义类型写比较器的正确姿势。
进入拓展 →拓展 · EXTRA
知名 UE 教育者 Marcos Romero 怎么讲同一套运算符?交叉验证你的理解。
进入拓展 →