Solucionando el bug de desaparición de entidades del Scene Graph en UEFN: Una guía de Verse para estados del mundo persistentes
En resumen
Esta guía técnica aborda el bug de replicación en UEFN que provoca la desaparición visual de las entidades de Scene Graph al teletransportarse, manteniendo las colisiones físicas. Se explica detalladamente el comportamiento del motor Chaos Physics y las limitaciones de la memoria de runtime en Verse. Finalmente, se presenta una solución temporal mediante scripts en Verse y se propone el uso de [horizOn](https://horizon.pm) como backend para gestionar estados de mundo persistentes en la nube.
Si tus jugadores se teletransportan a través de un mapa de UEFN, es muy probable que al regresar descubran que sus muebles cuidadosamente colocados son invisibles, no interactivos y, sin embargo, los bloquean físicamente como paredes fantasma. Este frustrante bug de replicación ha plagado a los desarrolladores que crean juegos sandbox, tycoon o centrados en la construcción en UEFN desde la actualización v41.00. Creas un sistema que permite a los jugadores colocar de forma personalizada muebles, cabinas de arcade o meshes decorativas usando entidades de Scene Graph, y todo funciona a la perfección en condiciones de juego local. Sin embargo, en el momento en que un jugador se teletransporta a un minijuego, un lobby o una sección separada de la isla y luego regresa, las meshes desaparecen.
Lo que hace que este problema sea particularmente desconcertante es el comportamiento de los datos de colisión. El jugador puede caminar hasta donde estaba el objeto, chocar contra una pared invisible y pararse sobre lo que debería ser una silla o un mueble sólido. Pero no pueden verlo, y sus componentes de interacción controlados por Verse se niegan a ejecutarse. El modelo visual ha desaparecido, pero la representación física sigue baked en la cuadrícula de World Partition.
Para entender por qué sucede esto, debemos mirar bajo el capó cómo gestiona UEFN la memoria y la replicación. Los actores estándar de Unreal Engine utilizan el network replication graph heredado para determinar qué debe renderizarse en la pantalla de un jugador según la distancia de la cámara y la relevancia. Con la introducción del nuevo sistema Scene Graph de UEFN, Epic Games intentó simplificar el diseño de juegos basado en componentes. Sin embargo, esto introdujo una discrepancia entre cómo el servidor almacena en caché las estructuras de entidades dinámicas y cómo el cliente hace streaming de assets visuales hacia y desde la memoria de la GPU.
Bajo el capó: La mecánica de culling de Scene Graph en UEFN
Cuando un jugador se encuentra dentro de la distancia de renderizado estándar de una entidad —típicamente alrededor de 15,000 a 20,000 Unreal Units (150-200 metros)—, el replication graph del servidor transmite activamente las actualizaciones de coordenadas, mesh y estado del componente al cliente. El cliente procesa estos paquetes de red y renderiza los componentes de la mesh visual en consecuencia.
En el momento en que el jugador se teletransporta, ocurren varias cosas en rápida sucesión:
- Relevancy Cutoff: El viewport del cliente se desplaza instantáneamente a miles de unidades de distancia. El replication graph marca las entidades de Scene Graph originales como "fuera de relevancia".
- GPU Garbage Collection: Para mantener framerates altos en dispositivos de gama baja como móviles y consolas, el cliente descarga de inmediato las representaciones visuales de estas entidades fuera de relevancia, purgando los componentes de static mesh de la memoria de la GPU.
- Collision Caching: A diferencia de las meshes visuales, la geometría de colisión es gestionada por el motor Chaos Physics. Las estructuras físicas se agrupan en bloques espaciales generales (cuadrículas de colisión HLOD) o se almacenan en caché localmente en el cliente para evitar que el jugador caiga a través del suelo durante los picos de red. Es por eso que la colisión permanece intacta incluso cuando se le hace culling a la mesh visual.
Cuando el jugador se teletransporta de regreso a las coordenadas originales, el cliente espera una secuencia de handshake para recrear los componentes visuales. Sin embargo, debido a un bug de replicación en la capa de red del Scene Graph, el servidor asume que el cliente aún conserva el estado visual almacenado en caché de las entidades. Dado que el servidor cree que el cliente tiene las entidades, no envía las RPCs de spawn de replicación. El cliente se queda con un bloque de colisión física pero sin mesh visual y sin conexión activa con los componentes de interacción.
Si otro jugador permanece en el área mientras el primero se teletransporta, el servidor mantiene activo el canal de replicación para esas entidades. Cuando el primer jugador regresa, aún puede ver los objetos porque el replication stream activo para el segundo jugador obliga al servidor a seguir transmitiendo actualizaciones a todos los clientes conectados. Sin embargo, una vez que ambos jugadores abandonan el área y regresan, el estado se rompe para todos.
Deep-Dive técnico: Por qué falla el Replication Graph al reingresar
El código de red de Unreal Engine se basa en un estricto sistema de relevancia de red. En entornos multiplayer, el servidor no puede replicar cada actor a cada jugador; de lo contrario, se saturaría el ancho de banda del cliente y se caería el hilo del servidor. En su lugar, el servidor utiliza un UReplicationGraph para agrupar los actores en celdas de cuadrícula espacial.
Para actores estáticos colocados previamente en un mapa, UEFN utiliza World Partition para hacer streaming de las meshes en el cliente. Pero las entidades dinámicas spawneadas a través de Verse o colocadas por los jugadores durante el runtime existen en un estado transitorio. Estas entidades transitorias no se benefician de las cuadrículas de streaming estáticas precompiladas. En su lugar, dependen de comprobaciones dinámicas de net relevancy.
Cuando un jugador se teletransporta, su conexión de red experimenta un cambio de estado importante. El servidor debe cerrar (tear down) los canales de replicación de la ubicación anterior y abrir (spin up) los canales para la nueva ubicación. Este cambio rápido puede provocar conflictos de priorización de paquetes. El servidor prioriza los elementos de gameplay de movimiento rápido (como la ubicación del jugador, la velocidad de movimiento y el disparo de armas) sobre los componentes visuales estáticos.
Esta congestión de red a menudo provoca la pérdida de paquetes de estado, de manera similar a los problemas de sincronización que detallamos en nuestra guía sobre cómo solucionar el desync de la ubicación del jugador en el multiplayer de UEFN y Unreal Engine. En el caso del bug del scene graph, el replication graph del servidor no logra reevaluar la relevancia de las entidades que sufrieron culling para el jugador que regresa. Debido a que el servidor no registra que el cliente carece de la entidad, omite el envío de las actualizaciones de propiedades necesarias. La entidad del lado del cliente existe en un estado "zombi": viva en el hilo de física, pero muerta en los hilos de renderizado e interacción.
Resolviendo el bug: Un workaround en Verse para refrescar la replicación
Para solucionar el bug de uefn scene graph entities disappearing, debemos obligar al servidor a marcar estas entidades como "dirty" cuando un jugador se teletransporta de regreso a su alcance. Al forzar una actualización de propiedad, activamos el replication graph para enviar un paquete de estado nuevo al cliente que regresa, lo que reconstruye la mesh y los componentes interactivos.
La forma más sólida de lograr esto es crear un administrador de entidades dinámicas en Verse. Este administrador rastrea las posiciones de los jugadores, detecta eventos de teletransportación (cambios repentinos de coordenadas) y ejecuta un toggle de visibilidad o transform localizado en las entidades cercanas para forzar un refresco de red.
A continuación se muestra un script de Verse completo y sintácticamente correcto que implementa este patrón de reconstrucción de estado.
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()
Cómo funciona el código
El replication_refresher_device funciona ejecutando un bucle asíncrono que realiza un polling de las ubicaciones de los jugadores. Al comparar el vector de coordenadas actuales del jugador con su vector de coordenadas de hace 0.5 segundos, el script puede distinguir entre la locomoción normal y la teletransportación a alta velocidad.
Cuando se activa un evento de teletransportación, la función HandlePlayerTeleport evalúa todos los props dinámicos registrados. Para cualquier prop dentro de un radio de 200 metros de la nueva posición del jugador, invoca ForceEntityReplicationRefresh. Hacer un toggle de Hide() y Show() en el creative_prop obliga al servidor a actualizar los flags de net-dirty en el actor de C++ subyacente. Esto fuerza al replication graph a enviar el payload completo del actor al dispositivo del cliente, reconstruyendo con éxito las static meshes que sufrieron culling y los canales interactivos.
Arquitectura de estados del mundo persistentes: Los límites de la memoria de Verse
Si bien el workaround de Verse resuelve el problema de renderizado para mapas pequeños y medianos, expone una limitación más profunda de la arquitectura de runtime de UEFN. Si tu juego depende de cientos de objetos colocados por los jugadores, rastrearlos a todos en arrays de Verse puede agotar rápidamente el presupuesto de memoria de tu servidor.
Cada instancia de clase dinámica, coordenada de vector y estructura de rastreo de variables en Verse consume memoria de runtime. Los servidores de Fortnite se ejecutan bajo estrictas restricciones de recursos. Si excedes el recuento máximo de instrucciones permitido o los límites de asignación de memoria, tu servidor experimentará una degradación severa del rendimiento o sufrirá un crash por completo, lo que provocará network driver timeouts que devolverán a los jugadores al lobby.
Además, las variables de Verse no persisten cuando una instancia del servidor hiberna o se apaga. Si un servidor queda inactivo porque todos los jugadores se han ido (un proceso analizado en nuestro deep-dive sobre el exploit de rendimiento de servidor en UEFN), todo el diseño (layout) de las cabinas de arcade y muebles colocados por los jugadores se pierde de forma permanente.
Para construir un juego verdaderamente persistente, necesitas almacenar estos datos de layout espacial en un backend externo. Configurar esto por tu cuenta es una tarea de ingeniería masiva:
- Aprovisionamiento de infraestructura: Debes alojar una base de datos (por ejemplo, PostgreSQL o MongoDB) capaz de escalar a miles de lecturas y escrituras concurrentes.
- API Gateway: Necesitas construir un servidor HTTP seguro que traduzca las peticiones de Verse en consultas de base de datos, maneje la autenticación de jugadores y gestione el rate limiting.
- Resiliencia de red: Los clientes HTTP de Verse están muy limitados y carecen de políticas de reintento automático o connection pooling. Debes escribir código boilerplate complejo para manejar la pérdida de paquetes y los timeouts del servidor.
Para un equipo indie pequeño, pasar de 4 a 6 semanas escribiendo código de integración de base de datos en lugar de pulir el gameplay es un desperdicio enorme de recursos.
Persistencia sin interrupciones: Cómo horizOn resuelve la sincronización de estados
Aquí es donde entra horizOn. Como un Backend-as-a-Service dedicado y diseñado específicamente para desarrolladores de videojuegos, horizOn proporciona almacenamiento de estado espacial preconfigurado y de baja latencia que se integra directamente con UEFN y Verse.
En lugar de mantener cientos de entidades activas del scene graph cargadas en la burbuja de memoria del servidor en todo momento, puedes guardar sus coordenadas espaciales, rotaciones y atributos personalizados en la base de datos en la nube de horizOn. Cuando un jugador se teletransporta, puedes hacer despawn de las entidades locales de forma segura para liberar memoria del servidor de Fortnite. Cuando el jugador regresa, consultas la base de datos y reconstruyes únicamente las entidades dentro de su vecindad inmediata.
Al aprovechar horizOn, solucionas el bug de culling y el problema de memoria del servidor simultáneamente. El servidor solo replica lo que el jugador puede ver activamente, reduciendo el ancho de banda de red de ~120 KB/s a menos de ~25 KB/s por cliente. Si una instancia del servidor hiberna o se reinicia, el progreso del jugador se recupera instantáneamente desde la nube.
Integrar horizOn requiere solo unas pocas líneas de código de Verse usando peticiones HTTP estándar:
# 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 encargándose de la infraestructura, obtienes una persistencia de datos sólida y un rendimiento óptimo del servidor sin tener que administrar una sola línea de código de servidor backend.
Buenas prácticas para gestionar la replicación de Scene Graph en UEFN
Si actualmente estás creando un juego sandbox o tycoon en UEFN, aplica estos cuatro consejos prácticos para garantizar que los estados de tu mundo se mantengan sincronizados y tus servidores funcionen de manera eficiente:
- Implementar spawning y despawning dinámico: No mantengas cientos de objetos interactivos cargados en el mundo simultáneamente. Escribe un script en Verse que spawnee meshes solo cuando un jugador esté cerca y les haga despawn cuando se aleje, ahorrando valiosos ciclos de CPU del servidor.
- Usar Gameplay Tags para consultas por lotes (batch): Evita rastrear cada actor dinámico en un único array global de Verse. En su lugar, asigna gameplay tags a tus props de Scene Graph. Utiliza el sistema de consulta de tags de UEFN para encontrar y refrescar props dinámicamente según límites espaciales.
- Desacoplar la colisión de los actores dinámicos: Si un objeto no necesita moverse, usa una caja de colisión invisible estática y previamente colocada en el layout de tu mapa. Adjunta la mesh visual y el componente de interacción de Verse únicamente a la entidad dinámica del Scene Graph. Esto garantiza que los jugadores nunca choquen con paredes de colisión invisibles cuando falle la replicación.
- Delegar la gestión del estado en la nube: Para cualquier juego con mecánicas de construcción persistentes, implementa una integración de backend como horizOn en las primeras fases del desarrollo. Almacenar los datos de coordenadas en la nube evita desbordamientos del presupuesto de memoria local y garantiza que el progreso del jugador se conserve entre las instancias del servidor.
¿Listo para crear juegos multiplayer persistentes y escalables en UEFN sin los dolores de cabeza del backend? Regístrate gratis en horizOn o consulta la documentación de la API para comenzar.
Fuente: Scene graph entities are buggy when player leaves render distance from v41.00