Verse Wiki — 写给 UE6 时代的 Verse 互动手册
第二章 · 第 5 课

数字:int 与 float——两款不许混装的格子

金币数量是 int,冷却秒数是 float——在 Verse 眼里,这是两款完全不同的格子,绝不混装。这一课讲透数字:四则运算、类型转换的惯用姿势、整数除法为什么要住进 if整数除法为什么非得接在 Branch 节点(if)上,顺带认识一位叫 NaN 的怪客。

一、int:数得清的都归它

上一课你已经会开格子(var)、换东西(set)了。这一课看看格子里最常住的两位居民——数字。先说 int:整数格子,金币数量、击杀数、剩余弹药,这些「一个一个数得清」的量全归它管。字面量可以写十进制,也可以写 0x 前缀的十六进制:0xFF 就是 255。给它填值时可以用十进制,也可以用 0x 打头的十六进制:0xFF 就是 255。

加、减、乘对 int 随便用,配合上一课的 set 口令连招:

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

gold_vault_device := class(creative_device):

    # 十进制、十六进制,写出来都是 int
    var Gold:int = 250
    ColorMask:int = 0xFF

    OnBegin<override>()<suspends>:void =
        set Gold += 50
        set Gold *= 2
        Print("Gold: {Gold}")

读图翻译:这段代码等于新建一个叫 gold_vault_device 的蓝图类(以 creative_device 为父类,做好后能拖进关卡)。里面开了两个变量——Gold(int,默认 250)是普通变量;ColorMask 没写 var,是个锁死、不给 Set 节点改的常量。OnBegin 就是 Event BeginPlay:游戏一开局,先用一个 Set 节点把 Gold 加 50,再用一个 Set 节点把它乘 2,最后接 Print String 把结果打到屏幕左上角。

两步连招跑完,(250 + 50) × 2,日志报出 Gold: 600。眼尖的同学会发现除法 / 没有出场——它在 Verse 里是个特殊角色,特殊到要单开一节(第四节见)。

int 能装多大的数?官方文档给了准话:目前的实现是 64 位有符号整数,范围从 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807——大约正负 922 亿亿,装下全服金币总和绰绰有余。但要是运算结果真越了界,Verse 不会像 C 那样悄悄「绕回去」变成一个负数不会像某些老语言那样悄悄「绕一圈」变回一个负数,而是直接抛出运行时错误——宁可当场翻车,也不给你一个错得离谱还面不改色的数。文档同时预告:未来版本中 int 在语义上会升级为任意大小的整数,届时连这个上限都没有了。

再补一条世界观设定:所有比较运算符(<<=>>==<>)在 Verse 里都是「可失败表达式」——比较成立就成功并交出值,不成立就失败。所以它们总是住在 if 这样的失败上下文里,比如 if (Gold >= 600):。这个设定贯穿全书,本课后面马上还会用到。

再补一条世界观设定:所有比较(<<=>>==<>)在 Verse 里问的都是「这条线能不能走通」——比较成立,线就走得通、并交出比较的值;不成立,这条线就走不通。所以它们总要接在一个 Branch 节点(if)上;Verse 的 Branch 问的不是 True/False,而是「能不能走通」,走不通就改走 else 引脚,比如 if (Gold >= 600):。这个设定贯穿全书,本课后面马上还会用到。

二、float:带小数点的世界,和一位叫 NaN 的怪客

float 是小数格子:移动速度、冷却秒数、伤害倍率。它是 IEEE 64 位浮点数(相当于其他语言的 double 档位),但写法上有条铁律:float 字面量必须带小数点。想表达「一点零」就写 1.0;写 1 的话那是 int,两者在 Verse 眼里毫无瓜葛。负数用前缀减号:-3.2

float 是小数格子:移动速度、冷却秒数、伤害倍率——蓝图里那些浅绿色的 float 引脚就是它。它精度很高(IEEE 64 位浮点),但写法上有条铁律:写 float 的值时必须带上小数点。想表达「一点零」就写 1.0;写成 1 那是 int,两者在 Verse 眼里毫无瓜葛。负数用前缀减号:-3.2

cooldown_device.verse
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /Verse.org/Simulation }

cooldown_device := class(creative_device):

    var Cooldown:float = 4.5

    OnBegin<override>()<suspends>:void =
        set Cooldown *= 2.0
        set Cooldown /= 3.0
        Print("Cooldown: {Cooldown}")
        Sleep(Cooldown)
        Print("技能就绪")

读图翻译:一个变量 Cooldown(float,默认 4.5)。Event BeginPlay 一跑:Set 节点把它乘 2.0,再一个 Set 节点除以 3.0,Print String 打出当前值,然后接一个 Delay 节点——Sleep(Cooldown) 就是「等 Cooldown 秒」;这也是签名带 <suspends> 的原因:函数体里挂了个带小钟表的节点(latent),调用它的线得能停下来等。等够了,再 Print String 报「技能就绪」。

4.5 × 2.0 ÷ 3.0 = 3.0,于是 Sleep(Cooldown) 让设备睡三秒——注意 Sleep 的参数类型就是 float,传 int 会直接编译报错。另外看日志:打印出来的是 Cooldown: 3.000000。float 转字符串固定带六位小数,做 UI 文本时这串尾巴零经常吓人一跳(拓展页里有 Epic 员工给出的「保留 N 位小数」方案)。

float 的除法脾气和 int 完全不同:不是可失败运算符,除以 0.0 也不会失败。不是那种「可能走不通」的运算,除以 0.0 也不会让这条线断掉。至于 1.0 / 0.0 到底产出什么值,官方文档没把话说死,严谨起见以编辑器实测和 Verse API Reference 为准。

float 家还住着一位怪客:NaN(Not a Number,非数)。Verse 给它立了三条家规,而且条条故意偏离 IEEE-754 标准:全语言只有一个 NaN;NaN = NaN 成立——NaN 等于它自己;NaN 参与比较,并且比所有其他 float 都大。这三条让 Verse 的 float 拥有「全序」——任何两个 float 都能分出大小,排序永远不会出现未定义行为排序结果永远是确定的,不会时好时坏。代价是:如果一堆数据里混进了 NaN,Max 会把它当最大值捞出来。这位怪客的完整档案,见本课拓展页。

三、纪律:不混装,要转换就明着来

敲黑板,本课最重要的纪律:int 和 float 绝不混用,也绝不自动转换1 + 1.0 在很多语言里等于 2.0,在 Verse 里等于一条编译错误。参数同理:Sleep(5) 过不了编译,必须写 Sleep(5.0)

敲黑板,本课最重要的纪律:int 和 float 绝不混用,也绝不自动转换。这条蓝图老手要特别留神——蓝图里你把一个 int 引脚拖到 float 引脚上,它会悄悄替你插一个转换节点;Verse 偏偏不给这个方便:1 + 1.0 直接是一条编译错误(Compile 会亮红),Sleep(5) 也过不了编译,必须写 Sleep(5.0)

mixing_types.verse
# 下面两行都过不了编译:int 和 float 互不相让
Broken1 := 1 + 1.0
Sleep(5)

# 正确姿势:同类型运算,参数按声明的类型给
Fine := 1.0 + 1.0
Sleep(5.0)

那需要转换时怎么办?两个方向,两种姿势:

int → float:乘 1.0。惯用写法是 KillsAsFloat := Kills * 1.0。这不是魔法,而是因为乘号 * 恰好提供了「int × float → float」的重载。Verse 没有内置的 Float()/ToFloat 转换函数,乘 1.0 就是官方口径的标准桥。

int → float:乘 1.0。惯用写法是 KillsAsFloat := Kills * 1.0。这不是魔法——乘号 * 本身就允许「一边整数、一边小数」这样相乘,算出来是 float。Verse 里没有一个现成的「转成 float」节点或函数,乘 1.0 就是官方认可的标准桥。

float → int:可失败的取整函数。Floor[X] 向下取整、Ceil[X] 向上取整、Round[X] 就近取整,注意它们用方括号调用——方括号在 Verse 里意味着「这个调用可能失败」:当 X 是 NaN、或者大到 int 装不下时,取整就会失败。所以它们也得住进 if:if (N := Floor[3.7]):

float → int:可能走不通的取整函数。Floor[X] 向下取整、Ceil[X] 向上取整、Round[X] 就近取整,注意它们用方括号调用——在 Verse 里,方括号就是在提醒你「这一步可能走不通」:当 X 是 NaN、或者大到 int 装不下时,取整就会失败、这条线走不通。所以它们也得接在 Branch 节点(if)上:if (N := Floor[3.7]):

光看不练假把式。下面这台「换算器」被挖掉了两处关键零件:一处是 int 变 float 的那座桥,一处是「向下取整」的可失败函数「向下取整」那个可能走不通的函数。填回去,点「检查答案」。

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

convert_device := class(creative_device):

    Kills:int = 7

    OnBegin<override>()<suspends>:void =
        # int → float:乘以一个特殊的小数
        KillsAsFloat := Kills * ____
        # float → int:「向下取整」的可失败函数,方括号调用
        if (Half := ____[KillsAsFloat / 2.0]):
            Print("半程战果:{Half}")

四、整数除法:/ 为什么住在 if 里

现在轮到被雪藏的除法登场。int 的 / 有两个惊人设定。第一,它可能失败——除数为 0 的时候。所以 X / 2 不能随手写在普通代码里,必须放进 if 这样的失败上下文,否则编译器直接打回。第二,它的结果既不是 int 也不是 float,而是第三种数字类型 rational(有理数):5 / 2 得到的是精确的分数 5/2,一丝精度都不丢。想拿回 int?你必须亲口回答「余数往哪边舍」:Floor(...) 向下(负无穷方向),Ceil(...) 向上。C 语言那种 5 / 2 == 2 的静默截断,在 Verse 里写作 Floor(5 / 2)——舍入方向白纸黑字,谁也别想蒙混过关。

现在轮到被雪藏的除法登场。int 的 / 有两个惊人设定。第一,它可能失败——除数为 0 的时候。所以 X / 2 不能随手连在普通执行线上,必须接到一个 Branch 节点(if)上、让它先检查「能不能走通」,否则 Compile 直接亮红打回。第二,它的结果既不是 int 也不是 float,而是第三种数字类型 rational(有理数):5 / 2 得到的是精确的分数 5/2,一丝精度都不丢。想拿回 int?你必须亲口回答「余数往哪边舍」:Floor(...) 向下(负无穷方向),Ceil(...) 向上。有些老语言里 5 / 2 会直接、悄悄地变成 2(小数被砍掉),在 Verse 里你得明写 Floor(5 / 2)——舍入方向白纸黑字,谁也别想蒙混过关。

求余数也没有 % 运算符,用可失败的 Mod[A, B]。顺手再记一批标准库数学函数:AbsMinMaxSqrt,以及 float 版的 Pow求余数没有 % 这个符号,改用 Mod[A, B](方括号 = 这一步可能走不通,除数为 0 会失败)。顺手再记一批内置数学函数——AbsMinMaxSqrt,还有 float 版的 Pow,其实就是你在蓝图里连过的那些数学节点。下面这台「分赃机」把除法、取整、求余一次演全——点「运行下一步」,逐行看它怎么分金币。

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

loot_split_device := class(creative_device):

    # 战利品:7 枚金币,2 名玩家
    TotalGold:int = 7
    Players:int = 2

    OnBegin<override>()<suspends>:void =
        if (Share := Floor(TotalGold / Players)):
            Print("每人分到 {Share} 金币")
        if (Extra := Mod[TotalGold, Players]):
            Print("多出 {Extra} 枚,进公会仓库")
输出日志

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

脑内改改参数:要是 Players 是 0,第 11 行的除法会失败,if 整体不成立,里面的 Print 一行都不会执行——没有崩溃,没有异常,失败被 if 稳稳接住。这就是 Verse 把「除零事故」从运行时炸弹变成编译期纪律的方式。

脑内改改参数:要是 Players 是 0,第 11 行的除法会走不通,整个 Branch(if)不成立,里面的 Print String 一行都不会执行——没有崩溃,没有异常,「走不通」被 Branch 稳稳接住、改走 else。这就是 Verse 把「除零事故」从运行时的炸弹,提前变成 Compile 时就必须处理掉的纪律的方式。

五、常见坑:数字流水线事故档案

下面是新手在数字流水线上最常出的八起事故,每一起都配好了病因和药方。先扫一遍,待会儿质检站考你。

症状 病因 药方
随手写 Half := X / 2,编译不过 int 除法可失败(除数可能为 0),必须待在失败上下文里int 除法可能走不通(除数可能为 0),必须接在 Branch 节点(if)上 if (Half := Floor(X / 2)):
以为 5 / 2 等于 2 结果是 rational 5/2,Verse 不做静默截断结果是精确分数 rational 5/2,Verse 不会背着你把小数砍掉 自己选方向:Floor(5 / 2) 得 2,Ceil(5 / 2) 得 3
Sleep(5) 编译报错 Sleep 要 float,int 不会自动转换 Sleep(5.0)
set MyInt /= 2 编译不过 可失败的 int 除法塞不进复合赋值int 除法可能走不通,没法塞进 /= 这种「一步改自己」的写法里 float 变量的 /= 没问题;int 请走 if + Floorfloat 变量的 /= 没问题;int 要老实用 Branch(if)+ Floor
想用 % 求余,查无此符 Verse 没有 % 运算符 Mod[A, B](可失败,方括号)Mod[A, B](方括号 = 可能走不通)
界面上显示 1.500000 float 转字符串固定六位小数 见拓展页「四舍五入到任意小数位」
以为 NaN 谁都不等于 Verse 故意偏离 IEEE-754:NaN = NaN 成立,且 NaN 最大 数据可能含 NaN 时,先比较拦截再交给 Max/排序
大数连乘,游戏运行到一半报错 int 溢出是运行时错误,不是静默回绕 留意 64 位范围,必要时提前设上限

六、关卡挑战:数字质检站 ★

恭喜你被任命为数字流水线的质检员。三批代码正被传送带送来,有的能放行,有的必须拦下——给出你的质检结论。答错零惩罚,可以一直重试。

第一批:普通函数体里一条普通执行线上孤零零的一行 Half := X / 2(X 是 int)。质检结论?

第二批:Total := Kills + 0.5(Kills 是 int)。质检结论?

第三批是一道理论题:关于 NaN,下面哪个说法在 Verse 里是对的?

拓展阅读

技巧 · EXTRA

四舍五入到任意小数位

Epic 员工给出的 RoundToDecimalPlaces,一个函数串起本课全部知识点。

进入拓展 →

拔高 · EXTRA

rational:第三种数字类型

整数除法的结果既不是 int 也不是 float——认识这位被忽略的精确分数。

进入拓展 →

拓展 · EXTRA

Verse 如何驯服 NaN

对 IEEE-754 的三处大胆修改,让 float 拥有全序。

进入拓展 →