Volver al Blog

El Multiplayer Sync Bug de Unreal Engine que arruina tus World States (y cómo solucionarlo)

Publicado el 23 de febrero de 2026
El Multiplayer Sync Bug de Unreal Engine que arruina tus World States (y cómo solucionarlo)

Pasas meses construyendo una transformación de mundo masiva y cinematográfica. En single-player, se ejecuta sin problemas. La vieja casa encantada se hunde bajo el terreno y la nueva versión impecable surge de las profundidades perfectamente a tiempo. Pero en el momento en que un segundo jugador se conecta al servidor, tu obra maestra se convierte en una pesadilla de solapamientos injugable. Las casas se fusionan. El Collision se rompe. Tus jugadores están atrapados en el purgatorio de la geometría.

Todo desarrollador indie de multiplayer acaba chocando contra un muro donde la lógica visual del lado del cliente colisiona violentamente con la realidad de Server-Authoritative. Si estás intentando mover cientos de assets usando un Cinematic Sequence Device o animaciones de timeline activadas por eventos locales del jugador, prácticamente estás pidiendo a gritos un Unreal Engine Multiplayer Sync Bug.

En este tutorial, vamos a diseccionar exactamente por qué mover cantidades masivas de actors causa desyncs catastróficos, por qué las Cinematic Sequences fallan con los late-joiners, y cómo arquitecturar un World State Manager Server-Authoritative a prueba de balas usando C++ y las Data Layers de Unreal Engine 5.

La anatomía del Desync: Por qué tus casas se fusionan

Para solucionar el problema, primero debes entender la matemática detrás de por qué el sistema de Replication de Unreal Engine se está asfixiando con tu secuencia de transformación.

Supongamos que tu secuencia mueve aproximadamente 450 assets individuales (paredes, props, iluminación) para intercambiar la "Casa 1" por la "Casa 2". Cuando mueves un actor replicado, Unreal Engine utiliza la estructura FRepMovement para sincronizar su ubicación, rotación y velocidad a través de la red.

Una actualización de movimiento comprimida estándar cuesta aproximadamente entre 40 y 50 bytes por actor.

Si 450 actors se mueven simultáneamente durante una secuencia cinematográfica de 5 segundos, actualizándose a unas modestas 30 veces por segundo, el cálculo es el siguiente: 450 actors × 50 bytes × 30 actualizaciones/seg = 675,000 bytes por segundo (675 KB/s).

El MaxClientRate por defecto de Unreal Engine (el ancho de banda máximo que el servidor puede enviar a un solo cliente) suele estar limitado entre 15,000 y 100,000 bytes por segundo.

Tu secuencia está exigiendo casi 7 veces el ancho de banda disponible. El canal de red se satura instantáneamente. El servidor comienza a limitar agresivamente las actualizaciones, descartando paquetes y priorizando otros actors basados en la NetPriority. Como resultado, la mitad de los assets de tu Casa 1 dejan de moverse a mitad de camino bajo tierra, y la mitad de los assets de tu Casa 2 nunca llegan a la superficie. Te quedas con un desastre desincronizado y fusionado permanentemente.

Además, si activas esta secuencia localmente a través de un evento del lado del cliente (como un jugador entrando en un trigger box), un jugador que se una al servidor 10 minutos más tarde nunca ejecutará la secuencia. Verá el estado del mapa por defecto, mientras que el primer jugador ve el estado transformado.

Paso 1: Olvida la manipulación de Transform y usa Data Layers

Mover 450 actors es un enfoque de fuerza bruta que desperdicia ciclos de CPU y ancho de banda de red. En Unreal Engine 5, el enfoque arquitectónico correcto para cambios masivos en el mundo son las Data Layers (la evolución del Level Streaming).

En lugar de mover la "Casa 1" bajo tierra, asignas todos los assets de la Casa 1 a una House1_DataLayer y todos los de la Casa 2 a una House2_DataLayer. Cuando el timeline cambia, simplemente descargas la primera capa y cargas la segunda.

Esto elimina por completo el cuello de botella del ancho de banda. En lugar de transmitir 675 KB/s de datos de movimiento continuo, el servidor envía una única y diminuta actualización de estado: "Data Layer 2 está ahora activa". El motor local del cliente gestiona la carga sin problemas desde el disco.

Paso 2: Arquitecturando el World State Manager Server-Authoritative

Para asegurar que cada jugador —incluyendo los que se unen tarde— vea exactamente el mismo World State, necesitamos una fuente única de verdad. Crearemos un actor WorldStateManager en C++ que use una variable RepNotify para rastrear la era actual de la casa.

El archivo de cabecera (WorldStateManager.h)

Necesitamos un Enum para definir nuestros estados, y una variable Replicated con una condición ReplicatedUsing.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Info.h"
#include "WorldDataLayers/WorldDataLayers.h"
#include "WorldStateManager.generated."

UENUM(BlueprintType)
enum class EWorldEraState : uint8
{
    Past_House1 UMETA(DisplayName = "Past (House 1)"),
    Future_House2 UMETA(DisplayName = "Future (House 2)")
};

UCLASS()
class MYGAME_API AWorldStateManager : public AInfo
{
    GENERATED_BODY()

public:
    AWorldStateManager();

    // The server-side function to trigger the transformation
    UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category = "World State")
    void AdvanceWorldEra();

protected:
    virtual void BeginPlay() override;
    virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

    // The replicated variable tracking our current state
    UPROPERTY(ReplicatedUsing = OnRep_CurrentEra, Transient)
    EWorldEraState CurrentEra;

    // The RepNotify function that fires on clients when CurrentEra changes
    UFUNCTION()
    void OnRep_CurrentEra();

    // Helper to toggle Data Layers
    void UpdateDataLayers(EWorldEraState NewState);
};

El archivo de implementación (WorldStateManager.cpp)

Aquí es donde ocurre la magia. Observa cómo usamos DOREPLIFETIME para registrar la variable, y cómo la función OnRep garantiza que el estado visual coincida con el estado lógico.

#include "WorldStateManager.h"
#include "Net/UnrealNetwork.h"
#include "Engine/World.h"
#include "WorldPartition/DataLayer/DataLayerSubsystem.h"

AWorldStateManager::AWorldStateManager()
{
    bReplicates = true;
    bAlwaysRelevant = true; // Ensure all players always receive updates for this actor
    CurrentEra = EWorldEraState::Past_House1;
}

void AWorldStateManager::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    // Replicate to all clients
    DOREPLIFETIME(AWorldStateManager, CurrentEra);
}

void AWorldStateManager::BeginPlay()
{
    Super::BeginPlay();
    
    // Ensure the initial state is set correctly on the server
    if (HasAuthority())
    { 
        UpdateDataLayers(CurrentEra);
    }
}

void AWorldStateManager::AdvanceWorldEra()
{
    // Only the server can change the era
    if (!HasAuthority()) return;

    CurrentEra = EWorldEraState::Future_House2;
    
    // The server updates its own local Data Layers immediately
    UpdateDataLayers(CurrentEra);
}

// This fires automatically on clients when the server changes CurrentEra
void AWorldStateManager::OnRep_CurrentEra()
{
    UpdateDataLayers(CurrentEra);
}

void AWorldStateManager::UpdateDataLayers(EWorldEraState NewState)
{
    UWorld* World = GetWorld();
    if (!World) return;

    UDataLayerSubsystem* DataLayerSubsystem = World->GetSubsystem<UDataLayerSubsystem>();
    if (!DataLayerSubsystem) return;

    // Pseudocode for Data Layer toggling - replace with your specific Data Layer Asset references
    if (NewState == EWorldEraState::Past_House1)
    {
        // Load House 1, Unload House 2
        // DataLayerSubsystem->SetDataLayerInstanceRuntimeState(House1Layer, EDataLayerRuntimeState::Activated);
        // DataLayerSubsystem->SetDataLayerInstanceRuntimeState(House2Layer, EDataLayerRuntimeState::Unloaded);
    }
    else if (NewState == EWorldEraState::Future_House2)
    {
        // Load House 2, Unload House 1
        // DataLayerSubsystem->SetDataLayerInstanceRuntimeState(House2Layer, EDataLayerRuntimeState::Activated);
        // DataLayerSubsystem->SetDataLayerInstanceRuntimeState(House1Layer, EDataLayerRuntimeState::Unloaded);
    }
}

Paso 3: Resolviendo el problema del Late-Joiner

El mayor error que cometen los desarrolladores al intentar arreglar un Unreal Engine Multiplayer Sync Bug es usar Multicast RPCs (Remote Procedure Calls) para activar eventos del mundo.

Si usas un Multicast RPC para decir Multicast_PlayHouseTransformation(), solo se ejecutará en los clientes que estén conectados actualmente al servidor en ese milisegundo exacto. Si un jugador tiene un crash y se reconecta 30 segundos después, se habrá perdido el RPC. Cargará el mapa y verá la Casa 1, mientras que todos los demás ven la Casa 2.

Al usar una UPROPERTY(ReplicatedUsing = OnRep_CurrentEra), resolvemos el problema del late-joiner automáticamente. Cuando un nuevo jugador se conecta, el servidor le envía el valor actual de CurrentEra. Como el valor que recibe (Future_House2) difiere de su valor inicial por defecto (Past_House1), Unreal Engine activa automáticamente OnRep_CurrentEra() para ese cliente específico en el momento en que carga. Cargan instantáneamente la Data Layer correcta. Sin necesidad de lógica de unión personalizada.

Si estás construyendo prototipos más pequeños basados en sesiones, echa un vistazo a nuestra guía sobre How To Architect A Local Co Op Shooter Prototype In Unreal Engine Step By Step.

Persistencia de World States más allá de la sesión de juego

La solución en C++ anterior es perfecta para una única instancia de servidor en ejecución. Pero, ¿qué pasa si tu servidor se cae? ¿O qué pasa si estás construyendo un juego de survival horror persistente donde la "Era" necesita guardarse durante semanas de juego, incluso cuando todos los jugadores se desconectan y el servidor se apaga?

Aquí es donde confiar únicamente en la Replication en memoria de Unreal Engine se queda corto. Para persistir World States globales, necesitas una base de datos backend.

Construir esto tú mismo requiere configurar bases de datos PostgreSQL, escribir REST APIs para manejar la serialización del estado, gestionar la autenticación del servidor y configurar una infraestructura de auto-scaling; fácilmente 4-6 semanas de tedioso trabajo de backend.

Con horizOn, estos servicios de backend vienen preconfigurados. Puedes enviar tus cambios de World State directamente a una base de datos de Game State gestionada a través de nuestro SDK. Cuando tu servidor dedicado se inicia, simplemente consulta el backend de horizOn, recupera {"CurrentEra": "Future_House2"}, inicializa el WorldStateManager, y tus jugadores continúan sin problemas exactamente donde lo dejaron. Tú te centras en diseñar tu juego de terror en lugar de escribir migraciones de bases de datos.

Si tu juego requiere comunicación bidireccional instantánea con un backend (por ejemplo, para eventos de live-ops), también deberías leer nuestro desglose sobre cómo Ditch Http Polling An Unreal Engine Websockets Tutorial For Real Time Backends.

5 Buenas Prácticas para Multiplayer State Synchronization

Para asegurarte de no volver a enfrentarte a un catastrófico Unreal Engine Multiplayer Sync Bug, integra estas reglas en tu arquitectura:

  1. Nunca uses Sequences para el Logical State: Los Cinematic Sequence Devices y Timelines deben usarse estrictamente para el acabado visual (VFX, camera shakes, UI local). Nunca dependas de que un timeline termine para establecer una variable que afecte al gameplay.
  2. RPCs para eventos, RepNotifies para el estado: Usa Multicast RPCs para eventos transitorios y temporales (una granada explotando, un sonido). Usa variables Replicated con RepNotifies para estados persistentes y duraderos (una puerta abierta, una casa transformada, un generador encendido).
  3. Respeta el límite de ancho de banda: Monitoriza tu network profiler (Stat Net). Si estás replicando transforms para más de 50-100 actors simultáneamente, es probable que estés saturando el canal. Usa Network Dormancy (ENetDormancy::DORM_Initial) para props que rara vez se mueven.
  4. Configura bAlwaysRelevant con cuidado: Para gestores de estado global (como nuestro AWorldStateManager), asegúrate de que bAlwaysRelevant = true. Si este actor queda fuera de la network cull distance de un jugador, dejará de recibir actualizaciones, provocando desyncs localizados.
  5. Server Authority es absoluta: Los clientes solo deben enviar "Requests" al servidor (ej. Server_RequestInteract()). El servidor valida la solicitud, actualiza la variable Replicated y deja que el sistema de Replication propague los cambios visuales a todos los clientes.

Deja de luchar contra el motor

El desarrollo de juegos multiplayer es notoriamente difícil, pero el 90% de los sync bugs provienen de intentar forzar herramientas del lado del cliente a hacer trabajos del lado del servidor. Al cambiar la manipulación de transform por Data Layers y utilizar RepNotifies en lugar de triggers locales, alineas tu juego con la arquitectura de red prevista de Unreal Engine.

¿Listo para escalar tu backend multiplayer y persistir tus World States sin dolores de cabeza de infraestructura? Prueba horizOn gratis o consulta la API docs.


Fuente: Houses Merged Weirdly HELPPPP