Risolvere il bug della scomparsa delle entities dello Scene Graph di UEFN: una guida in Verse ai persistent world states
In breve
Questo articolo analizza il bug della scomparsa degli elementi visivi delle entities dello Scene Graph in UEFN, causato da problemi di network replication in seguito al teletrasporto dei giocatori. Viene presentato un workaround pratico tramite uno script Verse che forza un refresh dello stato di rete dei prop culled per ricostruirne la mesh e i componenti interattivi. Infine, l'articolo illustra le limitazioni di memoria di Verse per mondi persistenti e suggerisce l'uso del Backend-as-a-Service horizOn per ottimizzare le prestazioni del server e salvare i dati spaziali nel cloud.
Se i tuoi giocatori si teletrasportano all'interno di una mappa UEFN, è probabile che al loro ritorno trovino i mobili posizionati con cura invisibili, non interattivi, ma che continuano a bloccarli fisicamente come pareti fantasma. Questo frustrante bug di replication affligge gli sviluppatori che creano giochi sandbox, tycoon o incentrati sulla costruzione in UEFN fin dall'aggiornamento v41.00. Crei un sistema che consente ai giocatori di posizionare mobili personalizzati, arcade cabinet o mesh decorative utilizzando entities dello Scene Graph, e tutto funziona perfettamente in condizioni di local play. Tuttavia, nel momento in cui un giocatore si teletrasporta via verso un minigioco, una lobby o una sezione separata dell'isola, per poi ritornare, le mesh scompaiono.
Ciò che rende questo problema particolarmente sconcertante è il comportamento dei dati di collisione. Il giocatore può camminare fino al punto in cui si trovava l'oggetto, colpire un muro invisibile e salire sopra quella che dovrebbe essere una sedia o un cabinet solido. Ma non può vederlo, e i suoi componenti di interazione gestiti tramite Verse si rifiutano di attivarsi. Il modello visivo è svanito, eppure la rappresentazione fisica rimane baked nella griglia del World Partition.
Per capire perché questo accade, dobbiamo guardare sotto il cofano di come UEFN gestisce la memoria e la replication. Gli actor standard di Unreal Engine utilizzano il legacy network replication graph per determinare cosa dovrebbe essere renderizzato sullo schermo di un giocatore in base alla distanza della telecamera e alla relevancy. Con l'introduzione del nuovo sistema Scene Graph di UEFN, Epic Games ha cercato di snellire il game design basato su componenti. Tuttavia, ciò ha introdotto una discrepanza tra il modo in cui il server memorizza in cache le strutture delle entità dinamiche e il modo in cui il client esegue lo streaming degli asset visivi all'interno e all'esterno della memoria GPU.
Sotto il cofano: la meccanica del culling dello Scene Graph in UEFN
Quando un giocatore si trova entro la distanza standard di rendering di un'entità — tipicamente intorno a 15.000 - 20.000 Unreal Units (150-200 metri) — il replication graph del server trasmette attivamente gli aggiornamenti delle coordinate, delle mesh e dello stato dei componenti al client. Il client elabora questi pacchetti di rete e renderizza di conseguenza i componenti della mesh visiva.
Nel momento in cui il giocatore si teletrasporta via, accadono diverse cose in rapida successione:
- Relevancy Cutoff: La viewport del client si sposta istantaneamente di migliaia di unità. Il replication graph contrassegna le entities originali dello Scene Graph come "out of relevancy".
- Garbage Collection della GPU: Per mantenere framerate elevati su dispositivi di fascia bassa come mobile e console, il client scarica immediatamente le rappresentazioni visive di queste entities out of relevancy, eliminando i componenti static mesh dalla memoria GPU.
- Collision Caching: A differenza delle mesh visive, la geometria di collisione è gestita dal motore fisico Chaos Physics. Le strutture fisiche sono raggruppate in blocchi spaziali grossolani (griglie di collisione HLOD) o memorizzate nella cache locale del client per evitare che il giocatore cada attraverso il pavimento durante i picchi di rete. Questo è il motivo per cui la collisione rimane intatta anche quando la mesh visiva subisce il culling.
Quando il giocatore si teletrasporta nuovamente alle coordinate originali, il client si aspetta una sequenza di handshake per ricreare i componenti visivi. Tuttavia, a causa di un bug di replication nel network layer dello Scene Graph, il server presume che il client conservi ancora lo stato visivo in cache delle entities. Poiché il server crede che il client disponga delle entities, non invia gli RPC di spawn per la replication. Il client si ritrova così con un blocco di collisione fisica, ma senza mesh visiva e senza alcuna connessione attiva con i componenti di interazione.
Se un altro giocatore rimane nella zona mentre il primo giocatore si teletrasporta altrove, il server mantiene attivo il canale di replication per quelle entities. Quando il primo giocatore ritorna, può ancora vedere gli oggetti perché lo stream di replication attivo per il secondo giocatore costringe il server a continuare a trasmettere aggiornamenti a tutti i client connessi. Tuttavia, una volta che entrambi i giocatori lasciano l'area e vi ritornano, lo stato si interrompe per tutti.
Approfondimento tecnico: perché il replication graph fallisce al rientro
Il codice di rete di Unreal Engine si affida a un rigido sistema di network relevancy. Negli ambienti multiplayer, il server non può replicare ogni actor su ogni giocatore; farlo saturerebbe la banda del client e manderebbe in crash il thread del server. Al contrario, il server utilizza un UReplicationGraph per inserire gli actor all'interno di celle di griglia spaziale.
Per gli actor statici pre-posizionati in una mappa, UEFN utilizza il World Partition per lo streaming delle mesh sul client. Ma le entities dinamiche spawnate tramite Verse o posizionate dai giocatori durante il runtime esistono in uno stato transitorio. Queste entities transitorie non beneficiano delle griglie di streaming statiche precompilate. Si affidano invece a controlli dinamici di net relevancy.
Quando un giocatore si teletrasporta, la sua connessione di rete subisce un importante cambiamento di stato. Il server deve chiudere i canali di replication per la vecchia posizione e aprire quelli per la nuova posizione. Questo rapido cambiamento può portare a conflitti di priorità dei pacchetti. Il server dà la priorità agli elementi di gameplay in rapido movimento (come la posizione del giocatore, la velocità di movimento e lo sparo delle armi) rispetto ai componenti visivi statici.
Questa congestione di rete si traduce spesso in pacchetti di stato persi (dropped packets), analogamente ai problemi di sincronizzazione che descriviamo dettagliatamente nella nostra guida su come risolvere il desync della posizione del giocatore in UEFN e nel multiplayer di Unreal Engine. Nel caso del bug dello scene graph, il replication graph del server non riesce a rivalutare la relevancy delle entities che hanno subito il culling per il giocatore che ritorna. Poiché il server non registra la mancanza dell'entità da parte del client, salta l'invio dei necessari aggiornamenti delle proprietà. L'entità lato client esiste in uno stato "zombie": viva nel thread della fisica, ma morta nei thread di rendering e interazione.
Risolvere il bug: un workaround in Verse per il refresh della replication
Per risolvere il bug delle uefn scene graph entities disappearing, dobbiamo forzare il server a contrassegnare queste entities come "dirty" quando un giocatore si teletrasporta nuovamente nel raggio d'azione. Forzando l'aggiornamento di una proprietà, attiviamo il replication graph per inviare un pacchetto di stato aggiornato al client di ritorno, il quale ricostruisce la mesh e i componenti interattivi.
Il modo più robusto per ottenere questo risultato è creare un entity manager dinamico in Verse. Questo manager traccia le posizioni dei giocatori, rileva gli eventi di teletrasporto (improvvisi cambi di coordinate) e attiva un toggle localizzato della visibilità o del transform sulle entities vicine per forzare un refresh di rete.
Di seguito è riportato uno script Verse completo e sintatticamente corretto che implementa questo pattern di ricostruzione dello stato.
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()
Come funziona il codice
Il replication_refresher_device funziona eseguendo un loop asincrono che effettua il polling delle posizioni dei giocatori. Confrontando il vettore delle coordinate correnti del giocatore con quello di 0,5 secondi prima, lo script può distinguere tra la normale locomozione e il teletrasporto ad alta velocità.
Quando viene attivato un evento di teletrasporto, la funzione HandlePlayerTeleport valuta tutti i prop dinamici registrati. Per qualsiasi prop entro un raggio di 200 metri dalla nuova posizione del giocatore, richiama ForceEntityReplicationRefresh. Il toggle di Hide() e Show() sul creative_prop costringe il server ad aggiornare i flag net-dirty sull'actor C++ sottostante. Ciò costringe il replication graph a inviare l'intero payload dell'actor al dispositivo client, ricostruendo con successo le static mesh culled e i canali interattivi.
Progettare persistent world states: i limiti della memoria di Verse
Sebbene il workaround in Verse risolva il problema di rendering per mappe di dimensioni medio-piccole, esso evidenzia un limite più profondo dell'architettura runtime di UEFN. Se il tuo gioco si affida a centinaia di oggetti posizionati dai giocatori, tenerne traccia in array di Verse può esaurire rapidamente il budget di memoria del server.
Ogni istanza di classe dinamica, coordinata vettoriale e struttura di tracciamento delle variabili in Verse consuma memoria a runtime. I server di Fortnite operano con rigidi vincoli di risorse. Se superi il conteggio massimo di istruzioni consentite o i limiti di allocazione della memoria, il tuo server subirà un grave degrado delle prestazioni o andrà in crash completo, portando a timeout dei driver di rete che rimandano i giocatori alla lobby.
Inoltre, le variabili di Verse non persistono quando un'istanza del server va in ibernazione o si spegne. Se un server diventa inattivo perché tutti i giocatori se ne sono andati (un processo analizzato nel nostro approfondimento su l'exploit delle prestazioni del server UEFN), l'intero layout dei cabinet arcade e dei mobili posizionati dai giocatori viene perso definitivamente.
Per creare un gioco davvero persistente, è necessario memorizzare questi dati di layout spaziale su un backend esterno. Configurare tutto questo da soli rappresenta un enorme sforzo ingegneristico:
- Provisioning dell'infrastruttura: devi ospitare un database (ad esempio PostgreSQL o MongoDB) in grado di scalare per gestire migliaia di letture e scritture simultanee.
- API Gateway: devi creare un server HTTP sicuro che traduca le richieste Verse in query al database, gestisca l'autenticazione dei giocatori e controlli il rate limiting.
- Resilienza di rete: i client HTTP di Verse sono estremamente limitati e non dispongono di policy di retry automatiche o di connection pooling. È necessario scrivere codice boilerplate complesso per gestire la perdita di pacchetti e i timeout del server.
Per un piccolo team indie, spendere dalle 4 alle 6 settimane a scrivere codice di integrazione del database invece di rifinire il gameplay è un enorme spreco di risorse.
Persistenza fluida: come horizOn risolve la sincronizzazione dello stato
È qui che entra in gioco horizOn. In qualità di Backend-as-a-Service dedicato progettato specificamente per gli sviluppatori di giochi, horizOn fornisce uno storage dello stato spaziale preconfigurato e a bassa latenza che si integra direttamente con UEFN e Verse.
Invece di tenere centinaia di entities dello Scene Graph attive caricate nella bolla di memoria del server in ogni momento, puoi salvare le loro coordinate spaziali, rotazioni e attributi personalizzati nel database cloud di horizOn. Quando un giocatore si teletrasporta via, puoi despawnare in sicurezza le entities locali per liberare memoria sul server di Fortnite. Quando il giocatore ritorna, interroghi il database e ricostruisci solo le entities nelle sue immediate vicinanze.
Sfruttando horizOn, risolvi contemporaneamente il bug del culling e il problema della memoria del server. Il server replica solo ciò che il giocatore può effettivamente vedere, riducendo la larghezza di banda di rete da circa 120 KB/s a meno di 25 KB/s per client. Se un'istanza del server va in ibernazione o si riavvia, i progressi del giocatore vengono ripristinati istantaneamente dal cloud.
L'integrazione di horizOn richiede solo poche righe di codice Verse utilizzando richieste HTTP standard:
# 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}")
Con horizOn che gestisce l'infrastruttura, ottieni una persistenza solida dei dati e prestazioni ottimali del server senza dover gestire una sola riga di codice del server backend.
Best practice per la gestione della replication dello Scene Graph in UEFN
Se attualmente stai creando un gioco sandbox o tycoon in UEFN, applica questi quattro consigli pratici per garantire che gli stati del tuo mondo rimangano sincronizzati e che i tuoi server funzionino in modo efficiente:
- Implementa spawn e despawn dinamici: non tenere centinaia di oggetti interattivi caricati contemporaneamente nel mondo. Scrivi uno script Verse che esegua lo spawn delle mesh solo quando un giocatore è vicino e il loro despawn quando si allontana, risparmiando preziosi cicli CPU del server.
- Usa i Gameplay Tag per query in batch: evita di tracciare ogni actor dinamico in un singolo array globale di Verse. Assegna invece gameplay tag ai tuoi prop dello Scene Graph. Utilizza il sistema di tag query di UEFN per trovare e aggiornare i prop in modo dinamico in base ai limiti spaziali.
- Disaccoppia la collisione dagli actor dinamici: se un oggetto non deve muoversi, usa un box di collisione invisibile statico e pre-posizionato nel layout della mappa. Collega la mesh visiva e il componente di interazione Verse solo all'entity dinamica dello Scene Graph. Questo garantisce che i giocatori non colpiscano mai muri di collisione invisibili in caso di fallimento della replication.
- Sposta la gestione dello stato nel cloud: per qualsiasi gioco con meccaniche di costruzione persistenti, implementa un'integrazione backend come horizOn nelle prime fasi dello sviluppo. La memorizzazione dei dati delle coordinate nel cloud previene il superamento del budget di memoria locale e assicura che i progressi dei giocatori siano preservati tra le diverse istanze del server.
Pronto a creare giochi multiplayer scalabili e persistenti in UEFN senza i grattacapi del backend? Registrati gratuitamente su horizOn o consulta la documentazione delle API per iniziare.
Source: Scene graph entities are buggy when player leaves render distance from v41.00