Назад к блогу

Устранение бага с исчезновением Scene Graph Entities в UEFN: руководство по сохранению состояний мира на Verse

Опубликовано 13 июня 2026 г.
Устранение бага с исчезновением Scene Graph Entities в UEFN: руководство по сохранению состояний мира на Verse

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

Статья посвящена багу репликации в UEFN Scene Graph, при котором динамические объекты становятся невидимыми после телепортации игрока, оставляя лишь невидимую физическую коллизию. Подробно разбираются технические причины проблемы, связанные с GPU Garbage Collection и особенностями работы сетевого слоя релевантности. В качестве решения приводится пример скрипта на Verse для принудительного обновления состояния сети, а также предлагается архитектурный паттерн с переносом пространственных данных в облачный Backend-as-a-Service horizOn для экономии памяти сервера.

Если ваши игроки телепортируются по карте UEFN, они, скорее всего, вернутся и обнаружат, что тщательно расставленная ими мебель стала невидимой и неинтерактивной, но при этом физически блокирует их, подобно призрачным стенам. Этот досадный replication bug преследует разработчиков, создающих игры в жанрах sandbox, tycoon или проекты с упором на строительство в UEFN со времени обновления v41.00. Вы создаете систему, позволяющую игрокам кастомно размещать мебель, аркадные автоматы или декоративные meshes с помощью Scene Graph entities, и всё отлично работает при локальном тестировании. Однако в тот момент, когда игрок телепортируется в мини-игру, лобби или в другую часть острова, а затем возвращается, meshes исчезают.

Что делает эту проблему особенно загадочной, так это поведение данных collision. Игрок может подойти к месту, где находился объект, врезаться в невидимую стену и встать на то, что должно быть твердым стулом или шкафом. Однако он не видит объект, а его interaction-компоненты, управляемые Verse, отказываются срабатывать. Визуальная модель исчезла, но физическое представление остается запеченным в сетке World Partition.

Чтобы понять, почему это происходит, нужно заглянуть под капот того, как UEFN управляет памятью и replication. Стандартные Unreal Engine actors используют устаревший сетевой replication graph для определения того, что должно рендериться на экране игрока в зависимости от расстояния до камеры и relevancy. С внедрением новой системы Scene Graph в UEFN Epic Games попыталась упростить компонентно-ориентированный дизайн игр. Однако это привело к несоответствию между тем, как сервер кэширует динамические структуры entities, и тем, как клиент загружает и выгружает визуальные ассеты из памяти GPU.

Под капотом: механика UEFN Scene Graph culling

Когда игрок находится на стандартной дистанции отрисовки от entity — обычно около 15 000–20 000 Unreal Units (150–200 метров) — replication graph сервера активно транслирует обновления координат, mesh и состояний компонентов на клиент. Клиент обрабатывает эти сетевые пакеты и соответствующим образом рендерит компоненты visual mesh.

В момент телепортации игрока в быстрой последовательности происходит несколько событий:

  • Relevancy Cutoff: Viewport клиента мгновенно смещается на тысячи юнитов. Replication graph помечает исходные Scene Graph entities как «out of relevancy».
  • GPU Garbage Collection: Для поддержания высокого фреймрейта на менее мощных устройствах, таких как мобильные телефоны и консоли, клиент немедленно выгружает визуальные представления этих out-of-relevancy сущностей, очищая static mesh компоненты из памяти GPU.
  • Collision Caching: В отличие от visual meshes, геометрия collision управляется движком Chaos Physics. Физические структуры группируются в грубые пространственные блоки (HLOD collision grids) или кэшируются локально на клиенте, чтобы предотвратить падение игрока сквозь пол во время сетевых сбоев. Вот почему collision остается нетронутой даже после того, как visual mesh подвергается culling.

Когда игрок телепортируется обратно на исходные координаты, клиент ожидает последовательность handshake для воссоздания визуальных компонентов. Однако из-за replication bug в сетевом слое Scene Graph сервер считает, что клиент все еще сохраняет закэшированное визуальное состояние entities. Поскольку сервер полагает, что у клиента есть эти entities, он не отправляет replication spawn RPCs. Клиент остается с физическим блоком collision, но без visual mesh и без активного подключения к interaction-компонентам.

Если другой игрок остается в этой области в то время как первый телепортируется, сервер сохраняет канал репликации активным для этих entities. Когда первый игрок возвращается, он все еще видит объекты, поскольку активный стрим репликации для второго игрока заставляет сервер продолжать транслировать обновления для всех подключенных клиентов. Однако как только оба игрока покидают область и возвращаются, состояние ломается для всех.

Технический разбор: почему replication graph дает сбой при повторном входе

Сетевой код Unreal Engine опирается на строгую систему сетевой релевантности (network relevancy). В multiplayer-среде сервер не может реплицировать каждого actor каждому игроку; это перегрузило бы пропускную способность клиента и вызвало бы сбой серверного потока. Вместо этого сервер использует UReplicationGraph для распределения actors по ячейкам пространственной сетки.

Для статических, заранее размещенных на карте actors UEFN использует World Partition для стриминга meshes на клиенте. Но динамические entities, созданные через Verse или размещенные игроками во время runtime, находятся в транзитном (transient) состоянии. Эти transient entities не используют преимущества статических скомпилированных сеток стриминга. Вместо этого они полагаются на проверки dynamic net relevancy.

Когда игрок телепортируется, его сетевое соединение претерпевает серьезное изменение состояния. Сервер должен закрыть каналы репликации для старого местоположения и открыть каналы для нового. Этот быстрый переход может привести к конфликтам приоритетов пакетов. Сервер отдает приоритет быстро меняющимся геймплейным элементам (таким как положение игрока, скорость движения и стрельба), а не статическим визуальным компонентам.

Такая перегрузка сети часто приводит к потере пакетов состояния, аналогично проблемам синхронизации, которые мы подробно описываем в нашем руководстве по устранению десинхронизации местоположения игроков в UEFN и Unreal Engine multiplayer. В случае с багом Scene Graph replication graph сервера не может повторно оценить relevancy отброшенных (culled) entities для возвращающегося игрока. Поскольку сервер не фиксирует отсутствие сущности у клиента, он пропускает отправку необходимых обновлений свойств. Сущность на стороне клиента существует в состоянии «зомби»: она жива в физическом потоке, но мертва для потоков рендеринга и взаимодействия (rendering и interaction threads).

Решение бага: обходной путь для обновления репликации на базе Verse

Чтобы исправить баг uefn scene graph entities disappearing, мы должны заставить сервер пометить эти entities как «dirty», когда игрок телепортируется обратно в зону видимости. Принудительно обновляя свойства, мы заставляем replication graph отправить свежий пакет состояния возвращающемуся клиенту, который восстанавливает mesh и интерактивные компоненты.

Самый надежный способ добиться этого — создать динамический entity manager в Verse. Этот менеджер отслеживает координаты игроков, обнаруживает события телепортации (внезапные изменения координат) и выполняет локальное переключение видимости или трансформации близлежащих entities, чтобы принудить сеть к обновлению.

Ниже приведен полный, синтаксически корректный скрипт на Verse, реализующий этот паттерн восстановления состояния.

using { /Fortnite.com/Devices }
using { /Fortnite.com/Characters }
using { /Verse.org/Simulation }
using { /Verse.org/SpatialMath }

# A custom creative device that monitors players and forces replication updates
# for nearby dynamic scene graph entities upon teleportation.
replication_refresher_device := class(creative_device):

    # Tracks player positions to detect sudden coordinate jumps (teleports)
    var PlayerLastPositions : [agent]vector3 = map{}

    # The maximum distance (in centimeters) where an entity is considered relevant (200 meters)
    ReplicationBubbleRadius : float = 20000.0

    # Minimum movement distance (in centimeters) to classify as a teleport (e.g., 50 meters)
    TeleportDistanceThreshold : float = 5000.0

    # Polling frequency in seconds. Checking twice a second is highly efficient.
    CheckInterval : float = 0.5

    # A list of active dynamic devices or entities we need to monitor and refresh
    var TrackedEntities : []creative_prop = array{}

    # Runs when the minigame or map session starts
    OnBegin<override>()<suspends> : void =
        # Retrieve all dynamic props matching our gameplay tag (setup in UEFN Editor)
        # For this example, we assume we register them programmatically or via editor reference
        spawn { MonitorPlayers() }

    # Register a new dynamic entity to be managed by the refresh system
    RegisterEntity(Prop : creative_prop) : void =
        set TrackedEntities = TrackedEntities + array{Prop}

    # Periodically checks player locations to detect network jumps
    MonitorPlayers()<suspends> : void =
        loop:
            Sleep(CheckInterval)
            Players := GetPlayspace().GetPlayers()
            for (Player : Players):
                if (FortCharacter := Player.GetFortCharacter[]):
                    CurrentPos := FortCharacter.GetTransform().Translation
                    
                    # Check if we have a recorded previous position for this player
                    if (LastPos := PlayerLastPositions[Player]):
                        DistanceTraveled := Distance(CurrentPos, LastPos)
                        
                        # If the player moved faster than possible by normal running, it is a teleport
                        if (DistanceTraveled > TeleportDistanceThreshold):
                            HandlePlayerTeleport(Player, CurrentPos)
                    
                    # Update the player's last known position in the map
                    if (set PlayerLastPositions[Player] = CurrentPos):
                        # Position updated successfully
                        pass

    # Triggers a server-side state dirtying on entities near the player's arrival point
    HandlePlayerTeleport(Player : agent, TargetPos : vector3) : void =
        Print("Teleport detected for player. Triggering scene graph replication sync...")
        
        # Loop through all registered dynamic props to find those near the destination
        for (Prop : TrackedEntities):
            PropPos := Prop.GetTransform().Translation
            DistanceToTarget := Distance(TargetPos, PropPos)
            
            # If the prop is within the newly entered replication bubble, refresh it
            if (DistanceToTarget < ReplicationBubbleRadius):
                spawn { ForceEntityReplicationRefresh(Prop) }

    # Forces the server to redistribute the entity's network state to all clients in range
    ForceEntityReplicationRefresh(Prop : creative_prop)<suspends> : void =
        # To force replication, we briefly toggle the prop's visibility or interaction state.
        # This flags the actor as \"dirty\" in the server's replication queue.
        # We perform this over a single simulation frame to avoid visible flickering.
        Prop.Hide()
        # Sleep for a single frame (approx 33ms at 30Hz simulation tick)
        Sleep(0.0)
        Prop.Show()

Как работает этот код

Устройство replication_refresher_device работает путем запуска асинхронного цикла, который опрашивает координаты игроков. Сравнивая текущий вектор координат игрока с его вектором координат 0,5 секунды назад, скрипт может отличить обычное перемещение от высокоскоростной телепортации.

При срабатывании события телепортации функция HandlePlayerTeleport оценивает все зарегистрированные dynamic props. Для любого пропса в радиусе 200 метров от новой позиции игрока вызывается метод ForceEntityReplicationRefresh. Переключение Hide() и Show() для creative_prop заставляет сервер обновить флаги net-dirty базового C++ actor. Это принуждает replication graph отправить полный payload актора на устройство клиента, успешно восстанавливая отброшенные (culled) static meshes и интерактивные каналы.

Проектирование персистентных состояний мира: лимиты памяти Verse

Хотя обходной путь на Verse решает проблему рендеринга для карт малого и среднего размера, он выявляет более глубокое ограничение архитектуры runtime UEFN. Если ваша игра опирается на сотни размещенных игроками предметов, отслеживание их всех в массивах Verse может быстро исчерпать бюджет памяти сервера.

Каждый экземпляр динамического класса, вектор координат и переменная структура отслеживания в Verse потребляют память runtime. Серверы Fortnite работают в условиях жестких ограничений ресурсов. Превышение максимального количества инструкций или лимитов выделения памяти приведет к сильному падению производительности сервера или его полному падению (crash), что вызовет network driver timeouts, выбрасывающие игроков обратно в лобби.

Кроме того, переменные Verse не сохраняются при гибернации (hibernation) или завершении работы инстанса сервера. Если сервер переходит в неактивное состояние из-за того, что все игроки вышли (этот процесс проанализирован в нашем подробном разборе the UEFN server performance exploit), вся планировка размещенных игроками аркадных автоматов и мебели теряется навсегда.

Чтобы создать по-настоящему persistent игру, вам необходимо сохранять эти данные пространственной планировки на внешнем backend. Самостоятельная настройка такой системы — масштабная инженерная задача:

  1. Infrastructure Provisioning: Вам придется развернуть базу данных (например, PostgreSQL или MongoDB), способную масштабироваться под тысячи одновременных операций чтения и записи.
  2. API Gateway: Вам потребуется создать безопасный HTTP-сервер, который будет транслировать запросы Verse в запросы к базе данных, обрабатывать аутентификацию игроков и управлять rate limiting.
  3. Network Resilience: HTTP-клиенты Verse сильно ограничены и не поддерживают автоматические политики повторных попыток (retry policies) или connection pooling. Вам придется написать сложный boilerplate-код для обработки потери пакетов и таймаутов сервера.

Для небольшой инди-команды тратить от 4 до 6 недель на написание кода интеграции с базой данных вместо полировки геймплея — это огромная трата ресурсов.

Бесшовная персистентность: как horizOn решает проблему синхронизации состояний

Именно здесь на помощь приходит horizOn. Как специализированный Backend-as-a-Service, созданный специально для разработчиков игр, horizOn предоставляет преднастроенное пространственное хранилище состояний с низкой задержкой (low-latency), которое интегрируется напрямую с UEFN и Verse.

Вместо того чтобы постоянно держать сотни активных scene graph entities загруженными в памяти сервера, вы можете сохранять их пространственные координаты, вращение (rotation) и кастомные атрибуты в облачную базу данных horizOn. Когда игрок телепортируется, вы можете безопасно вызвать despawn локальных entities, чтобы освободить память сервера Fortnite. Когда игрок возвращается, вы запрашиваете данные из базы и воссоздаете только те entities, которые находятся в его непосредственной близости.

Используя horizOn, вы одновременно решаете culling bug и проблему с памятью сервера. Сервер реплицирует только то, что игрок может видеть в данный момент, снижая пропускную способность сети с ~120 КБ/с до менее чем ~25 КБ/с на каждого клиента. Если инстанс сервера уходит в гибернацию или перезапускается, прогресс игрока мгновенно восстанавливается из облака.

Интеграция horizOn требует всего нескольких строк кода на Verse с использованием стандартных HTTP-запросов:

# Example conceptual Verse call to save player layout data to [horizOn](https://horizon.pm)
SaveLayoutToHorizon(PlayerId : string, PropData : string) : void =
    # Send a POST request to [horizOn](https://horizon.pm)'s structured data API
    # [horizOn](https://horizon.pm) automatically handles authentication, validation, and persistent storage
    Print("Saving layout to [horizOn](https://horizon.pm) database for player: {PlayerId}")

Благодаря тому, что horizOn берет на себя управление инфраструктурой, вы получаете надежное хранение данных и оптимальную производительность сервера без необходимости писать backend-код сервера.

Лучшие практики управления репликацией в UEFN Scene Graph

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

  1. Внедрите динамический spawning и despawning: Не держите сотни интерактивных объектов загруженными в мире одновременно. Напишите скрипт на Verse, который выполняет spawn для meshes только тогда, когда игрок находится рядом, и удаляет (despawn) их при его уходе, экономя драгоценные такты CPU сервера.
  2. Используйте gameplay tags для пакетных запросов: Избегайте отслеживания каждого динамического actor в едином глобальном массиве Verse. Вместо этого присваивайте gameplay tags вашим пропсам scene graph. Используйте систему запросов по тегам UEFN для динамического поиска и обновления пропсов на основе spatial bounds.
  3. Разделяйте collision и динамических actors: Если объекту не нужно двигаться, используйте статический, заранее размещенный невидимый collision box в разметке карты. Прикрепляйте visual mesh и компонент взаимодействия Verse только к динамической сущности scene graph. Это гарантирует, что игроки никогда не столкнутся с невидимыми стенами collision в случае сбоя репликации.
  4. Перенесите управление состоянием в облако: Для любой игры с механикой персистентного строительства внедряйте интеграцию с backend, подобную horizOn, на ранних этапах разработки. Хранение координатных данных в облаке предотвращает переполнение локального бюджета памяти и гарантирует сохранность прогресса игроков между запусками инстансов сервера.

Готовы создавать масштабируемые, персистентные multiplayer-игры в UEFN без головной боли по поводу backend? Зарегистрируйтесь в horizOn бесплатно или ознакомьтесь с API documentation, чтобы начать.


Source: Scene graph entities are buggy when player leaves render distance from v41.00