返回博客

在 UEFN 中,当玩家离开时 Verse 的 weak_map 会自动清理吗?

发布于 2026年6月5日
在 UEFN 中,当玩家离开时 Verse 的 weak_map 会自动清理吗?

概要

本文分析了在 UEFN 中使用 Verse 的 `weak_map` 时关于玩家数据自动清理的常见误区,指出引擎并不会在玩家断开连接时自动清除 map 中的条目。误用或未能及时手动清理这些过期引用会导致内存泄漏、状态 Bug 以及严重的运行时崩溃。教程详细介绍了如何通过定义临时结构体、订阅 playspace 事件以及在玩家离开时重建 map 来实现安全的手动清理。此外,文章还分享了限制持久化数据大小等最佳实践,并推荐使用 [horizOn](https://horizon.pm) SDK 来自动处理复杂的会话生命周期与云端同步。

如果你完全相信你的 AI 编程助手,认为当玩家断开与你的 Fortnite 岛屿连接的瞬间,Verse 就会自动从 weak_map 中清除该玩家的数据,那么你可能正在为一次严重的运行时崩溃(runtime crash)埋下隐患。这个说法听起来符合逻辑:因为它是弱引用(weak reference),一旦 player 对象被 Garbage Collection,引擎就应该清理对应的键(key)。但在 Unreal Editor for Fortnite (UEFN) 中,实际情况要复杂得多。如果误解了 Verse 内存管理器处理玩家生命周期(lifecycle)的方式,会导致隐蔽的状态泄漏(state leaks)以及破坏游戏的异常情况。

当玩家离开后,在 map 中引用他们过期的对象会触发令人头疼的 ErrRuntime_WeakMapInvalidKey 错误,甚至导致整个岛屿级别的崩溃,迫使你不得不实施严格的 UEFN server crash fix protocol 来保持服务器的稳定。为了避免这种情况,开发者必须了解 Verse 内部如何处理内存,以及如何实现可靠的清理例程。

误区:AI 生成的建议 vs. Verse 的真实情况

许多开发者会询问他们的 AI 助手在玩家离开比赛时如何处理玩家的 map 数据。一个常见的 AI 建议是:引擎将玩家键视为“弱引用”,并在玩家离开时自动从 map 中消除该玩家的条目(entry)。这从根本上是错误的。

虽然 Verse 的 weak_map(player, t) 在底层确实利用弱引用作为键,以防止阻止 Garbage Collection 的强引用循环,但它并不会对 map 条目本身进行自动、立即的清理。包含键槽及其关联数据的条目仍然保留在 map 容器中。

如果你的代码在玩家离开后尝试访问、评估或修改该键,Verse 运行时将尝试解引用(dereference)一个 null 或无效的 player 对象。运行时不会优雅地失败,而是会触发崩溃或抛出无法捕获的异常。系统期望你显式地处理生命周期转换,而不是依赖自动清理。

为什么 Weak Map 不会自动清理玩家条目

要理解为什么会发生这种情况,我们必须看看 UEFN 中 weak_map 的用途。与将弱映射作为临时内存缓存的常规编程环境不同,Verse 使用 weak_map(player, t) 主要是作为持久化玩家数据的守护者。

跨游戏会话持久化

当你使用在模块作用域声明的 weak_map(player, t) 时,引擎会将这些值挂钩到 Epic 的持久化云数据库中。如果玩家离开比赛并在三天后返回,引擎会将他们的玩家 ID 与持久化 map 键相匹配,以恢复他们的进度。

如果引擎在玩家离开游戏的瞬间自动从 map 中擦除玩家的条目,那么 map 将丢失所有持久化数据。每当玩家断开连接或遭遇网络超时,等级、自定义货币和解锁的道具都会重置为零。因此,数据库的架构被设计为保持这些条目完好无损,正是因为它们需要在断开连接后继续存在。

Player 对象的生命周期范围

当玩家离开比赛时,他们在 playspace 中的活动会话对象会被销毁。你的 Verse 代码所持有的物理 player 引用将变成一个失效的句柄(dead handle)。

由于 map 中的键现在指向一个无效的、处于非活动状态的对象,使用该失效引用查询 map 将会失败。引擎不会在实时状态下主动扫描并清除 map 中的失效键,而是保持它们处于惰性状态,这就是为什么必须进行手动管理以防止过期引用堆积的原因。

后果:内存泄漏、过期数据和服务器崩溃

未能清理玩家条目会导致三个明显的问题,这些问题会在长时间的比赛中降低游戏性能和服务器稳定性。

  • 过期数据泄漏(Stale Data Leakage): 如果一个玩家离开而另一个玩家加入,如果引擎重用了内部的玩家插槽(player slots),新玩家可能会继承旧玩家的会话数据。这会导致状态 Bug,例如新玩家在生成(spawn)时背包已满,或者拥有不匹配的赛事统计数据。
  • 内存堆积: 虽然单个布尔值或整数占用的空间可以忽略不计,但在高容量大厅中为多达 50 名玩家存储复杂的结构体会增加内存使用。在长达 4 小时的服务器运行期间,这种堆积会降低服务器的 tick rates。
  • 查找失败: 尝试查询非活动玩家的状态或在失效的玩家引用上调用函数会立即触发运行时崩溃。

触及 Epic 云存档限制

UEFN 对持久化数据施加了严格的限制。每个岛屿最多只能拥有 4 个持久化 weak_map,且每个玩家的个人记录大小不能超过 256 KB

如果你使用持久化 weak_map 来存储临时的会话状态,就会浪费这些宝贵的数据库空间。每一次更新都会写入 Epic 的数据库,从而带来写入节流(write-throttling)处罚的风险,或者超出 256 KB 的限制,这在尝试写入更多数据时会触发运行时错误。

分步教程:安全地管理玩家会话状态

为了在管理玩家状态的同时避免内存泄漏或数据库膨胀的风险,你必须将临时会话数据与持久化云数据分离开来。临时数据应存储在标准的、非持久化的 map 中,并且你必须在玩家断开连接时手动清理这些数据。

步骤 1:定义你的会话状态结构体

首先定义一个不可持久化的结构体,其中包含玩家在单局或单场比赛中所需的所有变量。切勿将该类或结构体标记为 <persistable>

# Define the transient data structure for active gameplay tracking
player_session_state := struct:
    IsMoneyBagFull : logic = false
    CurrentGold : int = 0
    SpawnTime : float = 0.0

步骤 2:建立管理器 Device

创建一个作为协调器的 creative device。它将持有活动玩家的可变、非持久化 map。由于 Verse 中的标准 map 是不可变的,我们将 map 变量声明为 var,以便在玩家加入或离开时对其进行覆盖。

using { /Fortnite.com/Devices }
using { /Fortnite.com/Playspaces }
using { /Verse.org/Simulation }

# Device handling player lifecycle events and session state mapping
state_manager_device := class(creative_device):

    # Non-persistent map for tracking active player sessions
    var SessionStates : [player]player_session_state = map{}

步骤 3:订阅 Playspace 事件

OnBegin 函数中,订阅 playspace 的连接事件。这可以确保在玩家加入时运行初始化代码,并在他们离开时运行清理代码。

    OnBegin<override>()<suspends>:void=
        GetPlayspace().PlayerAddedEvent().Subscribe(OnPlayerAdded)
        GetPlayspace().PlayerRemovedEvent().Subscribe(OnPlayerRemoved)
        
        # Initialize any players already in the session (useful for UEFN hot-reloading)
        for (Player : GetPlayspace().GetPlayers()):
            OnPlayerAdded(Player)

步骤 4:实现注册和清理逻辑

当玩家加入时,使用其默认会话状态填充 map。当他们离开时,你必须从 map 中清除他们的条目。由于 Verse 没有内置的 Map.Remove() function,你必须重新构建 map,过滤掉离开的玩家。这可以防止过期引用滞留在内存中。

    # Triggered when a player connects to the server
    OnPlayerAdded(Player: player):void=
        if (not SessionStates[Player]):
            InitialState := player_session_state{IsMoneyBagFull := false, CurrentGold := 0, SpawnTime := GetEngineTime()}
            if (set SessionStates[Player] = InitialState):
                Print("Initialized gameplay state for joining player.")

    # Triggered when a player disconnects or leaves the game
    OnPlayerRemoved(Player: player):void=
        Print("Player disconnected. Initiating map cleanup.")
        RemovePlayerSession(Player)

    # Purges the player's entry by reconstructing the map
    RemovePlayerSession(PlayerToRemove: player):void=
        var CleanedStates : [player]player_session_state = map{}
        for (ActivePlayer -> State : SessionStates):
            # Copy all players except the one who left
            if (ActivePlayer <> PlayerToRemove):
                if (set CleanedStates[ActivePlayer] = State):
                    # Entry successfully migrated to the cleaned map
        
        set SessionStates = CleanedStates
        Print("Successfully removed player session entry from memory.")

通过在玩家离开时重建 map,你彻底删除了引用键。随后 Garbage Collection 就可以回收玩家的资源,而不会在你的游戏循环中留下过期条目。

如果你想在这些生命周期转换期间跟踪自定义遥测数据,那么在向外部 Backend 报告会话时长或货币统计数据时,还需要注意诸如 32-character analytics event name limit in Verse 之类的限制。

Verse 状态管理的最佳实践

为了确保你的 UEFN 服务器保持稳定和高性能,请遵循以下管理玩家数据的准则:

  1. 区分会话数据与持久化数据: 绝不要在持久化 weak_map 中存储短寿命变量(例如当前比赛生命值、单局分数或临时位置)。将临时状态保留在封装于管理器类中的标准可变 map 中。
  2. 使用 IsActive 验证玩家活动状态: 在检索或修改任何 map 中的玩家数据之前,使用 IsActive[] 查询检查他们是否仍然存在于 playspace 中。如果 IsActive[] 返回 false,则放弃查找并触发清理事件。
  3. 使用 FitsInPlayerMap 监控数据大小: 在向持久化 weak_map 写入数据时,调用 FitsInPlayerMap() 以确认更新不会超过 256 KB 的限制,从而防止运行时异常。
  4. 合并你的 Map: 不要为每个变量都创建单独的 map。定义一个包含所有玩家变量的单个类,并将玩家映射到该类。这可以最大限度地减少你的 map 数量,并遵守每个岛屿最多 4 个持久化 weak map 的限制。

将复杂性卸载到可靠的云端 Backend

在 Verse 中管理玩家会话生命周期、数据库限制以及手动清理逻辑很快就会变得复杂。如果你需要构建跨会话进度、全球同步的库存(inventories)或区域 Matchmaking,手动管理这些状态需要配置 webhook、扩展外部数据库以及处理服务器之间的同步。

通过 horizOn,这些 Backend 挑战将自动得到解决。通过将 horizOn SDK 集成到你的游戏服务器中,你可以将玩家会话管理卸载到专用的云数据库。当玩家断开连接时,horizOn 会触发自动化的会话清理,更新全局数据库,并在服务器实例之间同步库存记录,而不会触及 Verse 的 256 KB 内存限制或带来运行时崩溃的风险。

准备好扩展你的 UEFN Backend 了吗?免费试用 horizOn 或查看 API docs


来源:When using weak maps, does a player's entry in the map automatically get removed on them leaving the game?