World States Yapınızı Bozan Unreal Engine Multiplayer Sync Bug (Ve Nasıl Düzeltilir)
Aylarınızı devasa, sinematik bir dünya dönüşümü inşa etmek için harcıyorsunuz. Single-player modunda kusursuz çalışıyor. Eski hayaletli ev yerin altına giriyor ve yeni, tertemiz versiyon tam zamanında derinliklerden yükseliyor. Ancak ikinci bir oyuncu sunucuya bağlandığı an, şaheseriniz oynanamaz, iç içe geçmiş bir kabusa dönüşüyor. Evler birleşiyor. Collision bozuluyor. Oyuncularınız geometri arafında mahsur kalıyor.
Her multiplayer indie geliştiricisi, eninde sonunda client-side görsel mantığın server-authoritative gerçeklikle şiddetle çarpıştığı bir duvara toslar. Eğer bir Cinematic Sequence Device veya yerel oyuncu olayları tarafından tetiklenen timeline animasyonlarını kullanarak yüzlerce asseti hareket ettirmeye çalışıyorsanız, resmen bir Unreal Engine Multiplayer Sync Bug için davetiye çıkarıyorsunuz demektir.
Bu eğitimde, devasa miktarda actor hareket ettirmenin neden feci desync sorunlarına yol açtığını, Cinematic Sequences yapısının late-joiners için neden başarısız olduğunu ve C++ ile Unreal Engine 5'in Data Layers özelliğini kullanarak nasıl kurşun geçirmez, server-authoritative bir dünya durumu yöneticisi (world state manager) mimarisi oluşturacağınızı inceleyeceğiz.
Desync Anatomisi: Evleriniz Neden Birleşiyor?
Sorunu çözmek için öncelikle Unreal Engine'in replication sisteminin dönüşüm sekansınız sırasında neden tıkandığının arkasındaki matematiği anlamanız gerekir.
Sekansınızın "Ev 1" ile "Ev 2"yi değiştirmek için yaklaşık 450 bireysel asseti (duvarlar, proplar, aydınlatma) hareket ettirdiğini varsayalım. Replicated bir actorü hareket ettirdiğinizde, Unreal Engine ağ üzerinden konumu, rotasyonu ve hızı senkronize etmek için FRepMovement struct yapısını kullanır.
Standart bir sıkıştırılmış hareket güncellemesi actor başına yaklaşık 40 ila 50 byte maliyetindedir.
Eğer 450 actor, 5 saniyelik bir sinematik sekans sırasında aynı anda hareket ediyorsa ve saniyede mütevazı bir 30 kez güncelleniyorsa, matematik şöyledir: 450 actor × 50 byte × 30 güncelleme/sn = saniyede 675.000 byte (675 KB/s).
Unreal Engine'in varsayılan MaxClientRate değeri (sunucunun tek bir istemciye göndermesine izin verilen maksimum bant genişliği) genellikle saniyede 15.000 ile 100.000 byte arasında sınırlandırılmıştır.
Sekansınız, mevcut bant genişliğinin yaklaşık 7 katını talep ediyor. Ağ kanalı anında doyuma ulaşır. Sunucu güncellemeleri agresif bir şekilde kısmaya (throttling), paketleri düşürmeye ve NetPriority temelinde diğer actorlere öncelik vermeye başlar. Sonuç olarak, Ev 1 assetlerinizin yarısı yerin altında hareket etmeyi durdurur ve Ev 2 assetlerinizin yarısı asla yüzeye çıkamaz. Elinizde kalıcı olarak birleşmiş, senkronizasyonu bozulmuş bir karmaşa kalır.
Dahası, bu sekansı bir client-side olay (bir oyuncunun trigger box içine girmesi gibi) aracılığıyla yerel olarak tetiklerseniz, sunucuya 10 dakika sonra katılan bir oyuncu sekansı asla çalıştırmayacaktır. İlk oyuncu dönüşmüş durumu görürken, onlar varsayılan harita durumunu göreceklerdir.
Adım 1: Transform Manipülasyonunu Bırakın, Data Layers Kullanın
450 actorü hareket ettirmek, CPU döngülerini ve ağ bant genişliğini boşa harcayan bir kaba kuvvet (brute-force) yaklaşımıdır. Unreal Engine 5'te, devasa dünya değişiklikleri için doğru mimari yaklaşım Data Layers (Level Streaming'in evrimi) özelliğidir.
"Ev 1"i yerin altına taşımak yerine, tüm Ev 1 assetlerini bir House1_DataLayer'a ve tüm Ev 2 assetlerini bir House2_DataLayer'a atarsınız. Zaman çizelgesi değiştiğinde, sadece ilk katmanı unload eder ve ikincisini load edersiniz.
Bu, bant genişliği darboğazını tamamen ortadan kaldırır. Sunucu, 675 KB/s sürekli hareket verisi akışı sağlamak yerine tek bir küçük durum güncellemesi gönderir: "Data Layer 2 artık Aktif." İstemcinin yerel motoru, yükleme işlemini diskten sorunsuz bir şekilde gerçekleştirir.
Adım 2: Server-Authoritative State Manager Mimarisini Kurmak
Geç katılanlar da dahil olmak üzere her oyuncunun tam olarak aynı World State'i görmesini sağlamak için merkezi bir doğruluk kaynağına (source of truth) ihtiyacımız var. C++'da, evin mevcut dönemini takip etmek için bir RepNotify değişkeni kullanan bir WorldStateManager actorü oluşturacağız.
Header Dosyası (WorldStateManager.h)
Durumlarımızı tanımlamak için bir Enum'a ve ReplicatedUsing koşuluna sahip bir Replicated değişkene ihtiyacımız var.
#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);
};
Implementation Dosyası (WorldStateManager.cpp)
İşte sihrin gerçekleştiği yer burası. Değişkeni kaydetmek için DOREPLIFETIMEı nasıl kullandığımıza ve OnRep fonksiyonunun görsel durumun mantıksal durumla eşleşmesini nasıl garanti ettiğine dikkat edin.
#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);
}
}
Adım 3: Late-Joiner Sorununu Çözmek
Geliştiricilerin bir Unreal Engine Multiplayer Sync Bug sorununu çözmeye çalışırken yaptıkları en büyük hata, dünya olaylarını tetiklemek için Multicast RPCs (Remote Procedure Calls) kullanmaktır.
Eğer Multicast_PlayHouseTransformation() demek için bir Multicast RPC kullanırsanız, bu yalnızca o milisaniyede sunucuya bağlı olan istemcilerde çalışacaktır. Bir oyuncunun oyunu çökerse ve 30 saniye sonra yeniden bağlanırsa, RPC'yi kaçırmış olur. Haritaya yüklendiklerinde Ev 1'i görecekler, diğer herkes ise Ev 2'yi görecektir.
Bir UPROPERTY(ReplicatedUsing = OnRep_CurrentEra) kullanarak, late-joiner sorununu otomatik olarak çözüyoruz. Yeni bir oyuncu bağlandığında, sunucu onlara CurrentEra'nın mevcut değerini gönderir. Aldıkları değer (Future_House2), varsayılan başlangıç değerlerinden (Past_House1) farklı olduğu için Unreal Engine, yüklendikleri an o istemci için OnRep_CurrentEra() fonksiyonunu otomatik olarak çalıştırır. Anında doğru Data Layer yüklenir. Özel bir katılma mantığı gerekmez.
Eğer daha küçük oturum tabanlı prototipler oluşturuyorsanız, How To Architect A Local Co Op Shooter Prototype In Unreal Engine Step By Step kılavuzumuza göz atın.
Oyun Oturumu Ötesinde World States Kalıcılığı
Yukarıdaki C++ çözümü, çalışan tek bir sunucu örneği için mükemmeldir. Peki ya sunucunuz çökerse? Ya da tüm oyuncular oturumu kapattığında ve sunucu kapandığında bile "Era"nın haftalarca kaydedilmesi gereken kalıcı bir survival horror oyunu inşa ediyorsanız?
İşte burada yalnızca Unreal Engine'in bellek içi replication sistemine güvenmek yetersiz kalır. Küresel World States yapılarını kalıcı hale getirmek için bir backend veritabanına ihtiyacınız vardır.
Bunu kendiniz inşa etmek; PostgreSQL veritabanları kurmayı, durum serileştirmesini (state serialization) yönetmek için REST APIs yazmayı, sunucu kimlik doğrulamasını yönetmeyi ve auto-scaling altyapısını yapılandırmayı gerektirir — bu da en az 4-6 haftalık sıkıcı bir backend işidir.
horizOn ile bu backend hizmetleri önceden yapılandırılmış olarak gelir. World State değişikliklerinizi SDK'mız aracılığıyla doğrudan yönetilen bir Game State veritabanına gönderebilirsiniz. Dedicated server'ınız açıldığında, sadece horizOn backend'ine sorgu gönderir, {"CurrentEra": "Future_House2"} bilgisini alır, WorldStateManager'ı başlatır ve oyuncularınız tam olarak kaldıkları yerden sorunsuz bir şekilde devam eder. Veritabanı migrasyonları yazmak yerine korku oyununuzu tasarlamaya odaklanabilirsiniz.
Oyununuz bir backend ile anlık, çift yönlü iletişim gerektiriyorsa (örneğin, bir yama gerektirmeden dünya durumunu küresel olarak değiştiren live-ops etkinliklerini tetiklemek için), Ditch Http Polling An Unreal Engine Websockets Tutorial For Real Time Backends hakkındaki incelememizi de okumalısınız.
Multiplayer State Synchronization İçin 5 En İyi Uygulama
Bir daha asla feci bir Unreal Engine Multiplayer Sync Bug ile karşılaşmamak için bu kuralları mimarinize dahil edin:
- Mantıksal Durum İçin Asla Sequence Kullanmayın: Cinematic Sequence Devices ve Timelines kesinlikle görsel şölen (VFX, kamera sarsıntıları, yerel UI) için kullanılmalıdır. Oynanışı etkileyen bir değişkeni ayarlamak için asla bir timeline'ın bitmesine güvenmeyin.
- Olaylar İçin RPC, Durum İçin RepNotify: Geçici, anlık olaylar (bir el bombasının patlaması, bir sesin çalması) için Multicast RPC kullanın. Kalıcı, süregelen durumlar (bir kapının açık olması, bir evin dönüşmesi, bir jeneratörün çalışması) için RepNotify ile Replicated değişkenler kullanın.
- Bant Genişliği Sınırına Saygı Duyun: Ağ profilleyicinizi (
Stat Net) izleyin. Aynı anda 50-100'den fazla actor için transform replikasyonu yapıyorsanız, muhtemelen kanalı doyuruyorsunuzdur. Nadiren hareket eden proplar için Network Dormancy (ENetDormancy::DORM_Initial) kullanın. bAlwaysRelevantAyarını Titizlikle Yapın: Küresel durum yöneticileri için (örneğinAWorldStateManager),bAlwaysRelevant = trueolduğundan emin olun. Eğer bu actor bir oyuncunun network cull distance mesafesinin dışına düşerse, güncellemeleri almayı durdurur ve bu da yerel desync sorunlarına yol açar.- Server Authority Mutlaktır: İstemciler sunucuya yalnızca "İstekler" göndermelidir (örneğin
Server_RequestInteract()). Sunucu isteği doğrular, Replicated değişkeni günceller ve replication sisteminin görsel değişiklikleri tüm istemcilere yaymasına izin verir.
Motorla Savaşmayı Bırakın
Multiplayer oyun geliştirme zordur, ancak senkronizasyon hatalarının %90'ı client-side araçları server-side işleri yapmaya zorlamaktan kaynaklanır. Kaba kuvvet transform manipülasyonundan Data Layers'a geçerek ve yerel tetikleyiciler yerine RepNotifies kullanarak, oyununuzu Unreal Engine'in amaçlanan ağ mimarisiyle uyumlu hale getirirsiniz.
Multiplayer backend'inizi ölçeklendirmeye ve altyapı sorunları olmadan World States yapılarınızı kalıcı hale getirmeye hazır mısınız? horizOn'u ücretsiz deneyin veya kalıcı bulut durumlarını Unreal projenize ne kadar kolay entegre edebileceğinizi görmek için API docs sayfamıza göz atın.
Kaynak: Houses Merged Weirdly HELPPPP