Verse Wiki — 写给 UE6 时代的 Verse 互动手册
拓展 · EXTRA

四舍五入到任意小数位:RoundToDecimalPlaces

「怎么把 float 保留两位小数?」是官方论坛的高频问题,Epic 员工给出的标准答案只有五行——但这五行恰好把第 5 课的全部知识点串成了一条链:Pow、乘 1.0 转换、可失败的 Round[]、参数校验会失败的 Round[](方括号调用)、给参数把关。逐行拆开看。

一、一个高频问题

做过计分板、伤害飘字或排行榜的创作者,几乎都在同一个地方停下来过:算出来的 float 是 10.12345,可界面上只想显示 10.123别的语言里这是一行 round(x, 3) 的事,但翻遍 Verse 标准库,你找不到「保留 N 位小数」的现成函数别的引擎里这也许是个现成的「round 到 N 位」节点,但翻遍 Verse 自带的函数库,你也找不到「保留 N 位小数」的现成函数——只有第 5 课认识的那三位:Floor[]Ceil[]Round[],它们一律取整到个位。

官方论坛上的提问「How to round float to X decimal places」最终由 Epic 员工 Kurtis Schmidt 给出了标准答案。思路本身是各语言通用的老手艺:先把小数「放大」成整数再取整,最后「缩」回去——想保留 3 位小数,就乘 10³ = 1000,取整,再除回 1000。妙的是,这条老手艺在 Verse 里落地时,每一步都要过一遍本课讲过的类型关卡,所以这五行代码是绝佳的复习材料。

二、逐行拆解

round_utils.verse
# 整理自 Epic 员工 Kurtis Schmidt 在官方论坛的回答(出处见文末)
# TODO: Round 在 .5 时的舍入方向以 Verse API Reference 为准
RoundToDecimalPlaces(Value:float, DecimalPlaces:int)<varies><decides>:float =
    DecimalPlaces >= 0
    Multiplier := Pow(10.0, DecimalPlaces * 1.0)
    Rounded := Round[Value * Multiplier] * 1.0
    Rounded / Multiplier

签名行:函数带着 <decides> 效果说明符——整个函数因此成为可失败表达式,调用时要用方括号、住进失败上下文,和 Floor[] 一个待遇。(原帖签名里还有 <varies>,这里原样保留。)

签名行:函数名后面挂着 <decides> 这个效果标记——它让整个函数变成「可能走不通」的一步:调用时要用方括号,还得接在 Branch 节点(if)上,和 Floor[] 一个待遇。(原帖签名里还有 <varies>,这里原样保留。)

第一行 DecimalPlaces >= 0:这不是被遗忘的半句话,而是参数校验。还记得第 5 课说比较运算符都是可失败表达式吗?在 <decides> 函数体里,任何一个表达式失败,整个调用就失败。于是「小数位数不能是负数」这条校验,一个 if 都不用写。

第一行 DecimalPlaces >= 0:这不是被遗忘的半句话,而是在给参数把关。还记得第 5 课说的吗——所有比较在 Verse 里都是「能不能走通」的判断。在带 <decides> 的函数里,只要有一步走不通,整个调用就走不通。于是「小数位数不能是负数」这条检查,你连一个 Branch(if)都不用画。

第二行 Pow(10.0, DecimalPlaces * 1.0):Pow 的两个参数都是 float,而 DecimalPlaces 是 int——没有隐式转换,所以乘 1.0 过桥Verse 不会自动帮你把 int 换成 float,所以乘 1.0 过桥。保留 3 位时,Multiplier 是 1000.0。

第三行 Round[Value * Multiplier] * 1.0:一行走了个来回:Value * Multiplier 把 10.12345 放大成 10123.45;可失败的 Round[] 取整成 int 10123(如果 Value 是 NaN 或放大后超出 int 范围,这里失败,整个函数跟着失败——正是我们想要的);再乘 1.0 转回 float,准备做下一行的除法。

第三行 Round[Value * Multiplier] * 1.0:一行走了个来回:Value * Multiplier 把 10.12345 放大成 10123.45;Round[] 用方括号调用(意味着可能走不通)把它取整成 int 10123(如果 Value 是 NaN、或放大后超出 int 范围,这一步走不通,整个函数跟着走不通——正是我们想要的);再乘 1.0 转回 float,准备做下一行的除法。

第四行 Rounded / Multiplier:两个 float 相除——float 除法不可失败,直接得 10.123。没有 return:Verse 隐式返回,最后一个表达式就是函数结果。

第四行 Rounded / Multiplier:两个 float 相除——float 除法不会走不通,直接得 10.123。这里没写 return:Verse 是隐式返回,函数里最后一个式子的结果,就直接当成这个蓝图函数的返回值(相当于最后一个节点的输出直接接到 Return 引脚上,不用专门放一个 Return 节点)。

用起来长这样:

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

round_demo_device := class(creative_device):

    OnBegin<override>()<suspends>:void =
        if (Result := RoundToDecimalPlaces[10.12345, 3]):
            Print("保留三位:{Result}")

读图翻译:一个设备,Event BeginPlay 里调用刚写好的 RoundToDecimalPlaces 函数,把 10.12345 保留 3 位。因为这个函数会失败(方括号调用),得接在 Branch 节点(if)上:走通了,才用 Print String 打出结果。

三、显示的最后一公里

先泼半盆冷水:上面的 PrintPrint String 节点 打出来的是 保留三位:10.123000。数值上确实已经是 10.123 了,但第 5 课的老坑还在——float 转字符串固定带六位小数,尾巴零照样挂着。所以这个函数解决的是「数值精度」问题(比如把结算金额规整到分),不是「显示格式」问题。想让界面上真的只出现三位小数,还要再走一步字符串处理:官方社区有现成的「Float to String with Decimal Places」代码片段,把 float 拼成指定小数位的字符串,正好接在本函数后面用。

把两件事分开想——「这个数是多少」归 RoundToDecimalPlaces 管,「这个数长什么样」归字符串格式化管——很多界面数字的乱象就都能理顺了。

第二行为什么写 DecimalPlaces * 1.0,而不是直接把 DecimalPlaces 传给 Pow?

调用这个函数时为什么写 RoundToDecimalPlaces[10.12345, 3](方括号)?

来源

本文整理自 Epic 开发者论坛与官方社区代码片段:How to round float to X decimal places(Epic 开发者论坛)↗ · Float to String with Decimal Places(官方社区代码片段)↗