Назад к блогу

Очищаются ли Verse weak_maps автоматически при выходе игрока в UEFN?

Опубликовано 5 июня 2026 г.
Очищаются ли Verse weak_maps автоматически при выходе игрока в UEFN?

Коротко о главном

В статье рассматривается распространенное заблуждение о том, что `weak_map` в Verse автоматически очищает записи игроков после их отключения в UEFN. На самом деле ключи в `weak_map` используются Epic для сохранения персистентных данных между сессиями, поэтому автоматическая очистка не происходит, что может приводить к утечкам памяти и runtime-сбоям при обращении к недействительным ссылкам. Автор предлагает пошаговое руководство по созданию кастомной системы управления сессиями игроков с ручной очисткой временных map. В качестве альтернативы рассматривается интеграция [horizOn](https://horizon.pm) SDK для переноса логики сессий на надежный внешний Backend без риска превышения лимитов памяти.

Если вы верите своему AI-помощнику по написанию кода, когда он говорит, что Verse автоматически удаляет данные игрока из weak_map в ту же миллисекунду, как только тот отключается от вашего острова Fortnite, вы обрекаете себя на серьезный runtime-сбой. Это заявление кажется логичным: поскольку это слабая ссылка, движок должен очистить ключ после того, как объект игрока будет собран с помощью Garbage Collection. Но в Unreal Editor for Fortnite (UEFN) реальность гораздо сложнее, и непонимание того, как Verse memory manager обрабатывает жизненные циклы игроков, приводит к скрытым утечкам состояния (state leaks) и ломающим игру исключениям.

Когда игрок выходит, обращение к его устаревшему объекту в map может вызвать пресловутую ошибку ErrRuntime_WeakMapInvalidKey или привести к падению всего острова. Это потребует от вас внедрения строгого протокола устранения сбоев сервера UEFN для поддержания стабильности ваших серверов. Чтобы избежать этого, разработчикам необходимо разобраться, как Verse управляет памятью изнутри, и научиться реализовывать надежные процедуры очистки.

Заблуждение: советы от AI против реальности Verse

Многие разработчики спрашивают своих AI-ассистентов, как обрабатывать данные игрока в map, когда тот покидает матч. Распространенный совет, генерируемый AI, заключается в том, что движок обрабатывает ключ игрока как «слабую ссылку» (weak reference) и автоматически удаляет запись игрока из map при его выходе. Это в корне неверно.

Хотя под капотом weak_map(player, t) в Verse использует слабые ссылки в качестве ключей для предотвращения жестких циклов ссылок (hard reference cycles), которые блокировали бы Garbage Collection, движок не выполняет автоматическую и мгновенную очистку самих записей в map. Запись, содержащая как слот ключа, так и связанные с ним данные, остается выделенной в контейнере map.

Если ваш код попытается получить доступ, оценить или изменить этот ключ после ухода игрока, runtime в Verse попытается разыменовать null-объект или недействительный объект игрока. Вместо корректной обработки ошибки, runtime вызывает сбой или генерирует неперехватываемое исключение (uncatchable exception). Система ожидает, что вы будете явно обрабатывать переходы жизненного цикла, а не полагаться на автоматическую очистку.

Почему weak_maps не очищают записи игроков автоматически

Чтобы понять, почему это происходит, нужно посмотреть на назначение weak_map в UEFN. В отличие от стандартных сред программирования, где weak maps выступают в роли временного кэша памяти, Verse использует weak_map(player, t) в первую очередь как шлюз для персистентных данных игрока.

Сохранение данных между игровыми сессиями

Когда вы используете weak_map(player, t), объявленный в области видимости модуля (module scope), движок связывает эти значения с персистентной облачной базой данных Epic. Если игрок покидает матч и возвращается через три дня, движок сопоставляет его player ID с ключом в персистентной map для восстановления прогресса.

Если бы движок автоматически удалял запись игрока из map в момент его выхода, map теряла бы все персистентные данные. Уровни, кастомные валюты и разблокированные предметы сбрасывались бы до нуля каждый раз, когда игрок отключается или сталкивается с сетевым таймаутом. Таким образом, база данных спроектирована так, чтобы сохранять эти записи нетронутыми именно потому, что они должны переживать отключения игроков.

Ограниченный жизненный цикл объектов игрока

Когда игрок покидает матч, его активный объект сессии в playspace уничтожается. Физическая ссылка player, хранящаяся в вашем Verse-коде, превращается в недействительный указатель (dead handle).

Поскольку ключ внутри map теперь указывает на недействительный, неактивный объект, запрос к map с этой устаревшей ссылкой завершится ошибкой. Движок не занимается активным сканированием и удалением неактивных ключей из map в реальном времени. Вместо этого он оставляет их в неактивном состоянии, поэтому ручное управление необходимо для предотвращения накопления устаревших ссылок (stale references).

Последствия: утечки памяти, устаревшие данные и сбои серверов

Отсутствие очистки записей игроков приводит к трем отдельным проблемам, которые снижают производительность игры и стабильность серверов при длительных матчах.

  • Утечка устаревших данных (Stale Data Leakage): Если один игрок выходит, а другой присоединяется, новый игрок может унаследовать данные сессии старого игрока, если движок повторно использует внутренние слоты игроков. Это приводит к багам состояния, например, когда новые игроки появляются с полным инвентарем или некорректной статистикой матча.
  • Накопление памяти: Хотя один boolean или integer занимает ничтожно мало места, хранение сложных структур для 50 игроков в лобби с большой вместимостью может увеличить потребление памяти. За 4 часа работы сервера это накопление может снизить серверный tick rate.
  • Ошибки поиска (Look-up Failures): Попытка запросить статус неактивного игрока или вызвать функции по устаревшей ссылке на игрока приводит к немедленному runtime-сбою.

Достижение лимитов Epic Cloud Save

UEFN накладывает строгие ограничения на персистентные данные. Вы ограничены максимум 4 персистентными weak_maps на остров, а размер индивидуальной записи каждого игрока не может превышать 256 KB.

Используя персистентную weak_map для хранения временных состояний сессии, вы впустую тратите это ценное пространство базы данных. Каждое обновление записывается в базу данных Epic, что создает риск ограничения скорости записи (write-throttling penalty) или превышения лимита в 256 KB, что вызывает runtime-ошибку при попытке записать больше данных.

Пошаговое руководство: безопасное управление состояниями игровых сессий

Чтобы управлять состояниями игроков без риска утечек памяти или раздувания базы данных, вы должны отделить временные данные сессии (transient session data) от персистентных облачных данных. Временные данные следует хранить в стандартных, неперсистентных 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: Создание Manager Device

Создайте creative device, который будет выступать в роли координатора. Он будет содержать изменяемую (mutable) неперсистентную map активных игроков. Поскольку стандартные maps в Verse неизменяемы (immutable), мы объявляем переменную 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(), вам придется реконструировать 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 может освободить ресурсы игрока, не оставляя устаревших записей в вашем игровом цикле.

Если вы хотите отслеживать кастомную телеметрию во время этих переходов жизненного цикла, также следует помнить о лимитах, таких как ограничение в 32 символа на имя события аналитики в Verse, при передаче данных о длительности сессий или статистике валюты на внешние Backend-системы.

Best Practices по управлению состоянием в Verse

Чтобы обеспечить стабильность и производительность ваших серверов UEFN, следуйте этим рекомендациям по управлению данными игроков:

  1. Разделяйте сессионные и персистентные данные: Никогда не храните кратковременные переменные (такие как текущее здоровье в матче, счет раунда или временные координаты) в персистентной weak_map. Держите временные состояния (transient states) в стандартной изменяемой map, обернутой в класс-менеджер.
  2. Проверяйте активность игрока с помощью IsActive: Прежде чем получать или изменять данные игрока в любой map, убедитесь, что он всё еще присутствует в playspace, используя запрос IsActive[]. Если IsActive[] возвращает false, отмените поиск и запустите событие очистки.
  3. Контролируйте размер данных с помощью FitsInPlayerMap: При записи в персистентную weak_map вызывайте FitsInPlayerMap(), чтобы подтвердить, что обновление не превысит лимит в 256 KB, предотвращая возникновение runtime-исключений.
  4. Объединяйте свои maps: Не создавайте отдельные maps для каждой переменной. Определите один класс, содержащий все переменные игрока, и сопоставьте игрока с этим классом. Это минимизирует количество ваших maps и поможет уложиться в лимит острова, равный четырем персистентным weak_maps.

Перенос сложных задач на надежный облачный Backend

Управление жизненными циклами сессий игроков, лимитами баз данных и логикой ручной очистки в Verse может быстро стать сложным процессом. Если вам нужно реализовать кросс-сессионный прогресс, глобально синхронизированные инвентари или региональный Matchmaking, ручное управление этими состояниями потребует настройки вебхуков, масштабирования внешних баз данных и обеспечения синхронизации между серверами.

С horizOn сложности с Backend решаются автоматически. Интегрировав horizOn SDK в свой игровой сервер, вы можете переложить управление сессиями игроков на выделенную облачную базу данных. При отключении игрока horizOn запускает автоматическую очистку сессии, обновляет глобальные базы данных и синхронизирует записи инвентаря между серверами (server instances) без превышения лимитов памяти Verse в 256 KB и риска runtime-сбоев.

Готовы масштабировать свой UEFN Backend? Попробуйте horizOn бесплатно или изучите API-документацию.


Источник: When using weak maps, does a player's entry in the map automatically get removed on them leaving the game?