Il Multiplayer Sync Bug di Unreal Engine che rovina i tuoi World States (e come risolverlo)
Passi mesi a costruire una trasformazione del mondo massiccia e cinematografica. In single-player, viene eseguita perfettamente. La vecchia casa infestata affonda nel terreno e la nuova versione incontaminata sorge dalle profondità perfettamente a tempo. Ma nel momento in cui un secondo giocatore si connette al server, il tuo capolavoro si trasforma in un incubo ingiocabile di sovrapposizioni. Le case si fondono. Il Collision si rompe. I tuoi giocatori sono bloccati nel purgatorio della geometria.
Ogni sviluppatore indie di multiplayer prima o poi sbatte contro un muro dove la logica visuale lato client collide violentemente con la realtà Server-Authoritative. Se stai cercando di muovere centinaia di asset usando un Cinematic Sequence Device o animazioni timeline attivate da eventi locali del giocatore, stai praticamente implorando per un Unreal Engine Multiplayer Sync Bug.
In questo tutorial, analizzeremo esattamente perché muovere quantità massicce di actor causa desync catastrofici, perché le Cinematic Sequences falliscono con i late-joiner e come progettare un World State Manager Server-Authoritative a prova di bomba usando C++ e le Data Layers di Unreal Engine 5.
L'anatomia del Desync: Perché le tue case si fondono
Per risolvere il problema, devi prima capire la matematica dietro il motivo per cui il sistema di Replication di Unreal Engine sta soffocando sulla tua sequenza di trasformazione.
Supponiamo che la tua sequenza muova circa 450 singoli asset (muri, props, luci) per scambiare "Casa 1" con "Casa 2". Quando muovi un actor replicato, Unreal Engine usa la struct FRepMovement per sincronizzare posizione, rotazione e velocità sulla rete.
Un aggiornamento di movimento compresso standard costa circa 40-50 byte per actor.
Se 450 actor si muovono simultaneamente durante una sequenza cinematografica di 5 secondi, con un aggiornamento di 30 volte al secondo, il calcolo è questo: 450 actor × 50 byte × 30 aggiornamenti/sec = 675.000 byte al secondo (675 KB/s).
Il MaxClientRate predefinito di Unreal Engine (la larghezza di banda massima che il server può inviare a un singolo client) è tipicamente limitato tra 15.000 e 100.000 byte al secondo.
La tua sequenza richiede quasi 7 volte la larghezza di banda disponibile. Il canale di rete si satura istantaneamente. Il server inizia a limitare aggressivamente gli aggiornamenti, scartando pacchetti e dando priorità ad altri actor in base alla NetPriority. Di conseguenza, metà degli asset della Casa 1 smette di muoversi a metà strada sottoterra e metà degli asset della Casa 2 non arriva mai in superficie. Ti ritrovi con un pasticcio fuso e desincronizzato in modo permanente.
Inoltre, se attivi questa sequenza localmente tramite un evento lato client (come un giocatore che entra in un trigger box), un giocatore che si unisce al server 10 minuti dopo non eseguirà mai la sequenza. Vedrà lo stato predefinito della mappa, mentre il primo giocatore vede lo stato trasformato.
Step 1: Abbandona la manipolazione dei Transform per le Data Layers
Muovere 450 actor è un approccio di forza bruta che spreca cicli di CPU e larghezza di banda di rete. In Unreal Engine 5, l'approccio architettonico corretto per cambiamenti massicci del mondo sono le Data Layers (l'evoluzione del Level Streaming).
Invece di muovere la "Casa 1" sottoterra, assegni tutti gli asset della Casa 1 a un House1_DataLayer e tutti gli asset della Casa 2 a un House2_DataLayer. Quando la timeline cambia, semplicemente scarichi il primo layer e carichi il secondo.
Questo elimina completamente il collo di bottiglia della larghezza di banda. Invece di trasmettere 675 KB/s di dati di movimento continuo, il server invia un singolo, minuscolo aggiornamento di stato: "Data Layer 2 è ora attivo." L'engine locale del client gestisce il caricamento senza problemi dal disco.
Step 2: Progettare il World State Manager Server-Authoritative
Per garantire che ogni giocatore — inclusi quelli che si uniscono in ritardo — veda esattamente lo stesso World State, abbiamo bisogno di una fonte centrale di verità. Creeremo un actor WorldStateManager in C++ che usa una variabile RepNotify per tracciare l'era attuale della casa.
Il file Header (WorldStateManager.h)
Abbiamo bisogno di un Enum per definire i nostri stati e di una variabile Replicated con una condizione 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);
};
Il file di implementazione (WorldStateManager.cpp)
Qui è dove avviene la magia. Nota come usiamo DOREPLIFETIME per registrare la variabile e come la funzione OnRep garantisce che lo stato visuale corrisponda allo stato logico.
#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);
}
}
Step 3: Risolvere il problema dei Late-Joiner
L'errore più grande che gli sviluppatori commettono cercando di risolvere un Unreal Engine Multiplayer Sync Bug è usare Multicast RPCs (Remote Procedure Calls) per attivare eventi del mondo.
Se usi un Multicast RPC per dire Multicast_PlayHouseTransformation(), verrà eseguito solo sui client che sono attualmente connessi al server in quel preciso millisecondo. Se un giocatore crasha e si riconnette 30 secondi dopo, ha perso l'RPC. Caricherà la mappa e vedrà la Casa 1, mentre tutti gli altri vedono la Casa 2.
Usando una UPROPERTY(ReplicatedUsing = OnRep_CurrentEra), risolviamo automaticamente il problema dei late-joiner. Quando un nuovo giocatore si connette, il server gli invia il valore attuale di CurrentEra. Poiché il valore che riceve (Future_House2) differisce dal suo valore iniziale predefinito (Past_House1), Unreal Engine esegue automaticamente OnRep_CurrentEra() per quel client specifico nel momento in cui carica. Caricano istantaneamente la Data Layer corretta. Nessuna logica di join personalizzata richiesta.
Se stai costruendo prototipi più piccoli basati su sessioni, dai un'occhiata alla nostra guida su How To Architect A Local Co Op Shooter Prototype In Unreal Engine Step By Step.
Persistenza dei World States oltre la sessione di gioco
La soluzione C++ sopra è perfetta per una singola istanza di server in esecuzione. Ma cosa succede se il tuo server crasha? O se stai costruendo un survival horror persistente dove l'"Era" deve rimanere salvata per settimane di gioco, anche quando tutti i giocatori si disconnettono e il server si spegne?
Qui è dove affidarsi esclusivamente alla Replication in-memory di Unreal Engine fallisce. Per persistere i World States globali, hai bisogno di un database backend.
Costruire questo da soli richiede la configurazione di database PostgreSQL, la scrittura di REST APIs per gestire la serializzazione dello stato, la gestione dell'autenticazione del server e la configurazione di un'infrastruttura di auto-scaling — facilmente 4-6 settimane di noioso lavoro di backend.
Con horizOn, questi servizi backend sono pre-configurati. Puoi inviare le modifiche al tuo World State direttamente a un database Game State gestito tramite il nostro SDK. Quando il tuo server dedicato si avvia, interroga semplicemente il backend di horizOn, recupera {"CurrentEra": "Future_House2"}, inizializza il WorldStateManager e i tuoi giocatori continuano esattamente da dove avevano interrotto. Puoi concentrarti sul design del tuo gioco horror invece di scrivere migrazioni di database.
Se il tuo gioco richiede una comunicazione bidirezionale istantanea con un backend (ad esempio, per attivare eventi live-ops), dovresti leggere anche la nostra analisi su come Ditch Http Polling An Unreal Engine Websockets Tutorial For Real Time Backends.
5 Best Practice per la Multiplayer State Synchronization
Per assicurarti di non dover mai più affrontare un catastrofico Unreal Engine Multiplayer Sync Bug, integra queste regole nella tua architettura:
- Mai usare Sequences per il Logical State: Cinematic Sequence Devices e Timeline dovrebbero essere usati rigorosamente per l'estetica visuale (VFX, camera shakes, UI locale). Mai fare affidamento sulla fine di una timeline per impostare una variabile che impatta il gameplay.
- RPC per gli eventi, RepNotifies per lo stato: Usa Multicast RPC per eventi transitori e temporanei (una granata che esplode, un suono). Usa variabili Replicated con RepNotifies per stati persistenti e duraturi (una porta aperta, una casa trasformata, un generatore alimentato).
- Rispetta il limite di banda: Monitora il tuo network profiler (
Stat Net). Se stai replicando transform per più di 50-100 actor simultaneamente, probabilmente stai saturando il canale. Usa la Network Dormancy (ENetDormancy::DORM_Initial) per props che si muovono raramente. - Imposta
bAlwaysRelevantcon cura: Per i gestori di stato globale (come il nostroAWorldStateManager), assicurati chebAlwaysRelevant = true. Se questo actor cade fuori dalla network cull distance di un giocatore, smetterà di ricevere aggiornamenti, portando a desync localizzati. - La Server Authority è assoluta: I client dovrebbero solo inviare "Request" al server (es.
Server_RequestInteract()). Il server valida la richiesta, aggiorna la variabile Replicated e lascia che il sistema di Replication propaghi i cambiamenti visuali a tutti i client.
Smetti di combattere contro l'Engine
Lo sviluppo di giochi multiplayer è notoriamente difficile, ma il 90% dei bug di sincronizzazione deriva dal tentativo di forzare strumenti lato client a fare lavori lato server. Passando dalla manipolazione dei transform a forza bruta alle Data Layers, e utilizzando i RepNotifies invece dei trigger locali, allinei il tuo gioco all'architettura di rete prevista da Unreal Engine.
Pronto a scalare il tuo backend multiplayer e a persistere i tuoi World States senza mal di testa per l'infrastruttura? Prova horizOn gratuitamente o consulta la API docs.