模块作用域变量与数据持久化:weak_map 与 <persistable>
课内讲了块级作用域,这里补上作用域洋葱的最外层:模块。UEFN 对模块级 var 有一条特殊规矩——必须是 weak_map 类型。这条规矩回答了「为什么我不能写全局变量」,还顺手送你一套跨对局存档系统。
一、为什么不能写全局 var
几乎每个学完第 4 课的人都会立刻试一下:在模块顶层(所有类的外面)写 var TotalKills:int = 0,当个全局计数器多方便。然后收获一条编译错误。这不是你写错了语法,而是 UEFN 的硬规矩:模块作用域的 var 不能是普通的 int / string / logic,必须是 weak_map 类型。常量不受限——MaxPlayers:int = 12 放在模块顶层完全合法,被限制的只有「可变」的那一半。
几乎每个学完第 4 课的人都会立刻试一下:在模块顶层(所有类的外面)写 var TotalKills:int = 0,当个全局计数器多方便。然后收获一条 Compile 报错。这不是你写错了写法,而是 UEFN 的硬规矩:模块最外层的可变变量不能是普通的整数 / 字符串 / 布尔,必须是 weak_map 类型。常量不受限——MaxPlayers:int = 12 放在最外层完全合法,被卡住的只有「可变」的那一半。
为什么这么设计?想想你的岛屿运行的环境:多人同时在线,玩家随时加入、随时退出。一个裸的全局 var 意味着「所有玩家共享同一格数据」——谁的击杀都往同一个数里加,玩家离开后数据也无处安放。UEFN 干脆在类型系统层面把问题堵死:想要模块级的可变状态?请说清楚它按什么分账。这正是 weak_map(弱映射)的角色——最常见的用法是以 player 为键,一名玩家一格,互不打架;玩家离开,对应的条目自然随之失效,不会泄漏。
为什么这么设计?想想你的岛屿运行的环境:多人同时在线,玩家随时加入、随时退出。一个光秃秃的全局可变变量意味着「所有玩家共享同一格数据」——谁的击杀都往同一个数里加,玩家离开后数据也无处安放。UEFN 干脆从根上把问题堵死:想要模块级的可变状态?请说清楚它按什么分账。这正是 weak_map(弱映射)的角色——最常见的用法是以 player 为键,一名玩家一格,互不打架;玩家离开,对应的那格自然作废,不会泄漏。这其实就是「每个玩家一份的 SaveGame」的雏形。
二、模块级 var 的正确姿势
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
# 模块级 var 必须是 weak_map 类型
# 以 player 为键:一名玩家一格,互不打架
var PlayerScores:weak_map(player, int) = map{}
score_board_device := class(creative_device):
AddTen(Player:player):void =
var NewScore:int = 10
if (Old := PlayerScores[Player]):
set NewScore = Old + 10
if (set PlayerScores[Player] = NewScore) {}
三个看点。第一,声明:weak_map(player, int) 读作「以 player 为键、int 为值的弱映射」,初始值 map{} 是一张空表。第二,读:PlayerScores[Player] 是可失败表达式(这名玩家可能还没有记录),所以要包在 if 里——失败上下文的完整故事在后面的章节展开,这里先记住「读写这张表都要过 if」。第三,写:对 weak_map 的 set 同样可能失败,所以官方惯用法是 if (set …[…] = …) {},用一个空块把「失败了就算了」写明白。
三个看点。第一,新建:weak_map(player, int) 读作「以 player 为键、整数为值的弱映射」,初始值 map{} 是一张空表。第二,读:PlayerScores[Player] 可能走不通(这名玩家也许还没有记录),所以得包在 if(蓝图的 Branch)里——「可能走不通的判断」的完整故事在后面章节展开,这里先记住「读写这张表都要过一道 Branch」。第三,写:往 weak_map 里塞值(set)同样可能走不通,所以官方惯用法是 if (set …[…] = …) {},用一个空的分支把「走不通就算了」写明白。
三、免费赠品:跨对局存档
这条「麻烦」的规矩附赠了一个大礼包:数据持久化。按官方文档,以 player 为键、值类型满足 persistable 约束的模块级 weak_map var,其中的数据可以跨对局持久保存——玩家这局攒的分,下局进来还在。你不用写任何存档代码:声明形状对了,存档就是自动的。
这条「麻烦」的规矩附赠了一个大礼包:数据持久化。按官方文档,以 player 为键、值类型满足 persistable 约束的模块级 weak_map,里面的数据可以跨对局保存——玩家这局攒的分,下局进来还在,活脱脱一套自动 SaveGame。你不用写任何存档逻辑:形状声明对了,存档就是自动的。
什么算 persistable?基础类型基本都在列:int、float、string、logic 等;想存自己的一组数据(比如「等级 + 金币 + 称号」),可以把承载它们的类或枚举标注 <persistable> 说明符,再作为值类型放进 weak_map。具体的约束清单和版本升级注意事项以官方「Using Persistable Data in Verse」页面为准,做长线运营的岛屿之前值得完整读一遍。
什么算 persistable?常见的基础类型基本都在列:整数、小数、字符串、布尔等;想存自己的一组数据(比如「等级 + 金币 + 称号」),可以把装它们的类或枚举资产挂上 <persistable> 这个设置,再作为值塞进 weak_map。具体的约束清单和版本升级注意事项以官方「Using Persistable Data in Verse」页面为准,做长线运营的岛屿之前值得完整读一遍。
回头看,这正是 Verse 一以贯之的风格:第 4 课里「可变要申请(var)、修改要喊口令(set)」,到了模块层则是「全局可变状态必须按玩家分账(weak_map)」。每一层都在逼你把危险的事情写得明明白白——而一旦写对,语言就把最难的部分(并发安全、生命周期、存档)替你包了。
回头看,这正是 Verse 一以贯之的风格:第 4 课里「可变要申请(var)、改值要接 Set」,到了模块层则是「全局可变状态必须按玩家分账(weak_map)」。每一层都在逼你把危险的事情写得明明白白——而一旦写对,语言就把最难的那部分(多人同时改的安全、数据的生命周期、存档)替你包了。
四、小测验
在模块顶层写 var TotalKills:int = 0,会发生什么?
来源
本文整理自 Epic 官方文档:Using Persistable Data in Verse(官方文档)↗ · Verse Language Quick Reference(官方文档)↗