Terug naar Blog

De Unreal Engine Multiplayer Sync Bug die je World States ruïneert (en hoe je het oplost)

Gepubliceerd op 23 februari 2026
De Unreal Engine Multiplayer Sync Bug die je World States ruïneert (en hoe je het oplost)

Je besteedt maanden aan het bouwen van een enorme, cinematische wereldtransformatie. In single-player werkt het vlekkeloos. Het oude spookhuis zakt weg in het terrein en de nieuwe, ongerepte versie rijst op precies het juiste moment uit de diepte op. Maar op het moment dat een tweede speler verbinding maakt met de server, verandert je meesterwerk in een onspeelbare, overlappende nachtmerrie. Huizen smelten samen. Collision breekt. Je spelers zitten vast in een geometrisch vagevuur.

Elke multiplayer indie dev loopt uiteindelijk tegen een muur aan waar client-side visuele logica hardhandig botst met de Server-Authoritative realiteit. Als je probeert honderden assets te verplaatsen met een Cinematic Sequence Device of timeline-animaties die worden getriggerd door lokale player events, smeek je praktisch om een Unreal Engine Multiplayer Sync Bug.

In deze tutorial gaan we precies analyseren waarom het verplaatsen van enorme hoeveelheden actors catastrofale desyncs veroorzaakt, waarom Cinematic Sequences falen voor late-joiners, en hoe je een kogelvrije, Server-Authoritative World State Manager ontwerpt met C++ en de Data Layers van Unreal Engine 5.

De anatomie van de Desync: Waarom je huizen samensmelten

Om het probleem op te lossen, moet je eerst de wiskunde begrijpen achter de reden waarom het Replication-systeem van Unreal Engine stikt in je transformatiesequentie.

Laten we aannemen dat je sequentie ongeveer 450 individuele assets verplaatst (muren, props, verlichting) om "Huis 1" te wisselen met "Huis 2". Wanneer je een replicated actor verplaatst, gebruikt Unreal Engine de FRepMovement struct om de locatie, rotatie en snelheid over het netwerk te synchroniseren.

Een standaard gecomprimeerde movement update kost ongeveer 40 tot 50 bytes per actor.

Als 450 actors tegelijkertijd bewegen tijdens een cinematische sequentie van 5 seconden, met een bescheiden update-frequentie van 30 keer per seconde, ziet de berekening er als volgt uit: 450 actors × 50 bytes × 30 updates/sec = 675.000 bytes per seconde (675 KB/s).

De standaard MaxClientRate van Unreal Engine (de maximale bandbreedte die de server naar een enkele client mag sturen) is meestal gecapt tussen 15.000 en 100.000 bytes per seconde.

Je sequentie vraagt bijna 7 keer de beschikbare bandbreedte. Het netwerkkanaal raakt direct verzadigd. De server begint updates agressief te throttelen, pakketten te droppen en prioriteit te geven aan andere actors op basis van NetPriority. Als gevolg hiervan stopt de helft van je Huis 1-assets halverwege onder de grond, en bereikt de helft van je Huis 2-assets nooit de oppervlakte. Je blijft achter met een permanent samengesmolten, gedesynchroniseerde puinhoop.

Bovendien: als je deze sequentie lokaal triggert via een client-side event (zoals een speler die in een trigger box stapt), zal een speler die 10 minuten later de server joint de sequentie nooit uitvoeren. Zij zien de standaard map state, terwijl de eerste speler de getransformeerde staat ziet.

Stap 1: Vervang Transform-manipulatie door Data Layers

Het verplaatsen van 450 actors is een brute-force aanpak die CPU-cycli en netwerkbandbreedte verspilt. In Unreal Engine 5 is de juiste architecturale aanpak voor enorme wereldveranderingen Data Layers (de evolutie van Level Streaming).

In plaats van "Huis 1" onder de grond te verplaatsen, wijs je alle Huis 1-assets toe aan een House1_DataLayer en alle Huis 2-assets aan een House2_DataLayer. Wanneer de timeline verschuift, unload je gewoon de eerste laag en load je de tweede.

Dit elimineert de bandbreedte-bottleneck volledig. In plaats van 675 KB/s aan continue movement data te streamen, stuurt de server een enkele, piepkleine state update: "Data Layer 2 is nu actief." De lokale engine van de client regelt het laden naadloos vanaf de schijf.

Stap 2: Het ontwerpen van de Server-Authoritative State Manager

Om ervoor te zorgen dat elke speler — inclusief degenen die laat joinen — precies dezelfde World State ziet, hebben we een centrale source of truth nodig. We maken een WorldStateManager actor in C++ die een RepNotify variabele gebruikt om de huidige era van het huis bij te houden.

De Header File (WorldStateManager.h)

We hebben een Enum nodig om onze states te definiëren, en een Replicated variabele met een ReplicatedUsing conditie.

#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);
};

De Implementation File (WorldStateManager.cpp)

Hier gebeurt de magie. Let op hoe we DOREPLIFETIME gebruiken om de variabele te registreren, and hoe de OnRep functie garandeert dat de visuele staat overeenkomt met de logische staat.

#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);
    }
}

Stap 3: Het Late-Joiner probleem oplossen

De grootste fout die developers maken bij het oplossen van een Unreal Engine Multiplayer Sync Bug is het gebruik van Multicast RPCs (Remote Procedure Calls) om wereldgebeurtenissen te triggeren.

Als je een Multicast RPC gebruikt zoals Multicast_PlayHouseTransformation(), wordt deze alleen uitgevoerd op clients die op dat exacte milliseconde verbonden zijn met de server. Als een speler crasht en 30 seconden later opnieuw verbindt, hebben ze de RPC gemist. Ze laden de map en zien Huis 1, terwijl de rest Huis 2 ziet.

Door een UPROPERTY(ReplicatedUsing = OnRep_CurrentEra) te gebruiken, lossen we het late-joiner probleem automatisch op. Wanneer een nieuwe speler verbindt, stuurt de server hen de huidige waarde van CurrentEra. Omdat de waarde die ze ontvangen (Future_House2) verschilt van hun standaard geïnitialiseerde waarde (Past_House1), vuurt Unreal Engine automatisch OnRep_CurrentEra() af voor die specifieke client op het moment dat ze inladen. Ze laden direct de juiste Data Layer. Geen custom join-logica nodig.

Als je kleinere session-based prototypes bouwt, bekijk dan onze gids over How To Architect A Local Co Op Shooter Prototype In Unreal Engine Step By Step.

World States behouden buiten de gamesessie

De bovenstaande C++ oplossing is perfect voor een enkele, draaiende serverinstantie. Maar wat gebeurt er als je server crasht? Of wat als je een persistente survival horror game bouwt waarbij de "Era" wekenlang bewaard moet blijven, zelfs als alle spelers uitloggen en de server afsluit?

Dit is waar het uitsluitend vertrouwen op de in-memory replication van Unreal Engine tekortschiet. Om globale World States te behouden, heb je een backend database nodig.

Dit zelf bouwen vereist het opzetten van PostgreSQL databases, het schrijven van REST APIs voor state-serialisatie, server-authenticatie en het configureren van auto-scaling infrastructuur — makkelijk 4-6 weken aan saai backend-werk.

Met horizOn zijn deze backend services al geconfigureerd. Je kunt je World State wijzigingen direct naar een managed Game State database pushen via onze SDK. Wanneer je dedicated server opstart, vraagt deze simpelweg de horizOn backend op, haalt {"CurrentEra": "Future_House2"} op, initialiseert de WorldStateManager, en je spelers gaan naadloos verder waar ze gebleven waren. Jij kunt je focussen op het ontwerpen van je horror game in plaats van het schrijven van database-migraties.

Als je game directe, bi-directionele communicatie met een backend vereist (bijvoorbeeld voor live-ops events), lees dan ook onze breakdown over Ditch Http Polling An Unreal Engine Websockets Tutorial For Real Time Backends.

5 Best Practices voor Multiplayer State Synchronization

Om ervoor te zorgen dat je nooit meer te maken krijgt met een catastrofale Unreal Engine Multiplayer Sync Bug, neem je deze regels op in je architectuur:

  1. Gebruik nooit Sequences voor Logical State: Cinematic Sequence Devices en Timelines mogen strikt alleen worden gebruikt voor visuele flair (VFX, camera shakes, lokale UI). Vertrouw nooit op het voltooien van een timeline om een variabele in te stellen die de gameplay beïnvloedt.
  2. RPCs voor Events, RepNotifies voor State: Gebruik Multicast RPCs voor kortstondige, tijdelijke events (een granaat die ontploft, een geluid dat afspeelt). Gebruik Replicated variabelen met RepNotifies voor persistente, blijvende states (een deur is open, een huis is getransformeerd, een generator staat aan).
  3. Respecteer de bandbreedtelimiet: Houd je netwerkprofiler in de gaten (Stat Net). Als je transforms repliceert voor meer dan 50-100 actors tegelijkertijd, verzadig je waarschijnlijk het kanaal. Gebruik Network Dormancy (ENetDormancy::DORM_Initial) voor props die zelden bewegen.
  4. Stel bAlwaysRelevant zorgvuldig in: Voor globale state managers (zoals onze AWorldStateManager), zorg dat bAlwaysRelevant = true. Als deze actor buiten de network cull distance van een speler valt, ontvangen ze geen updates meer, wat leidt tot lokale desyncs.
  5. Server Authority is absoluut: Clients mogen alleen "Requests" naar de server sturen (bijv. Server_RequestInteract()). De server valideert het verzoek, werkt de Replicated variabele bij en laat het replication-systeem de visuele wijzigingen naar alle clients propageren.

Stop met vechten tegen de Engine

Multiplayer game development is berucht moeilijk, maar 90% van de sync bugs komt voort uit het proberen te dwingen van client-side tools om server-side taken uit te voeren. Door over te stappen van brute-force transform-manipulatie naar Data Layers, en RepNotifies te gebruiken in plaats van lokale triggers, breng je je game in lijn met de bedoelde netwerkarchitectuur van Unreal Engine.

Klaar om je multiplayer backend te schalen en je World States te behouden zonder de infrastructuur-hoofdpijn? Probeer horizOn gratis of bekijk de API docs.


Bron: Houses Merged Weirdly HELPPPP