Powrót do Bloga

Błąd Multiplayer Sync w Unreal Engine, który niszczy Twoje World States (i jak go naprawić)

Opublikowano 23 lutego 2026
Błąd Multiplayer Sync w Unreal Engine, który niszczy Twoje World States (i jak go naprawić)

Spędzasz miesiące na budowaniu ogromnej, kinowej transformacji świata. W trybie single-player wszystko działa bez zarzutu. Stary nawiedzony dom zapada się pod ziemię, a nowa, nieskazitelna wersja wyłania się z głębin idealnie w odpowiednim momencie. Jednak w chwili, gdy drugi gracz łączy się z serwerem, Twoje arcydzieło zmienia się w niegrywalny koszmar nakładających się obiektów. Domy się łączą. Collision przestaje działać. Twoi gracze utykają w geometrycznym czyśćcu.

Każdy niezależny twórca gier multiplayer w końcu uderza w ścianę, gdzie wizualna logika po stronie klienta brutalnie zderza się z rzeczywistością Server-Authoritative. Jeśli próbujesz przesuwać setki assetów za pomocą Cinematic Sequence Device lub animacji timeline wyzwalanych przez lokalne zdarzenia gracza, praktycznie prosisz się o Unreal Engine Multiplayer Sync Bug.

W tym tutorialu przeanalizujemy dokładnie, dlaczego przesuwanie ogromnej liczby actorów powoduje katastrofalne desynki, dlaczego Cinematic Sequences zawodzą w przypadku late-joinerów i jak zaprojektować niezawodny, Server-Authoritative World State Manager przy użyciu C++ i Data Layers z Unreal Engine 5.

Anatomia Desyncu: Dlaczego Twoje domy się łączą

Aby naprawić problem, musisz najpierw zrozumieć matematykę stojącą za tym, dlaczego system Replication w Unreal Engine dławi się przy Twojej sekwencji transformacji.

Załóżmy, że Twoja sekwencja przesuwa około 450 pojedynczych assetów (ściany, propy, oświetlenie), aby zamienić „Dom 1” na „Dom 2”. Kiedy przesuwasz replikowanego actora, Unreal Engine używa struktury FRepMovement do synchronizacji jego lokalizacji, rotacji i prędkości w sieci.

Standardowa skompresowana aktualizacja ruchu kosztuje około 40 do 50 bajtów na actora.

Jeśli 450 actorów porusza się jednocześnie podczas 5-sekundowej sekwencji kinowej, aktualizując się skromne 30 razy na sekundę, obliczenia wyglądają następująco: 450 actorów × 50 bajtów × 30 aktualizacji/sek = 675 000 bajtów na sekundę (675 KB/s).

Domyślny MaxClientRate w Unreal Engine (maksymalna przepustowość, jaką serwer może wysłać do pojedynczego klienta) jest zazwyczaj ograniczony do przedziału od 15 000 do 100 000 bajtów na sekundę.

Twoja sekwencja wymaga prawie 7-krotności dostępnej przepustowości. Kanał sieciowy natychmiast się nasyca. Serwer zaczyna agresywnie ograniczać aktualizacje, odrzucać pakiety i priorytetyzować inne actory na podstawie NetPriority. W rezultacie połowa assetów Domu 1 przestaje się poruszać w połowie drogi pod ziemię, a połowa assetów Domu 2 nigdy nie dociera na powierzchnię. Zostajesz z trwale połączonym, zdesynchronizowanym bałaganem.

Co więcej, jeśli wyzwolisz tę sekwencję lokalnie poprzez zdarzenie po stronie klienta (np. gracz wchodzący w trigger box), gracz, który dołączy do serwera 10 minut później, nigdy nie wykona tej sekwencji. Zobaczy domyślny stan mapy, podczas gdy pierwszy gracz widzi stan przetransformowany.

Krok 1: Porzuć manipulację Transform na rzecz Data Layers

Przesuwanie 450 actorów to podejście siłowe, które marnuje cykle CPU i przepustowość sieci. W Unreal Engine 5 właściwym podejściem architektonicznym dla masowych zmian w świecie są Data Layers (ewolucja Level Streaming).

Zamiast przesuwać „Dom 1” pod ziemię, przypisujesz wszystkie assety Domu 1 do House1_DataLayer, a wszystkie assety Domu 2 do House2_DataLayer. Gdy następuje zmiana, po prostu odładowujesz pierwszą warstwę i ładujesz drugą.

To całkowicie eliminuje wąskie gardło przepustowości. Zamiast przesyłać strumieniowo 675 KB/s ciągłych danych o ruchu, serwer wysyła jedną, malutką aktualizację stanu: „Data Layer 2 jest teraz aktywna”. Lokalny silnik klienta zajmuje się ładowaniem płynnie z dysku.

Krok 2: Projektowanie Server-Authoritative State Managera

Aby upewnić się, że każdy gracz — w tym ci, którzy dołączą później — widzi dokładnie ten sam World State, potrzebujemy centralnego źródła prawdy. Utworzymy actora WorldStateManager w C++, który używa zmiennej RepNotify do śledzenia aktualnej ery domu.

Plik nagłówkowy (WorldStateManager.h)

Potrzebujemy Enuma do zdefiniowania naszych stanów oraz zmiennej Replicated z warunkiem 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);
};

Plik implementacji (WorldStateManager.cpp)

Tutaj dzieje się magia. Zauważ, jak używamy DOREPLIFETIME do rejestracji zmiennej oraz jak funkcja OnRep gwarantuje, że stan wizualny zgadza się ze stanem logicznym.

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

Krok 3: Rozwiązanie problemu Late-Joinera

Największym błędem, jaki popełniają deweloperzy próbujący naprawić Unreal Engine Multiplayer Sync Bug, jest używanie Multicast RPCs (Remote Procedure Calls) do wyzwalania zdarzeń w świecie.

Jeśli użyjesz Multicast RPC do wywołania Multicast_PlayHouseTransformation(), zostanie on wykonany tylko na klientach, którzy są aktualnie połączeni z serwerem w tej konkretnej milisekundzie. Jeśli graczowi wyłączy się gra i połączy się ponownie 30 sekund później, ominie go RPC. Załaduje mapę i zobaczy Dom 1, podczas gdy wszyscy inni widzą Dom 2.

Używając UPROPERTY(ReplicatedUsing = OnRep_CurrentEra), rozwiązujemy problem late-joinera automatycznie. Gdy nowy gracz się łączy, serwer wysyła mu aktualną wartość CurrentEra. Ponieważ otrzymana wartość (Future_House2) różni się od domyślnej wartości zainicjalizowanej (Past_House1), Unreal Engine automatycznie wywołuje OnRep_CurrentEra() dla tego konkretnego klienta w momencie ładowania. Natychmiast ładują oni poprawną Data Layer. Nie jest wymagana żadna niestandardowa logika dołączania.

Jeśli budujesz mniejsze prototypy oparte na sesjach, sprawdź nasz przewodnik How To Architect A Local Co Op Shooter Prototype In Unreal Engine Step By Step.

Persystencja World States poza sesję gry

Powyższe rozwiązanie w C++ jest idealne dla pojedynczej, działającej instancji serwera. Ale co się stanie, jeśli serwer ulegnie awarii? Albo jeśli budujesz trwały survival horror, w którym „Era” musi pozostać zapisana przez tygodnie rozgrywki, nawet gdy wszyscy gracze się wylogują, a serwer zostanie wyłączony?

W tym miejscu poleganie wyłącznie na replikacji w pamięci Unreal Engine zawodzi. Aby zachować globalne World States, potrzebujesz bazy danych backend.

Samodzielne zbudowanie tego wymaga skonfigurowania baz danych PostgreSQL, napisania REST APIs do obsługi serializacji stanu, zarządzania uwierzytelnianiem serwera i konfiguracji infrastruktury auto-scaling — to łatwo 4-6 tygodni żmudnej pracy nad backendem.

Z horizOn te usługi backendowe są wstępnie skonfigurowane. Możesz przesyłać zmiany World State bezpośrednio do zarządzanej bazy danych Game State za pomocą naszego SDK. Gdy Twój serwer dedykowany się uruchamia, po prostu odpytuje backend horizOn, pobiera {"CurrentEra": "Future_House2"}, inicjalizuje WorldStateManager, a Twoi gracze płynnie kontynuują dokładnie tam, gdzie przerwali. Możesz skupić się na projektowaniu gry, zamiast pisać migracje bazy danych.

Jeśli Twoja gra wymaga natychmiastowej, dwukierunkowej komunikacji z backendem (np. wyzwalanie zdarzeń live-ops), przeczytaj również naszą analizę Ditch Http Polling An Unreal Engine Websockets Tutorial For Real Time Backends.

5 dobrych praktyk synchronizacji stanu w Multiplayer

Aby upewnić się, że nigdy więcej nie spotkasz katastrofalnego Unreal Engine Multiplayer Sync Bug, wprowadź te zasady do swojej architektury:

  1. Nigdy nie używaj Sequences dla stanu logicznego: Cinematic Sequence Devices i Timelines powinny być używane wyłącznie do efektów wizualnych (VFX, camera shakes, lokalne UI). Nigdy nie polegaj na zakończeniu timeline'u w celu ustawienia zmiennej wpływającej na rozgrywkę.
  2. RPC dla zdarzeń, RepNotifies dla stanu: Używaj Multicast RPC dla przejściowych, tymczasowych zdarzeń (wybuch granatu, odtworzenie dźwięku). Używaj zmiennych Replicated z RepNotifies dla trwałych, utrzymujących się stanów (otwarte drzwi, przetransformowany dom, włączony generator).
  3. Szanuj limit przepustowości: Monitoruj swój network profiler (Stat Net). Jeśli replikujesz transformy dla więcej niż 50-100 actorów jednocześnie, prawdopodobnie nasycasz kanał. Używaj Network Dormancy (ENetDormancy::DORM_Initial) dla propów, które rzadko się poruszają.
  4. Ustawiaj bAlwaysRelevant rozważnie: Dla globalnych menedżerów stanu (jak nasz AWorldStateManager) upewnij się, że bAlwaysRelevant = true. Jeśli ten actor znajdzie się poza network cull distance gracza, przestanie on otrzymywać aktualizacje, co prowadzi do lokalnych desynków.
  5. Server Authority jest absolutna: Klienci powinni wysyłać do serwera wyłącznie „Requests” (np. Server_RequestInteract()). Serwer waliduje żądanie, aktualizuje zmienną Replicated i pozwala systemowi Replication rozpropagować zmiany wizualne do wszystkich klientów.

Przestań walczyć z silnikiem

Tworzenie gier multiplayer jest niezwykle trudne, ale 90% błędów synchronizacji wynika z próby zmuszenia narzędzi po stronie klienta do wykonywania zadań po stronie serwera. Przechodząc z siłowej manipulacji transformami na Data Layers i wykorzystując RepNotifies zamiast lokalnych triggerów, dostosowujesz swoją grę do zamierzonej architektury sieciowej Unreal Engine.

Gotowy na skalowanie swojego backendu multiplayer i persystencję World States bez problemów z infrastrukturą? Wypróbuj horizOn za darmo lub sprawdź API docs.


Źródło: Houses Merged Weirdly HELPPPP