解决 Fortnite Creative 缺失 Clear Player Data 选项:Verse 重置与自定义 Backend 状态恢复
概要
本文针对 Fortnite Creative 和 UEFN 中发布确认页面缺失 “Clear Player Data” 选项的问题,提供了深度的技术解决方案。作者探讨了如何通过在 Verse 中实现 Schema 版本控制以及配置本地编辑器设置来规避此 Bug,并分析了原生存储的局限性。最后,文章提出了使用外部数据库及 [horizOn](https://horizon.pm) 进行状态同步与管理的替代方案,以避免受制于平台特定的 UI 缺陷与存储上限。
你的最新 Fortnite Creative 或 UEFN 地图已经准备就绪。你重构了经济系统,重新设计了职业系统(class system),并优化了 gameplay 循环。但在发布时,你遇到了一个让引擎崩溃的噩梦:发布确认界面上的 “Press Triangle to Clear Player Data”(按三角键清除玩家数据)选项消失了。你被困住了。玩家正带着过时的 persistence 数据加载进你的地图,导致瞬间发生 stack overflow(栈溢出)、任务线损坏以及引起游戏崩溃的状态不匹配。
在 Epic 的开发者论坛上,这个问题困扰了许多创作者。重置按钮的突然缺失导致没有任何基于 UI 的机制来清除陈旧的生产状态(production states)。当你的玩家 persistence 数据 schema 发生变化时,Epic 的 backend 会尝试将旧的序列化数据映射到你的新结构上。如果你的代码没有设计成能够优雅地处理这些不匹配的属性,你将直接导致 100% 回流玩家的游戏状态彻底损坏(brick)。
在本教程中,我们将深入探讨该 bug 的技术变通解决方案。我们将向你展示如何在 Verse 中构建一个健壮的 schema 版本控制系统,配置本地 UEFN 设置以清除测试状态,并探讨自定义外部 backend 如何将你从封闭平台的 persistence 限制中解放出来。
根本原因:为什么 Schema 在版本升级时会失败
在 Fortnite Creative 和 Unreal Editor for Fortnite (UEFN) 中,持久化数据是通过 weak_map 结构存储在 Epic 的数据库基础架构中。weak_map 将 player 实例关联到标有 <persistable> 修饰符的自定义 class。与传统的 SQL 或文档数据库不同,Epic 的存储层在你发布更新时不会运行正式的 migration 流程。相反,它会尝试将玩家云存档中存在的任何二进制 payload 反序列化为当前的 Verse class 定义。
当你删除变量、重命名属性或更改类型(例如,将字段从 int 更改为 float)时,反序列化器就会遇到不匹配的情况。根据修改的严重程度,这种 schema 分歧会导致以下三种失败模式之一:
- 静默失败 (Silent Failures): runtime 会为修改后的属性分配默认值,虽然不会导致崩溃,但依然会抹去玩家的进度。
- 状态损坏 (State Corruption): 应用程序读取了无效的内存状态,或遇到了意外的数据结构,从而在其他地方触发逻辑错误。
- 严重的 Runtime 崩溃 (Severe Runtime Crashes): 地图加载失败,或者 Verse VM 停止执行,因为它无法解包 persistent map。
当发布者控制台的 “Clear Player Data” 按钮消失时,你就失去了全局清空数据的能力。如果开发者对他们的存档数据布局进行了根本性修改,游戏就会崩溃。
手动修复:Verse 内部的 Schema 版本控制
既然你不能指望 Epic 的发布门户来清除数据,你就必须实现一个软件层面的 fallback。策略非常直接:直接在你的 persistable classes 中引入一个版本追踪器。通过在 session 开始时将玩家保存的版本与当前运行的应用程序版本进行比较,你可以识别出过时的存档,并用默认值覆盖它们。
下面是一个生产就绪的 Verse 实现,它建立了一个版本受控的 persistence 层。
using { /Fortnite.com/Devices }
using { /Fortnite.com/Characters }
using { /Fortnite.com/Playspaces }
using { /Verse.org/Simulation }
# A simple persistable class representing our player save schema.
# Note: Classes tagged with <persistable> can only contain certain types like int, float, string, and maps.
player_save_data := class<persistable>:
# The SchemaVersion tracks which generation of player data this struct belongs to.
SchemaVersion : int = 0
GoldCount : int = 0
XP : int = 0
HasCompletedTutorial : int = 0 # Using int as a boolean flag representation for older Verse constraints
# The persistence manager device handles loading, validating, and migrating player data.
persistence_manager_device := class(creative_device):
# By updating this value in the Editor, we force a schema migration next time the game runs.
@editable
TargetSchemaVersion : int = 2
# Map to store player data in memory
var PlayerDataMap : weak_map(player, player_save_data) = map{}
# Initialize the persistence logic for a joining player
InitializePlayer(Player : player) : void =
# Check if the player already has persistent data
if (ExistingData := PlayerDataMap[Player]):
# If the stored version is older than our target version, trigger a manual reset
if (ExistingData.SchemaVersion < TargetSchemaVersion):
Print("Data version mismatch. Local: {ExistingData.SchemaVersion}, Target: {TargetSchemaVersion}. Resetting player data.")
ResetPlayerData(Player)
else:
Print("Loaded existing player data. Version: {ExistingData.SchemaVersion}")
else:
# If no data exists, initialize a new record with the current version
Print("No player data found. Initializing new record.")
ResetPlayerData(Player)
# Re-initializes a player's record with default values at the current version
ResetPlayerData(Player : player) : void =
NewData := player_save_data:
SchemaVersion := TargetSchemaVersion
GoldCount := 0
XP := 0
HasCompletedTutorial := 0
# Save the freshly initialized data block to the persistence map
if (set PlayerDataMap[Player] = NewData):
Print("Successfully saved fresh persistent data block.")
解析版本控制脚本
这种模式依赖于 InitializePlayer 方法内部的早期验证。其逐步逻辑如下:
- 入口检查 (The Entry Check): 当玩家加入时,系统会查询
PlayerDataMap中是否存在已有的player_save_data实例。 - 版本评估 (The Version Assessment): 如果存在存档状态,引擎会检查其
SchemaVersion属性。 - 触发 Migration (The Migration Trigger): 如果
SchemaVersion低于 device 的@editable属性TargetSchemaVersion,脚本就会调用ResetPlayerData(Player)。 - 状态覆盖 (State Overwrite):
ResetPlayerData构建一个设置为新TargetSchemaVersion的player_save_data新实例,并更新 map。
通过在 UEFN 中手动增加 device 配置中的 TargetSchemaVersion,你可以在所有玩家下一次 session 初始化时强制清除目标数据,从而使缺失的 Epic UI 按钮变得无关紧要。
逐步测试步骤
为了在你的 playtest 周期中安全地验证此 migration 脚本,请按照以下步骤操作:
- 将自定义的
persistence_manager_device拖入你的 UEFN 关卡视口(viewport)中。 - 在 UEFN Details 面板中,将初始的
TargetSchemaVersion设置为 1。 - 启动一个 play session,积累一些分数或金币,然后结束游戏。
- 返回编辑器,将 device 上的
TargetSchemaVersion字段从 1 更新为 2。 - 再次启动 session。观察控制台日志输出:'Data version mismatch. Local: 1, Target: 2. Resetting player data.'。
- 验证玩家的金币和进度是否已干净重置,且没有导致 Verse VM 崩溃。
本地开发设置:在迭代期间绕过存档文件
虽然 runtime 代码版本控制解决了正式服务器上的问题,但在本地 playtesting 期间它会带来不必要的阻力。每次修改正在开发的功能时都要在 UEFN 中手动提高版本号是非常繁琐的。为了简化你的调试流程,你可以通过编辑器设置完全绕过本地 persistence。
请按照以下步骤禁用本地 persistence:
- 在 UEFN 中,导航到 Outliner 面板中的 Island Settings。
- 搜索 User Options - Game Rules 子板块。
- 找到 Enable Persistence 开关并将其禁用。
- 如果在本地测试 multiplayer 循环,打开 Editor Preferences,进入 In-Game Play 菜单,然后找到 Clear Local Saved Data On Launch 复选框。勾选此项可确保每次模拟都从空缓存开始。
UEFN 内置 Persistence 的局限性
使用 Verse 原生的 persistence 层会带来显著的架构约束。即使云端清除选项功能完好,开发者也受限于严格的平台限制:
对于轻度小游戏,这些界限是可以接受的。但对于复杂的持久化 RPG、实时运营(live-ops)射击游戏或跨平台游戏,仅依赖封闭的专属 persistence 方案会限制你的扩展路线。
# This keeps the server-side payload lightweight (under 240 bytes)
# while securing long-term storage off the Fortnite engine.
## 管理游戏状态 Persistence 的最佳实践
无论你是坚持使用原生的 Verse maps,还是利用外部服务器架构,维持稳定的 persistence 生命周期对于 multiplayer 游戏都至关重要。请遵循以下原则来保护玩家状态:
1. **尽早实现隐式版本控制 (Implement Implicit Versioning Early):** 务必在初始存档数据布局中包含一个 `SchemaVersion` 整数。即使你目前没有修改 schema 的计划,从第一天起就保留该版本键(version key)也能防止后续发生灾难性的数据损坏。
2. **执行状态验证规则 (Enforce State Validation Rules):** 在读取加载的状态时,验证每个属性的范围。如果玩家保存的金币数量为负数或超出了地图的结构上限,请重置该特定属性以防止经济系统漏洞(economy exploits)。
3. **最小化保存频率 (Minimize Save Frequency):** 写入云端是高开销的操作。与其在每次捡起金币时都更新数据库,不如进行批量写入。在关键里程碑事件(如比赛结束、玩家升级或玩家离开 session 时)触发保存。
4. **构建干净的 Fallback 结构 (Construct Clean Fallback Structs):** 绝不允许读取失败阻碍玩家进入游戏。如果 persistence payload 反序列化失败,应立即回退到默认的状态结构。
5. **将 Gameplay 代码与存储逻辑解耦 (Decouple Gameplay Code from Storage Logic):** 将你的存储逻辑隔离在专用的 persistence 管理器 device 中。你的武器、进度和 UI devices 应该查询该管理器,而不是直接调用数据 map。
## 下一步
依靠 Epic 的控制台 UI 来管理关键的状态迁移(state migrations)是一种高风险的设计模式。当 “Clear Player Data” 选项失效时,Verse 内部的 runtime 版本控制就是你的第一道防线。然而,如果你的游戏需要高频保存、复杂的 telemetry(遥测)或完全的管理控制权,将你的数据库迁移到专用的 backend 则是终极解决方案。
---
来源:[clear players data button in creative 1](https://forums.unrealengine.com/t/clear-players-data-button-in-creative-1/2735737)