O Multiplayer Sync Bug do Unreal Engine que arruína seus World States (e como corrigi-lo)
Você passa meses construindo uma transformação de mundo massiva e cinematográfica. No single-player, ela funciona perfeitamente. A velha casa mal-assombrada afunda no terreno, e a nova versão impecável surge das profundezas perfeitamente no momento certo. Mas no momento em que um segundo jogador se conecta ao servidor, sua obra-prima se transforma em um pesadelo de sobreposições injogável. Casas se fundem. O Collision quebra. Seus jogadores ficam presos no purgatório da geometria.
Todo desenvolvedor indie de multiplayer acaba batendo em um muro onde a lógica visual do lado do cliente colide violentamente com a realidade Server-Authoritative. Se você está tentando mover centenas de assets usando um Cinematic Sequence Device ou animações de timeline disparadas por eventos locais do jogador, você está praticamente implorando por um Unreal Engine Multiplayer Sync Bug.
Neste tutorial, vamos analisar exatamente por que mover quantidades massivas de actors causa desyncs catastróficos, por que Cinematic Sequences falham com late-joiners e como arquitetar um World State Manager Server-Authoritative à prova de balas usando C++ e as Data Layers do Unreal Engine 5.
A Anatomia do Desync: Por que suas casas estão se fundindo
Para corrigir o problema, primeiro você precisa entender a matemática por trás do motivo pelo qual o sistema de Replication do Unreal Engine está engasgando em sua sequência de transformação.
Vamos assumir que sua sequência move cerca de 450 assets individuais (paredes, props, iluminação) para trocar a "Casa 1" pela "Casa 2". Quando você move um actor replicado, o Unreal Engine usa a struct FRepMovement para sincronizar sua localização, rotação e velocidade pela rede.
Uma atualização de movimento comprimida padrão custa cerca de 40 a 50 bytes por actor.
Se 450 actors estiverem se movendo simultaneamente durante uma sequência cinematográfica de 5 segundos, atualizando a modestas 30 vezes por segundo, a conta é esta: 450 actors × 50 bytes × 30 atualizações/seg = 675.000 bytes por segundo (675 KB/s).
O MaxClientRate padrão do Unreal Engine (a largura de banda máxima que o servidor tem permissão para enviar a um único cliente) é normalmente limitado entre 15.000 e 100.000 bytes por segundo.
Sua sequência está exigindo quase 7 vezes a largura de banda disponível. O canal de rede satura instantaneamente. O servidor começa a limitar agressivamente as atualizações, descartando pacotes e priorizando outros actors com base na NetPriority. Como resultado, metade dos assets da sua Casa 1 para de se mover no meio do caminho sob a terra, e metade dos assets da sua Casa 2 nunca chega à superfície. Você fica com uma bagunça fundida e desincronizada permanentemente.
Além disso, se você disparar essa sequência localmente via um evento do lado do cliente (como um jogador entrando em uma trigger box), um jogador que entrar no servidor 10 minutos depois nunca executará a sequência. Ele verá o estado padrão do mapa, enquanto o primeiro jogador vê o estado transformado.
Passo 1: Abandone a manipulação de Transform pelas Data Layers
Mover 450 actors é uma abordagem de força bruta que desperdiça ciclos de CPU e largura de banda de rede. No Unreal Engine 5, a abordagem arquitetônica correta para mudanças massivas no mundo são as Data Layers (a evolução do Level Streaming).
Em vez de mover a "Casa 1" para baixo da terra, você atribui todos os assets da Casa 1 a uma House1_DataLayer e todos os assets da Casa 2 a uma House2_DataLayer. Quando o timeline muda, você simplesmente descarrega a primeira camada e carrega a segunda.
Isso elimina completamente o gargalo de largura de banda. Em vez de transmitir 675 KB/s de dados de movimento contínuo, o servidor envia uma única e minúscula atualização de estado: "Data Layer 2 está ativa agora." O engine local do cliente lida com o carregamento perfeitamente a partir do disco.
Passo 2: Arquitetando o World State Manager Server-Authoritative
Para garantir que cada jogador — incluindo aqueles que entram tarde — veja exatamente o mesmo World State, precisamos de uma fonte central de verdade. Criaremos um actor WorldStateManager em C++ que usa uma variável RepNotify para rastrear a era atual da casa.
O Arquivo de Cabeçalho (WorldStateManager.h)
Precisamos de um Enum para definir nossos estados e uma variável Replicated com uma condição 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);
};
O Arquivo de Implementação (WorldStateManager.cpp)
Aqui é onde a mágica acontece. Note como usamos DOREPLIFETIME para registrar a variável, e como a função OnRep garante que o estado visual corresponda ao 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);
}
}
Passo 3: Resolvendo o problema do Late-Joiner
O maior erro que os desenvolvedores cometem ao tentar corrigir um Unreal Engine Multiplayer Sync Bug é usar Multicast RPCs (Remote Procedure Calls) para disparar eventos do mundo.
Se você usar um Multicast RPC para dizer Multicast_PlayHouseTransformation(), ele só será executado nos clientes que estão atualmente conectados ao servidor naquele milissegundo exato. Se um jogador cair e reconectar 30 segundos depois, ele perdeu o RPC. Ele carregará o mapa e verá a Casa 1, enquanto todos os outros veem a Casa 2.
Ao usar uma UPROPERTY(ReplicatedUsing = OnRep_CurrentEra), resolvemos o problema do late-joiner automaticamente. Quando um novo jogador se conecta, o servidor envia a ele o valor atual de CurrentEra. Como o valor que ele recebe (Future_House2) difere do seu valor inicial padrão (Past_House1), o Unreal Engine dispara automaticamente OnRep_CurrentEra() para aquele cliente específico no momento em que ele carrega. Eles carregam instantaneamente a Data Layer correta. Nenhuma lógica de entrada personalizada é necessária.
Se você está construindo prototipos menores baseados em sessões, confira nosso guia sobre How To Architect A Local Co Op Shooter Prototype In Unreal Engine Step By Step.
Persistindo World States além da sessão de jogo
A solução em C++ acima é perfeita para uma única instância de servidor em execução. Mas o que acontece se o seu servidor cair? Ou se você estiver construindo um jogo de survival horror persistente onde a "Era" precisa permanecer salva por semanas de gameplay, mesmo quando todos os jogadores saem e o servidor desliga?
É aqui que confiar apenas na Replication em memória do Unreal Engine falha. Para persistir World States globais, você precisa de um banco de dados backend.
Construir isso sozinho requer configurar bancos de dados PostgreSQL, escrever REST APIs para lidar com a serialização de estado, gerenciar autenticação de servidor e configurar infraestrutura de auto-scaling — facilmente 4-6 semanas de trabalho tedioso de backend.
Com horizOn, esses serviços de backend vêm pré-configurados. Você pode enviar suas alterações de World State diretamente para um banco de dados de Game State gerenciado via nosso SDK. Quando seu servidor dedicado inicia, ele simplesmente consulta o backend do horizOn, recupera {"CurrentEra": "Future_House2"}, inicializa o WorldStateManager, e seus jogadores continuam perfeitamente de onde pararam. Você foca em projetar seu jogo de terror em vez de escrever migrações de banco de dados.
Se o seu jogo requer comunicação bidirecional instantânea com um backend (por exemplo, para disparar eventos de live-ops), você também deve ler nossa análise sobre como Ditch Http Polling An Unreal Engine Websockets Tutorial For Real Time Backends.
5 Boas Práticas para Multiplayer State Synchronization
Para garantir que você nunca mais enfrente um catastrófico Unreal Engine Multiplayer Sync Bug, incorpore estas regras em sua arquitetura:
- Nunca use Sequences para Logical State: Cinematic Sequence Devices e Timelines devem ser usados estritamente para visual (VFX, camera shakes, UI local). Nunca dependa do término de uma timeline para definir uma variável que impacta o gameplay.
- RPCs para eventos, RepNotifies para estado: Use Multicast RPCs para eventos transitórios e temporários (uma granada explodindo, um som tocando). Use variáveis Replicated com RepNotifies para estados persistentes e duradouros (uma porta aberta, uma casa transformada, um gerador ligado).
- Respeite o limite de largura de banda: Monitore seu network profiler (
Stat Net). Se você estiver replicando transforms para mais de 50-100 actors simultaneamente, provavelmente está saturando o canal. Use Network Dormancy (ENetDormancy::DORM_Initial) para props que raramente se movem. - Configure
bAlwaysRelevantcriteriosamente: Para gestores de estado global (como nossoAWorldStateManager), garanta quebAlwaysRelevant = true. Se este actor cair fora da network cull distance de um jogador, ele parará de receber atualizações, levando a desyncs localizados. - Server Authority é absoluta: Clientes só devem enviar "Requests" para o servidor (ex:
Server_RequestInteract()). O servidor valida a requisição, atualiza a variável Replicated e deixa o sistema de Replication propagar as mudanças visuais para todos os clientes.
Pare de lutar contra o Engine
O desenvolvimento de jogos multiplayer é notoriamente difícil, mas 90% dos bugs de sincronização vêm de tentar forçar ferramentas do lado do cliente a fazer trabalhos do lado do servidor. Ao mudar da manipulação de transform por força bruta para Data Layers, e utilizar RepNotifies em vez de triggers locais, você alinha seu jogo com a arquitetura de rede pretendida do Unreal Engine.
Pronto para escalar seu backend multiplayer e persistir seus World States sem dores de cabeça com infraestrutura? Experimente o horizOn gratuitamente ou confira a API docs.