Correction du bug de disparition des entités du Scene Graph UEFN : un guide Verse pour des états de monde persistants
En bref
Ce guide technique explique comment résoudre le bug de disparition des entités du Scene Graph UEFN lors de la téléportation des joueurs en forçant une mise à jour de la réplication réseau via Verse. Il analyse les limites de mémoire runtime associées au suivi d'entités dynamiques complexes directement sur les serveurs Fortnite. Enfin, il propose d'externaliser la persistance de l'état spatial sur un service cloud comme horizOn afin de préserver les performances serveur et d'assurer une persistance durable.
Si vos joueurs se téléportent à travers une carte UEFN, ils risquent à leur retour de trouver leurs meubles soigneusement placés invisibles, non interactifs, mais les bloquant physiquement comme des murs fantômes. Ce bug de réplication frustrant affecte les développeurs créant des jeux de type sandbox, tycoon ou axés sur la construction dans UEFN depuis la mise à jour v41.00. Vous concevez un système permettant aux joueurs de placer de manière personnalisée des meubles, des bornes d'arcade ou des meshes décoratifs en utilisant des entités Scene Graph, et tout fonctionne parfaitement en conditions de jeu local. Cependant, dès qu'un joueur se téléporte vers un mini-jeu, un lobby ou une section distincte de l'île, puis revient, les meshes disparaissent.
Ce qui rend ce problème particulièrement déroutant, c'est le comportement des données de collision. Le joueur peut s'approcher de l'endroit où se trouvait l'objet, heurter un mur invisible et se tenir debout sur ce qui devrait être une chaise ou un meuble solide. Mais il ne peut pas le voir, et ses composants d'interaction pilotés par Verse refusent de se déclencher. Le modèle visuel a disparu, pourtant sa représentation physique reste baked dans la grille de World Partition.
Pour comprendre pourquoi cela se produit, nous devons regarder sous le capot de la gestion de la mémoire et de la réplication par UEFN. Les actors Unreal Engine standards utilisent le network replication graph historique pour déterminer ce qui doit être rendu sur l'écran du joueur en fonction de la distance de caméra et de la relevancy. Avec l'introduction du nouveau système Scene Graph d'UEFN, Epic Games a tenté de simplifier la conception de jeux basée sur les composants. Cependant, cela a introduit un décalage entre la façon dont le serveur met en cache les structures d'entités dynamiques et la façon dont le client stream les assets visuels vers et depuis la mémoire GPU.
Sous le capot : le fonctionnement du culling du Scene Graph UEFN
Lorsqu'un joueur se trouve à une distance de rendu standard d'une entité — généralement entre 15 000 et 20 000 Unreal Units (150 à 200 mètres) — le replication graph du serveur diffuse activement les mises à jour de coordonnées, de mesh et d'état des composants au client. Le client traite ces paquets réseau et effectue le rendu des composants de mesh visuel en conséquence.
Le moment où le joueur se téléporte au loin, plusieurs choses se produisent en succession rapide :
- Relevancy Cutoff : Le viewport du client se déplace instantanément de plusieurs milliers d'unités. Le replication graph marque les entités du Scene Graph d'origine comme « out of relevancy ».
- GPU Garbage Collection : Pour maintenir des framerates élevés sur les appareils moins performants comme les mobiles et les consoles, le client décharge immédiatement les représentations visuelles de ces entités « out of relevancy », purgeant les composants de static mesh de la mémoire GPU.
- Collision Caching : Contrairement aux meshes visuels, la géométrie de collision est gérée par le moteur Chaos Physics. Les structures physiques sont regroupées dans des blocs spatiaux grossiers (grilles de collision HLOD) ou mises en cache localement sur le client pour éviter que le joueur ne passe à travers le sol lors des pics réseau. C'est pourquoi la collision reste intacte même lorsque le mesh visuel est culled.
Lorsque le joueur se téléporte à nouveau vers les coordonnées d'origine, le client s'attend à une séquence de handshake pour recréer les composants visuels. Cependant, en raison d'un bug de réplication dans la couche réseau (network layer) du Scene Graph, le serveur suppose que le client conserve toujours l'état visuel mis en cache des entités. Comme le serveur croit que le client possède les entités, il n'envoie pas les RPC de spawn de réplication. Le client se retrouve avec un bloc de collision physique mais aucun mesh visuel, et aucune connexion active vers les composants d'interaction.
Si un autre joueur reste dans la zone pendant que le premier joueur se téléporte au loin, le serveur maintient le canal de réplication (replication channel) actif pour ces entités. Lorsque le premier joueur revient, il peut toujours voir les objets car le flux de réplication (replication stream) actif pour le second joueur oblige le serveur à continuer de diffuser les mises à jour à tous les clients connectés. Cependant, une fois que les deux joueurs quittent la zone et reviennent, l'état se détériore pour tout le monde.
Deep-Dive technique : pourquoi le replication graph échoue lors du retour dans la zone
Le code réseau d'Unreal Engine repose sur un système de network relevancy très strict. Dans les environnements multiplayer, le serveur ne peut pas répliquer chaque actor à chaque joueur ; faire cela saturerait la bande passante du client et ferait crasher le thread du serveur. Au lieu de cela, le serveur utilise un UReplicationGraph pour regrouper (bin) les actors dans des cellules de grille spatiale.
Pour les actors statiques pré-placés dans une carte, UEFN utilise le World Partition pour streamer les meshes sur le client. Mais les entités dynamiques spawnées via Verse ou placées par les joueurs pendant le runtime existent dans un état transitoire (transient state). Ces entités transitoires ne bénéficient pas des grilles de streaming statiques précompilées. À la place, elles s'appuient sur des vérifications dynamiques de net relevancy.
Lorsqu'un joueur se téléporte, sa connexion réseau subit un changement d'état majeur. Le serveur doit tear down les replication channels de l'ancien emplacement et spin up des canaux pour le nouvel emplacement. Ce changement rapide peut entraîner des conflits de packet prioritization. Le serveur donne la priorité aux éléments de gameplay rapides (comme la position du joueur, sa vitesse de déplacement et les tirs d'armes) par rapport aux composants visuels statiques.
Cette congestion réseau entraîne souvent des dropped state packets, similaires aux problèmes de synchronisation que nous détaillons dans notre guide sur comment résoudre la désynchronisation de position des joueurs dans le multiplayer UEFN et Unreal Engine. Dans le cas du bug de Scene Graph, le replication graph du serveur ne parvient pas à réévaluer la relevancy des entités culled pour le joueur qui revient. Comme le serveur ne détecte pas que le client n'a plus l'entité, il évite d'envoyer les mises à jour de propriétés requises. L'entité côté client existe alors dans un état « zombie » : vivante dans le physics thread, mais morte dans les threads de rendering et d'interaction.
Résoudre le bug : un workaround de rafraîchissement de réplication basé sur Verse
Pour corriger le bug uefn scene graph entities disappearing, nous devons forcer le serveur à marquer ces entités comme « dirty » lorsqu'un joueur se téléporte à nouveau à portée. En forçant une mise à jour de propriété, nous déclenchons le replication graph pour envoyer un nouveau paquet d'état (state packet) au client qui revient, ce qui reconstruit le mesh et les composants interactifs.
La méthode la plus robuste pour y parvenir consiste à créer un gestionnaire d'entités dynamiques dans Verse. Ce gestionnaire suit les positions des joueurs, détecte les événements de téléportation (changements soudains de coordonnées) et exécute un basculement (toggle) localisé de visibilité ou de transform sur les entités proches pour forcer un rafraîchissement réseau.
Voici ci-dessous un script Verse complet et syntaxiquement correct qui implémente ce pattern de reconstruction d'état.
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()
Comment fonctionne le code
Le replication_refresher_device fonctionne en exécutant une boucle asynchrone qui interroge (poll) la position des joueurs. En comparant le vecteur de coordonnées actuel du joueur avec son vecteur de coordonnées d'il y a 0,5 seconde, le script peut faire la distinction entre une locomotion normale et une téléportation à grande vitesse.
Lorsqu'un événement de téléportation est déclenché, la fonction HandlePlayerTeleport évalue tous les props dynamiques enregistrés. Pour tout prop situé dans un rayon de 200 mètres autour de la nouvelle position du joueur, elle appelle ForceEntityReplicationRefresh. Le basculement entre Hide() et Show() sur le creative_prop oblige le serveur à mettre à jour les drapeaux (flags) net-dirty sur l'actor C++ sous-jacent. Cela force le replication graph à pousser l'intégralité du payload de l'actor vers l'appareil du client, reconstruisant ainsi avec succès les static meshes et les canaux interactifs (interactive channels) culled.
Concevoir des états de monde persistants : les limites de la mémoire Verse
Bien que le workaround Verse résolve le problème de rendu pour les cartes de petite à moyenne taille, il expose une limite plus profonde de l'architecture d'exécution (runtime architecture) d'UEFN. Si votre jeu repose sur des centaines d'objets placés par les joueurs, les suivre tous dans des tableaux Verse peut rapidement épuiser le budget mémoire de votre serveur.
Chaque instance de classe dynamique, coordonnée de vecteur et structure de suivi de variables dans Verse consomme de la mémoire runtime. Les serveurs Fortnite fonctionnent sous des contraintes de ressources strictes. Si vous dépassez le nombre maximal d'instructions ou les limites d'allocation de mémoire, votre serveur subira d'importantes baisses de performances ou crashera complètement, provoquant des network driver timeouts qui renverront les joueurs vers le lobby.
De plus, les variables Verse ne persistent pas lorsqu'une instance de serveur passe en veille (hibernate) ou s'éteint. Si un serveur devient inactif parce que tous les joueurs sont partis (un processus analysé dans notre deep-dive sur l'exploit de performance de serveur UEFN), toute la disposition des bornes d'arcade et des meubles placés par les joueurs est définitivement perdue.
Pour créer un jeu véritablement persistant, vous devez stocker ces données de disposition spatiale sur un backend externe. Mettre cela en place vous-même représente un travail d'ingénierie colossal :
- Provisioning d'infrastructure : Vous devez héberger une base de données (par exemple, PostgreSQL ou MongoDB) capable de monter en charge (scale) pour supporter des milliers de lectures et d'écritures simultanées.
- API Gateway : Vous devez créer un serveur HTTP sécurisé qui traduit les requêtes Verse en requêtes de base de données, gère l'authentification des joueurs et s'occupe du rate limiting.
- Résilience réseau : Les clients HTTP de Verse sont extrêmement restreints et ne disposent pas de politiques de retry automatiques ni de pooling de connexions. Vous devez écrire du code boilerplate complexe pour gérer les pertes de paquets et les timeouts de serveur.
Pour une petite équipe indépendante (indie), passer 4 à 6 semaines à écrire du code d'intégration de base de données au lieu de peaufiner le gameplay est un énorme gaspillage de ressources.
Persistance fluide : comment horizOn résout la synchronisation d'état
C'est là que horizOn intervient. En tant que Backend-as-a-Service dédié, conçu spécifiquement pour les développeurs de jeux, horizOn fournit un stockage d'état spatial préconfiguré à faible latence qui s'intègre directement à UEFN et Verse.
Au lieu de conserver à tout moment des centaines d'entités Scene Graph actives chargées dans la bulle de mémoire du serveur, vous pouvez enregistrer leurs coordonnées spatiales, leurs rotations et leurs attributs personnalisés dans la base de données cloud de horizOn. Lorsqu'un joueur se téléporte au loin, vous pouvez despawner en toute sécurité les entités locales pour libérer de la mémoire sur le serveur Fortnite. Lorsque le joueur revient, vous interrogez (query) la base de données et ne reconstruisez que les entités situées à proximité immédiate.
En tirant parti de horizOn, vous résolvez simultanément le bug de culling et le problème de mémoire du serveur. Le serveur ne réplique que ce que le joueur peut réellement voir, réduisant la bande passante réseau de ~120 Ko/s à moins de ~25 Ko/s par client. Si une instance de serveur passe en veille (hibernate) ou redémarre, la progression du joueur est instantanément récupérée depuis le cloud.
L'intégration de horizOn ne nécessite que quelques lignes de code Verse en utilisant des requêtes HTTP standards :
# 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}")
Avec horizOn qui gère l'infrastructure, vous bénéficiez d'une persistance des données robuste et de performances de serveur optimales sans avoir à gérer une seule ligne de code de serveur backend.
Bonnes pratiques pour gérer la réplication du Scene Graph UEFN
Si vous construisez actuellement un jeu sandbox ou tycoon dans UEFN, appliquez ces quatre conseils pratiques pour garantir la synchronisation de vos états du monde et l'efficacité de vos serveurs :
- Implémenter le spawn et despawn dynamique : Ne conservez pas des centaines d'objets interactifs chargés en même temps dans le monde. Écrivez un script Verse qui spawn des meshes uniquement lorsqu'un joueur est proche, et les despawn lorsqu'il s'éloigne, économisant ainsi de précieux cycles CPU du serveur.
- Utiliser les Gameplay Tags pour le requêtage par lot (batch querying) : Évitez de suivre chaque actor dynamique dans un seul tableau global Verse. Assignez plutôt des gameplay tags à vos props Scene Graph. Utilisez le système de tag query d'UEFN pour trouver et rafraîchir dynamiquement les props en fonction des limites spatiales.
- Découpler la collision des actors dynamiques : Si un objet n'a pas besoin de bouger, utilisez une boîte de collision invisible statique et pré-placée dans la disposition de votre carte. Attachez le mesh visuel et le composant d'interaction Verse uniquement à l'entité dynamique du Scene Graph. Cela garantit que les joueurs ne heurteront jamais de murs de collision invisibles en cas d'échec de la réplication.
- Déporter la gestion d'état vers le cloud : Pour tout jeu doté de mécaniques de construction persistante, implémentez une intégration backend comme horizOn tôt dans le développement. Stocker les données de coordonnées dans le cloud évite les dépassements de budget mémoire locaux et garantit que la progression des joueurs est préservée entre les instances de serveur.
Prêt à créer des jeux multiplayer persistants et scalables dans UEFN sans les tracas du backend ? Inscrivez-vous gratuitement sur horizOn ou consultez la documentation de l'API pour commencer.
Source : Scene graph entities are buggy when player leaves render distance from v41.00