Powrót do Bloga

Naprawa buga ze znikającymi encjami UEFN Scene Graph: Przewodnik po Verse dla zachowania Persistent World States

Opublikowano 13 czerwca 2026
Naprawa buga ze znikającymi encjami UEFN Scene Graph: Przewodnik po Verse dla zachowania Persistent World States

W skrócie

Artykuł szczegółowo opisuje uciążliwy błąd replikacji w nowym systemie UEFN Scene Graph, który powoduje znikanie obiektów wizualnych po teleportacji gracza przy jednoczesnym zachowaniu ich kolizji. Autor przedstawia rozwiązanie tego problemu w języku Verse, polegające na wymuszeniu odświeżenia stanu sieciowego encji poprzez szybkie przełączenie jej widoczności. Wskazano również na ograniczenia pamięciowe serwera przy dużej liczbie obiektów oraz przedstawiono korzyści z zastosowania chmurowego backendu horizOn do trwałego przechowywania współrzędnych przestrzennych.

Jeśli Twoi gracze teleportują się na mapie w UEFN, po powrocie najprawdopodobniej zastaną swoje starannie rozmieszczone meble niewidzialnymi i nieaktywnymi, choć wciąż fizycznie blokującymi drogę niczym niewidzialne ściany (ghost walls). Ten frustrujący bug replikacji nęka deweloperów tworzących gry typu sandbox, tycoon czy produkcje oparte na budowaniu w UEFN od czasu aktualizacji v41.00. Budujesz system pozwalający graczom na niestandardowe rozmieszczanie mebli, automatów arkadowych czy siatek dekoracyjnych (meshes) za pomocą encji Scene Graph, i wszystko działa bez zarzutu w warunkach lokalnej rozgrywki. Jednak w momencie, gdy gracz teleportuje się do minigry, lobby lub innej sekcji wyspy, a następnie powraca, meshe znikają.

To, co czyni ten problem szczególnie zdumiewającym, to zachowanie danych kolizji. Gracz może podejść do miejsca, w którym znajdował się obiekt, uderzyć w niewidzialną ścianę i stanąć na czymś, co powinno być solidnym krzesłem lub szafką. Jednak nie może tego zobaczyć, a komponenty interakcji sterowane przez Verse nie chcą się uruchomić. Model wizualny zniknął, ale reprezentacja fizyki pozostaje wypalona (baked) w siatce World Partition.

Aby zrozumieć, dlaczego tak się dzieje, musimy zajrzeć pod maskę i przyjrzeć się temu, jak UEFN zarządza pamięcią i replikacją. Standardowi aktorzy Unreal Engine używają starszego network replication graph, aby określić, co powinno być renderowane na ekranie gracza na podstawie odległości kamery i istotności (relevancy). Wraz z wprowadzeniem nowego systemu Scene Graph w UEFN, Epic Games próbowało usprawnić projektowanie gier oparte na komponentach. Wprowadziło to jednak niezgodność między tym, jak serwer buforuje dynamiczne struktury encji, a tym, jak klient wczytuje i usuwa (streams in and out) zasoby wizualne z pamięci GPU.

Pod maską: Mechanika cullingu w UEFN Scene Graph

Gdy gracz znajduje się w standardowej odległości renderowania od encji — zazwyczaj około 15 000 do 20 000 Unreal Units (150-200 metrów) — replication graph serwera aktywnie nadaje aktualizacje współrzędnych, mesha i stanu komponentów do klienta. Klient przetwarza te pakiety sieciowe i odpowiednio renderuje wizualne komponenty mesha.

W momencie, gdy gracz się teleportuje, dzieje się szybko kilka rzeczy po sobie:

  • Relevancy Cutoff: Viewport klienta natychmiast przesuwa się o tysiące jednostek dalej. Replication graph oznacza oryginalne encje Scene Graph jako „out of relevancy” (poza zasięgiem istotności).
  • GPU Garbage Collection: Aby utrzymać wysoką płynność (framerate) na słabszych urządzeniach, takich jak urządzenia mobilne i konsole, klient natychmiast wyładowuje wizualne reprezentacje tych encji oznaczonych jako out-of-relevancy, oczyszczając pamięć GPU z komponentów static mesh.
  • Collision Caching: W przeciwieństwie do wizualnych meshy, geometria kolizji jest zarządzana przez silnik Chaos Physics. Fizyka jest grupowana w zgrubne bloki przestrzenne (siatki kolizji HLOD) lub buforowana lokalnie na kliencie, aby zapobiec zapadaniu się gracza pod podłogę podczas skoków opóźnień sieciowych (network spikes).

Gdy gracz teleportuje się z powrotem do oryginalnych współrzędnych, klient oczekuje sekwencji handshake w celu odtworzenia komponentów wizualnych. Jednak ze względu na bug replikacji w warstwie sieciowej Scene Graph, serwer zakłada, że klient nadal zachowuje zbuforowany stan wizualny encji. Ponieważ serwer uważa, że klient posiada encje, nie wysyła replikacyjnych RPC spawnu. Klient zostaje z blokiem kolizji fizycznej, ale bez wizualnego mesha i bez aktywnego połączenia z komponentami interakcji.

Jeśli inny gracz pozostanie w tym obszarze, gdy pierwszy się teleportuje, serwer utrzymuje kanał replikacji aktywny dla tych encji. Kiedy pierwszy gracz powraca, nadal widzi obiekty, ponieważ aktywny strumień replikacji (replication stream) dla drugiego gracza zmusza serwer do ciągłego nadawania aktualizacji do wszystkich połączonych klientów. Jednak gdy obaj gracze opuszczą ten obszar i wrócą, stan psuje się u każdego.

Głęboka analiza techniczna: Dlaczego Replication Graph zawodzi przy powrocie

Kod sieciowy Unreal Engine opiera się na ścisłym systemie network relevancy. W środowiskach multiplayer serwer nie może replikować każdego aktora do każdego gracza; spowodowałoby to nasycenie przepustowości (bandwidth) klienta i zawieszenie wątku serwera. Zamiast tego serwer używa klasy UReplicationGraph do przypisywania aktorów do komórek siatki przestrzennej.

W przypadku statycznych, wcześniej umieszczonych aktorów na mapie, UEFN używa World Partition do strumieniowania meshy na kliencie. Jednak dynamiczne encje spawnowane przez Verse lub umieszczane przez graczy w czasie rzeczywistym (runtime) istnieją w stanie przejściowym (transient). Te przejściowe encje nie korzystają ze statycznych, prekompilowanych siatek strumieniowania. Zamiast tego polegają na dynamicznych testach net relevancy.

Gdy gracz się teleportuje, jego połączenie sieciowe przechodzi poważną zmianę stanu. Serwer musi zamknąć kanały replikacji dla starej lokalizacji i otworzyć nowe kanały dla nowej lokalizacji. Ta szybka zmiana może prowadzić do konfliktów priorytetyzacji pakietów. Serwer traktuje priorytetowo szybko zmieniające się elementy rozgrywki (takie jak pozycja gracza, prędkość poruszania się i strzelanie z broni) ponad statycznymi komponentami wizualnymi.

Ta kongestia sieciowa często skutkuje utraconymi pakietami stanu, podobnie jak w przypadku problemów z synchronizacją, które szczegółowo opisujemy w naszym przewodniku o tym, jak naprawić desynchronizację pozycji gracza w UEFN i Unreal Engine multiplayer. W przypadku buga Scene Graph, replication graph serwera nie dokonuje ponownej oceny relevancy odrzuconych (culled) encji dla powracającego gracza. Ponieważ serwer nie rejestruje braku encji u klienta, pomija wysyłanie niezbędnych aktualizacji właściwości. Encja po stronie klienta istnieje w stanie „zombie”: żywa w wątku fizyki, ale martwa w wątkach renderowania i interakcji.

Rozwiązanie buga: Obejście odświeżające replikację za pomocą Verse

Aby naprawić bug uefn scene graph entities disappearing, musimy zmusić serwer do oznaczenia tych encji jako „dirty” (brudne), gdy gracz teleportuje się z powrotem w ich zasięg. Wymuszając aktualizację właściwości, wyzwalamy replication graph do wysłania nowego pakietu stanu do powracającego klienta, co odbudowuje mesha oraz komponenty interaktywne.

Najbardziej niezawodnym sposobem na osiągnięcie tego jest stworzenie dynamicznego menedżera encji w Verse. Menedżer ten śledzi pozycje graczy, wykrywa zdarzenia teleportacji (nagłe zmiany współrzędnych) i wykonuje lokalne przełączenie widoczności lub transformacji na pobliskich encjach, aby wymusić odświeżenie sieciowe.

Poniżej znajduje się kompletny, poprawny składniowo skrypt Verse, który implementuje ten wzorzec rekonstrukcji stanu.

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()

Jak działa ten kod

Urządzenie replication_refresher_device działa poprzez uruchomienie asynchronicznej pętli, która odpytuje (polls) pozycje graczy. Porównując obecny wektor współrzędnych gracza z jego wektorem współrzędnych sprzed 0,5 sekundy, skrypt potrafi odróżnić zwykłe poruszanie się od szybkiej teleportacji.

Po wyzwoleniu zdarzenia teleportacji funkcja HandlePlayerTeleport ewaluuje wszystkie zarejestrowane dynamiczne rekwizyty. Dla każdego rekwizytu w promieniu 200 metrów od nowej pozycji gracza wywoływana jest funkcja ForceEntityReplicationRefresh. Przełączenie Hide() i Show() na creative_prop zmusza serwer do zaktualizowania flag net-dirty na powiązanym aktorze C++. To z kolei zmusza replication graph do przesłania pełnego payloadu aktora na urządzenie klienta, z powodzeniem odbudowując usunięte (culled) static meshe i kanały interaktywne.

Architektura Persistent World States: Ograniczenia pamięci Verse

Choć obejście w Verse rozwiązuje problem z renderowaniem na małych i średnich mapach, ujawnia ono głębsze ograniczenie architektury runtime UEFN. Jeśli Twoja gra opiera się na setkach obiektów umieszczanych przez graczy, śledzenie ich wszystkich w tablicach Verse może szybko wyczerpać budżet pamięci serwera.

Każda instancja klasy dynamicznej, współrzędna wektora i struktura śledząca zmienne w Verse zużywa pamięć w czasie rzeczywistym (runtime memory). Serwery Fortnite działają pod ścisłymi ograniczeniami zasobów. Jeśli przekroczysz maksymalną dozwoloną liczbę instrukcji lub limity alokacji pamięci, Twój serwer doświadczy poważnego spadku wydajności lub całkowicie się zawiesi, co doprowadzi do błędu network driver timeouts, który wyrzuci graczy z powrotem do lobby.

Co więcej, zmienne Verse nie są zachowywane, gdy instancja serwera przechodzi w stan hibernacji lub wyłącza się. Jeśli serwer stanie się nieaktywny, ponieważ wszyscy gracze go opuścili (proces ten przeanalizowaliśmy w naszej głębokiej analizie dotyczącej the UEFN server performance exploit), cały układ umieszczonych przez graczy automatów do gry i mebli zostanie trwale utracony.

Aby stworzyć naprawdę trwałą (persistent) grę, musisz przechowywać te dane o układzie przestrzennym na zewnętrznym backendzie. Samodzielne przygotowanie takiego rozwiązania to ogromne przedsięwzięcie inżynieryjne:

  1. Infrastructure Provisioning: Musisz hostować bazę danych (np. PostgreSQL lub MongoDB) zdolną do skalowania się do tysięcy jednoczesnych operacji odczytu i zapisu.
  2. API Gateway: Musisz zbudować bezpieczny serwer HTTP, który tłumaczy żądania Verse na zapytania do bazy danych, obsługuje uwierzytelnianie graczy i zarządza rate limitingiem.
  3. Network Resilience: Klienci HTTP w Verse są mocno ograniczeni i nie posiadają automatycznych retry policies ani connection pooling. Musisz napisać skomplikowany kod boilerplate, aby obsłużyć utratę pakietów i limity czasu serwera (server timeouts).

Dla małego zespołu indie spędzenie od 4 do 6 tygodni na pisaniu kodu integracji z bazą danych zamiast na szlifowaniu rozgrywki (polishing gameplay) to ogromne marnotrawstwo zasobów.

Seamless Persistence: Jak horizOn rozwiązuje synchronizację stanu

W tym miejscu z pomocą przychodzi horizOn. Jako dedykowany Backend-as-a-Service zaprojektowany specjalnie dla deweloperów gier, horizOn oferuje prekonfigurowane, niskolatencyjne (low-latency) przechowywanie stanów przestrzennych, które integruje się bezpośrednio z UEFN i Verse.

Zamiast stale utrzymywać setki aktywnych encji Scene Graph załadowanych w obszarze pamięci serwera, możesz zapisać ich współrzędne przestrzenne, rotacje i niestandardowe atrybuty w chmurowej bazie danych horizOn. Gdy gracz teleportuje się daleko, możesz bezpiecznie zdespawnować lokalne encje, aby zwolnić pamięć serwera Fortnite. Kiedy gracz wraca, odpytujesz bazę danych i rekonstruujesz tylko te encje, które znajdują się w jego najbliższym sąsiedztwie.

Dzięki wykorzystaniu horizOn rozwiązujesz jednocześnie bug cullingu oraz problem z pamięcią serwera. Serwer replikuje tylko to, co gracz może rzeczywiście zobaczyć, zmniejszając zużycie przepustowości sieciowej (network bandwidth) z ~120 KB/s do mniej niż ~25 KB/s na jednego klienta. Jeśli instancja serwera przejdzie w stan hibernacji lub zostanie zrestartowana, postęp gracza zostanie natychmiast przywrócony z chmury.

Integracja z horizOn wymaga zaledwie kilku linii kodu w Verse przy użyciu standardowych żądań 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}")

Dzięki temu, że horizOn zajmuje się infrastrukturą, zyskujesz niezawodną trwałość danych (data persistence) i optymalną wydajność serwera bez konieczności pisania ani jednej linijki kodu dla backendu.

Dobre praktyki zarządzania replikacją w UEFN Scene Graph

Jeśli obecnie tworzysz grę typu sandbox lub tycoon w UEFN, zastosuj te cztery praktyczne wskazówki, aby zapewnić synchronizację stanów świata i wydajne działanie serwerów:

  1. Wdróż dynamiczny spawning i despawning: Nie trzymaj setek interaktywnych obiektów załadowanych w świecie jednocześnie. Napisz skrypt Verse, który spawnuje meshe tylko wtedy, gdy gracz jest blisko, i usuwa je (despawns), gdy odchodzi, oszczędzając cenne cykle CPU serwera.
  2. Używaj Gameplay Tags do zapytań wsadowych (batch querying): Unikaj śledzenia każdego dynamicznego aktora w pojedynczej globalnej tablicy Verse. Zamiast tego przypisz tagi rozgrywki (gameplay tags) do swoich rekwizytów (props) w Scene Graph. Użyj systemu zapytań o tagi (tag query system) w UEFN, aby dynamicznie wyszukiwać i odświeżać rekwizyty na podstawie granic przestrzennych.
  3. Oddziel kolizję od dynamicznych aktorów: Jeśli obiekt nie musi się poruszać, użyj statycznego, wcześniej umieszczonego niewidzialnego pudła kolizyjnego (collision box) w układzie mapy. Do dynamicznej encji Scene Graph dołącz tylko wizualny mesh oraz komponent interakcji Verse. Gwarantuje to, że gracze nigdy nie uderzą w niewidzialne ściany kolizji, gdy replikacja zawiedzie.
  4. Przenieś zarządzanie stanem do chmury: W przypadku każdej gry z trwałą mechaniką budowania wdróż integrację z backendem, takim jak horizOn, na wczesnym etapie produkcji. Przechowywanie danych współrzędnych w chmudze zapobiega przepełnieniu lokalnego budżetu pamięci i zapewnia zachowanie postępów graczy pomiędzy różnymi instancjami serwera.

Chcesz tworzyć skalowalne, trwałe gry multiplayer w UEFN bez problemów związanych z backendem? Zarejestruj się za darmo w horizOn lub zapoznaj się z dokumentacją API, aby rozpocząć.


Źródło: Scene graph entities are buggy when player leaves render distance from v41.00