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

weak_map 与跨局持久化:让玩家的进度活过下一局

游戏一结束,普通 map 里的分数就灰飞烟灭。想做排行榜、玩家等级、货币系统?你需要 map 的近亲 weak_map——UE6 持久化系统的官方载体。这一页是 map 课通向实战的那座桥。

一、map 的近亲:处处受限的 weak_map

weak_map(k, v) 和 map 是近亲,但写法和脾气都不一样:类型用圆括号写成 weak_map(player, my_data),而不是方括号的 [k]v;初始值倒是同款的 map{}。真正的差别在能力清单上——weak_map 不可遍历,没有 .Length,也不能用 ConcatenateMaps 合并;它只支持一种操作:按键逐条读写。

weak_map(k, v) 和 map 是近亲,但写法和脾气都不一样:声明类型时用圆括号 weak_map(player, my_data),不是 map 那种方括号 [k]v;初始值倒是同款的 map{}。真正的差别在能力清单上——weak_map 不能逐行点名(没有 ForEach)、拿不到条数(没有 .Length)、也不能整表合并;它只会一件事:拿着某个键,读或写那一条。

听起来像个「阉割版 map」?恰恰相反:正是这些限制换来了它的超能力。因为运行时不需要维护「完整的键列表」,weak_map 的条目可以由引擎按需加载和落盘——这正是持久化存储需要的性质。

听起来像个「阉割版 map」?恰恰相反:正是这些限制换来了它的超能力。因为引擎不必随时攥着「所有键的完整名单」,weak_map 里的每一条都可以由引擎按需读进来、写回硬盘——这正是「存档」需要的性质。

二、跨局存档:模块作用域 + persistable跨局存档:放对地方 + 标成可存档

让数据跨会话保存,官方配方只有两味药:第一,把 weak_map 变量声明在模块作用域(类的外面),键类型用 player;第二,值类型满足 persistable——最常见的做法是定义一个 class<final><persistable>,所有字段都是可持久化类型并带默认值。凑齐这两条,UE6 就会按「每玩家、每岛屿」自动把数据跨会话保存,你一行存盘代码都不用写。

让数据跨会话保存,官方配方只有两味药:第一,把 weak_map 变量声明在模块作用域(所有类的外面,相当于放进一个谁都能拿到的公共抽屉),键类型用 player;第二,值得是「能存档」的类型——最常见的做法是定义一个带 <persistable> 标记的数据结构,里面每个字段都用能存档的类型、并且都给默认值。凑齐这两条,UE6 就会按「每玩家、每岛屿」自动帮你存,你一行存盘代码都不用写(比蓝图里手动 Save/Load SaveGame 还省事)。

player_progress.verse
using { /Verse.org/Simulation }

# 可持久化的玩家进度:final + persistable,字段全部带默认值
player_progress := class<final><persistable>:
    Level:int = 1
    Coins:int = 0

# 模块作用域的 weak_map:UE6 按「每玩家、每岛屿」自动跨局保存
var PlayerProgress:weak_map(player, player_progress) = map{}

读写沿用 map 课的全部肌肉记忆——按键查询是失败上下文,索引赋值也要包 if。因为 persistable 类的实例同样是不可变值,「改一个字段」的姿势是「造一个新实例整条覆盖」:

读写沿用 map 课的全部肌肉记忆——按键查询要接进 Branch,写入也要接进 Branch。因为「能存档」的记录同样是「不能原地改的整体」,「改一个字段」的姿势是「照旧的造一份新的、整条覆盖」:

level_up.verse
using { /Verse.org/Simulation }

# 给某位玩家升一级:读出旧进度,写回新实例
LevelUp(Target:player):void =
    if (Old := PlayerProgress[Target]):
        NewProgress := player_progress{Level := Old.Level + 1, Coins := Old.Coins}
        if (set PlayerProgress[Target] = NewProgress) {}

读图翻译:LevelUp 是个蓝图函数,给某位玩家升一级。先用一个 Branch 按玩家键把旧进度查出来(Old);查到了,就照着旧值造一份新记录——等级 +1、金币照抄;再用一个 Branch 把这条新记录写回那位玩家名下。整段没有一处「就地改」,全是「读出来、造新的、整条写回」。

额度也要心里有数:一条 weak_map 记录,每位玩家最多 256 KB。听着不大,但存等级、货币、任务进度这类结构化数据绰绰有余——它是存档槽,不是仓库。

三、什么时候用它:分工与版本升级

先划清分工:局内统计用普通 map,跨局数据才动用 weak_map。官方 Team Elimination 教程就是个标准示范——一局之内用 [player]int 的普通 map 追踪每个玩家的淘汰数,随用随查随遍历;要是想让「历史总淘汰数」跨局累积,才把它挪进持久化 weak_map。别为了「以防万一」把什么都持久化:weak_map 不可遍历,你连「列出所有玩家的数据」都做不到。

先划清分工:局内统计用普通 map,跨局数据才动用 weak_map。官方 Team Elimination 教程就是个标准示范——一局之内用一张「玩家 → 淘汰数」的普通 map 追踪每个人的淘汰数,随用随查、还能逐行点名;要是想让「历史总淘汰数」跨局累积,才把它挪进能存档的 weak_map。别为了「以防万一」把什么都存档:weak_map 不能点名,你连「把所有玩家的数据列出来」都做不到。

最后是老项目都会撞上的问题:游戏更新了,持久化 class 要加新字段怎么办?官方 Verse Persistence Best Practices 给出的策略是带默认值地加字段——老玩家的旧存档读出来时,新字段自动取默认值,平滑升级;千万不要删字段或改字段含义,那会让旧数据无处安放。给每个字段配上保守的默认值,是持久化系统的长寿秘诀。

最后是老项目都会撞上的问题:游戏更新了,那个「能存档」的数据结构要加新字段怎么办?官方 Verse Persistence Best Practices 给出的策略是加字段时一定给默认值——老玩家的旧存档读出来时,新字段自动填默认值,平滑升级;千万别删字段、也别改字段原来的含义,那会让旧存档无处安放。给每个字段配上保守的默认值,是存档系统的长寿秘诀。

下面哪件事是 weak_map 做不到的?

来源

本文整理自 Epic 官方文档与教程:

Using Persistable Data in Verse(官方文档)↗
Verse Persistence Best Practices(官方文档)↗
Team Elimination: Tracking Players Using Maps in Verse(官方教程)↗