Multiplayer Sync Bug di Unreal Engine yang Merusak World States Anda (Dan Cara Memperbaikinya)
Anda menghabiskan waktu berbulan-bulan membangun transformasi dunia sinematik yang masif. Di single-player, semuanya berjalan sempurna. Rumah berhantu tua tenggelam ke bawah tanah, dan versi baru yang bersih muncul dari kedalaman tepat pada waktunya. Namun saat pemain kedua terhubung ke server, mahakarya Anda berubah menjadi mimpi buruk objek yang tumpang tindih dan tidak bisa dimainkan. Rumah-rumah menyatu. Collision rusak. Pemain Anda terjebak dalam api penyucian geometri.
Setiap developer indie multiplayer akhirnya akan membentur tembok di mana logika visual client-side bertabrakan keras dengan realitas Server-Authoritative. Jika Anda mencoba memindahkan ratusan asset menggunakan Cinematic Sequence Device atau animasi timeline yang dipicu oleh event pemain lokal, Anda secara praktis mengundang Unreal Engine Multiplayer Sync Bug.
Dalam tutorial ini, kita akan membedah mengapa memindahkan Actor dalam jumlah besar menyebabkan desync yang fatal, mengapa Cinematic Sequences gagal untuk Late-joiners, dan cara merancang World State Manager yang Server-Authoritative dan tangguh menggunakan C++ dan Data Layers di Unreal Engine 5.
Anatomi Desync: Mengapa Rumah Anda Menyatu
Untuk memperbaiki masalah ini, pertama-tama Anda perlu memahami matematika di balik mengapa sistem Replication Unreal Engine tersedak pada sekuens transformasi Anda.
Mari kita asumsikan sekuens Anda memindahkan sekitar 450 asset individu (dinding, prop, pencahayaan) untuk menukar "Rumah 1" dengan "Rumah 2". Saat Anda memindahkan Replicated Actor, Unreal Engine menggunakan struct FRepMovement untuk menyinkronkan lokasi, rotasi, dan kecepatannya melalui jaringan.
Update gerakan terkompresi standar memakan biaya sekitar 40 hingga 50 byte per Actor.
Jika 450 Actor bergerak secara bersamaan selama sekuens sinematik 5 detik, dengan update 30 kali per detik, hitungannya seperti ini: 450 actors × 50 bytes × 30 updates/sec = 675,000 bytes per second (675 KB/s).
Default MaxClientRate di Unreal Engine (bandwidth maksimum yang diizinkan server untuk dikirim ke satu client) biasanya dibatasi antara 15.000 hingga 100.000 byte per detik.
Sekuens Anda menuntut hampir 7 kali lipat bandwidth yang tersedia. Saluran jaringan langsung jenuh. Server mulai membatasi update secara agresif, membuang paket, dan memprioritaskan Actor lain berdasarkan NetPriority. Hasilnya, separuh asset Rumah 1 Anda berhenti bergerak di tengah jalan di bawah tanah, dan separuh asset Rumah 2 Anda tidak pernah sampai ke permukaan. Anda ditinggalkan dengan kekacauan yang menyatu dan tidak sinkron secara permanen.
Terlebih lagi, jika Anda memicu sekuens ini secara lokal melalui client-side event (seperti pemain melangkah ke dalam trigger box), pemain yang bergabung ke server 10 menit kemudian tidak akan pernah menjalankan sekuens tersebut. Mereka akan melihat status map default, sementara pemain pertama melihat status yang telah bertransformasi.
Langkah 1: Tinggalkan Manipulasi Transform dan Gunakan Data Layers
Memindahkan 450 Actor adalah pendekatan brute-force yang membuang siklus CPU dan bandwidth jaringan. Di Unreal Engine 5, pendekatan arsitektur yang benar untuk perubahan dunia yang masif adalah Data Layers (evolusi dari Level Streaming).
Alih-alih memindahkan "Rumah 1" ke bawah tanah, Anda menetapkan semua asset Rumah 1 ke House1_DataLayer dan semua asset Rumah 2 ke House2_DataLayer. Saat timeline bergeser, Anda cukup melakukan unload pada layer pertama dan load pada layer kedua.
Ini sepenuhnya menghilangkan hambatan bandwidth. Alih-alih melakukan streaming data gerakan terus-menerus sebesar 675 KB/s, server mengirimkan satu update status kecil: "Data Layer 2 sekarang Aktif." Engine lokal client menangani pemuatan secara mulus dari disk.
Langkah 2: Merancang Server-Authoritative State Manager
Untuk memastikan setiap pemain—termasuk mereka yang bergabung terlambat—melihat World State yang sama persis, kita memerlukan sumber kebenaran pusat. Kita akan membuat Actor WorldStateManager di C++ yang menggunakan variabel RepNotify untuk melacak era rumah saat ini.
File Header (WorldStateManager.h)
Kita membutuhkan Enum untuk mendefinisikan status kita, dan variabel Replicated dengan kondisi 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);
};
File Implementasi (WorldStateManager.cpp)
Di sinilah keajaiban terjadi. Perhatikan bagaimana kita menggunakan DOREPLIFETIME untuk mendaftarkan variabel, dan bagaimana fungsi OnRep menjamin bahwa status visual cocok dengan status logis.
#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);
}
}
Langkah 3: Menyelesaikan Masalah Late-Joiner
Kesalahan terbesar yang dilakukan developer saat mencoba memperbaiki Unreal Engine Multiplayer Sync Bug adalah menggunakan Multicast RPCs (Remote Procedure Calls) untuk memicu world event.
Jika Anda menggunakan Multicast RPC untuk memanggil Multicast_PlayHouseTransformation(), itu hanya akan dieksekusi pada client yang sedang terhubung ke server pada milidetik tersebut. Jika seorang pemain mengalami crash dan terhubung kembali 30 detik kemudian, mereka melewatkan RPC tersebut. Mereka akan masuk ke map dan melihat Rumah 1, sementara orang lain melihat Rumah 2.
Dengan menggunakan UPROPERTY(ReplicatedUsing = OnRep_CurrentEra), kita menyelesaikan masalah late-joiner secara otomatis. Saat pemain baru terhubung, server mengirimi mereka nilai CurrentEra saat ini. Karena nilai yang mereka terima (Future_House2) berbeda dari nilai awal default mereka (Past_House1), Unreal Engine secara otomatis memicu OnRep_CurrentEra() untuk client tersebut saat mereka masuk. Mereka langsung memuat Data Layer yang benar. Tidak diperlukan logika join kustom.
Jika Anda sedang membangun prototipe berbasis sesi yang lebih kecil, lihat panduan kami tentang How To Architect A Local Co Op Shooter Prototype In Unreal Engine Step By Step.
Persistensi World States di Luar Sesi Game
Solusi C++ di atas sempurna untuk satu instance server yang sedang berjalan. Tapi apa yang terjadi jika server Anda crash? Atau bagaimana jika Anda membangun game survival horror yang persisten di mana "Era" perlu tetap tersimpan selama berminggu-minggu gameplay, bahkan ketika semua pemain log off dan server dimatikan?
Di sinilah mengandalkan Replication dalam memori Unreal Engine saja tidak cukup. Untuk mempertahankan World States global, Anda memerlukan database backend.
Membangun ini sendiri memerlukan pengaturan database PostgreSQL, menulis REST APIs untuk menangani serialisasi status, mengelola autentikasi server, dan mengonfigurasi infrastruktur auto-scaling—setidaknya 4-6 minggu pekerjaan backend yang membosankan.
Dengan horizOn, layanan backend ini sudah terkonfigurasi. Anda dapat mengirim perubahan World State Anda langsung ke database Game State yang dikelola melalui SDK kami. Saat dedicated server Anda menyala, ia cukup melakukan query ke backend horizOn, mengambil {"CurrentEra": "Future_House2"}, menginisialisasi WorldStateManager, dan pemain Anda melanjutkan tepat di tempat mereka tinggalkan. Anda bisa fokus mendesain game horror Anda daripada menulis migrasi database.
Jika game Anda memerlukan komunikasi dua arah yang instan dengan backend (misalnya, memicu event live-ops yang mengubah status dunia secara global tanpa memerlukan patch), Anda juga harus membaca ulasan kami tentang cara Ditch Http Polling An Unreal Engine Websockets Tutorial For Real Time Backends.
5 Best Practices untuk Multiplayer State Synchronization
Untuk memastikan Anda tidak pernah menghadapi Unreal Engine Multiplayer Sync Bug yang fatal lagi, terapkan aturan ini ke dalam arsitektur Anda:
- Jangan Pernah Gunakan Sequences untuk Logical State: Cinematic Sequence Devices dan Timelines harus digunakan secara ketat hanya untuk pemanis visual (VFX, camera shakes, UI lokal). Jangan pernah mengandalkan selesainya timeline untuk mengatur variabel yang berdampak pada gameplay.
- RPC untuk Event, RepNotifies untuk State: Gunakan Multicast RPC untuk event transien dan sementara (granat meledak, suara diputar). Gunakan variabel Replicated dengan RepNotifies untuk status yang persisten dan bertahan lama (pintu terbuka, rumah bertransformasi, generator menyala).
- Hargai Batas Bandwidth: Pantau network profiler Anda (
Stat Net). Jika Anda mereplikasi transform untuk lebih dari 50-100 Actor secara bersamaan, Anda kemungkinan besar menjenuhkan saluran. Gunakan Network Dormancy (ENetDormancy::DORM_Initial) untuk prop yang jarang bergerak. - Atur
bAlwaysRelevantdengan Cermat: Untuk manajer status global (sepertiAWorldStateManagerkita), pastikanbAlwaysRelevant = true. Jika Actor ini berada di luar network cull distance pemain, mereka akan berhenti menerima update, yang menyebabkan desync lokal. - Server Authority adalah Mutlak: Client hanya boleh mengirim "Requests" ke server (misalnya,
Server_RequestInteract()). Server memvalidasi permintaan, memperbarui variabel Replicated, dan membiarkan sistem Replication menyebarkan perubahan visual kembali ke semua client.
Berhenti Melawan Engine
Pengembangan game multiplayer memang sulit, tetapi 90% bug sinkronisasi berasal dari upaya memaksa alat client-side untuk melakukan pekerjaan server-side. Dengan beralih dari manipulasi transform brute-force ke Data Layers, dan memanfaatkan RepNotifies alih-alih trigger lokal, Anda menyelaraskan game Anda dengan arsitektur jaringan yang dimaksudkan oleh Unreal Engine.
Siap untuk menskalakan multiplayer backend Anda dan mempertahankan World States Anda tanpa pusing memikirkan infrastruktur? Coba horizOn secara gratis atau lihat API docs untuk melihat betapa mudahnya Anda dapat mengintegrasikan status cloud persisten ke dalam proyek Unreal Anda.
Sumber: Houses Merged Weirdly HELPPPP