Ghost Actor Reappearance: Destructible Grid Yapıları için Multiplayer Network Replication Desync Fix
Özet olarak
Bu makalede, yok edilebilir grid yapılarında ortaya çıkan "ghost build" senkronizasyon hatasının çözümü için client-side prediction buffer tabanlı bir multiplayer network replication desync fix yöntemi ele alınmaktadır. Unreal Engine gibi modern oyun motorlarında property replication ve kanal kapatma (channel teardown) işlemlerinin farklı yollar izlemesinden kaynaklanan bu gecikmelerin teknik arka planı incelenmektedir. C++ kod örnekleriyle sunulan çözümde, client tarafında geçici süreyle server property güncellemelerinin bastırılması ve anti-cheat doğrulama başarısızlıklarına karşı rollback mekanizmasının kurulması gösterilmektedir. Ayrıca network performansını artırmak ve altyapı yükünü azaltmak için uygulanabilecek en iyi pratikler listelenmektedir.
Ghost Actor Reappearance: Destructible Grid Yapıları için Multiplayer Network Replication Desync Fix
Oyuncunuz bir toplama aracını savurur, ekranında ahşap bir duvar anında parçalanır, ancak 80 milisaniye sonra tam canla tekrar belirip 400 milisaniye sonra kalıcı olarak ortadan kaybolur. Genellikle "ghost build" olarak bilinen bu görsel anomali, client-side prediction uyumsuzluğunun ve out-of-order replication durumunun klasik bir göstergesidir. Hızlı tempolu multiplayer ortamlarda, bu kısa süreli state rollback'leri oyuncunun oyuna kapılma hissini (immersion) bozar ve görsel karmaşaya yol açar. Bu sorunu çözmek için geliştiricilerin, anlık local feedback ile yetkili (authoritative) server validation süreçlerini uyumlu hale getiren güçlü bir multiplayer network replication desync fix uygulaması gerekir.
Ghost Build Anatomisi: Prediction vs. Replication
Network oynanışı geliştirirken, geliştiriciler hız ve tepkisellik ile server authority arasındaki dengeyi kurmalıdır. Hareket ve yıkımın anlık hissettirmesi için client'lar, server onayını almadan önce aksiyonların sonuçlarını tahmin eder (predict). Örneğin, bir oyuncu yok edilebilir bir yapıya saldırdığında, client-side oyun mantığı hasar hesaplamasını anında çalıştırır, collision'ı devre dışı bırakır ve partikül efektlerini tetikler.
Arka planda (under the hood), bu durum client simülasyonunun server'ın önünde olduğu anlık bir sapma yaratır. Standart bir netcode modelinde bu sapma, server client'ın input RPC'sini işledikten ve yeni state'i geri replicate ettikten sonra çözülür. Ancak, bir paket gecikirse veya server tick rate (genellikle 20Hz - 30Hz) client frame rate'inin (60Hz - 120Hz) gerisinde kalırsa bir race condition meydana gelir. Client-side prediction actor'ı kaldırır, ancak server'ın bir sonraki replication güncellemesi hala actor'ın eski state'ini (canı dolu şekilde hayatta) içerir.
Bu özel race condition durumu, ahşap yapılarda son derece belirgindir. Taş veya metale kıyasla ahşabın can eşiği daha düşüktür (örneğin 300 HP'ye karşı 90 HP); bu da tek bir vuruşta yok edildiği anlamına gelir. Bu durum, oyuncu eylemi ile server onayı arasındaki zaman aralığını son derece daraltır. Herhangi bir replication gecikmesi, client'ın network driver'ını state'i eşitlemeye (reconcile) zorlar ve server actor'ı hala hayatta bildirdiği için actor'ı yeniden oluşturur.
Packet Loss ve Tick Rate Etkileri
Packet loss meydana geldiğinde, client'ın tahmin edilen yıkımı (predicted destruction) belirsiz bir boşlukta kalır. Eğer client'ın gönderdiği bir hasar paketi düşerse (dropped), server bunu asla işlemez ancak client hasarın uygulandığını varsayar. Client, actor'ın yok olduğu yönündeki yanlış varsayımla simülasyona devam eder. Server bir sonraki state güncellemesini ilettiğinde uyumsuzluk belirginleşir ve client'ı actor'ı dünyaya geri çağırmaya (spawn etmeye) zorlar. Bu reconciliation süreci, özellikle bu tür paket düşüşlerinin sık yaşandığı %1.5 ila %3 packet loss durumlarında rahatsız edici bir görsel sıçramaya (visual pop) neden olur.
Actor Lifecycle ve Channel Teardown Detayları
Unreal Engine ve benzeri modern multiplayer motorları, actor varlığını özel network bağlantı kanalları (connection channels) kullanarak senkronize eder. Replicate edilen her actor'a bir actor channel atanır. Server üzerinde bir actor yok edildiğinde, server bu kanalı kapatır ve client'a bir kanal kapatma kontrol mesajı (NetGUID retirement) gönderir.
Kritik sorun, property replication ile kanal kapatma (channel closure) işlemlerinin aynı replication yolunu paylaşmamasıdır. Property güncellemeleri (bir yapının Health değişkenindeki güncellemeler gibi) serialize edilir ve actor'ın normal replication paketinin bir parçası olarak gönderilir. Server bir hasar event'ini işler ancak actor'ı henüz Garbage Collection ile temizlememişse, actor tamamen yok edilmek üzere işaretlenmeden önce son bir property güncellemesini serialize edebilir. Property güncellemesini içeren UDP paketi, kanal kapatmayı içeren paketten önce ulaşırsa, client actor'ın canını günceller ve local predicted destruction durumunu geçersiz kılar.
Bu davranış, multiplayer desyncs fixing the Unreal Engine RPC replication issue breaking your states rehberimizde ele aldığımız diğer netcode senkronizasyon sorunlarıyla yakından ilişkilidir. O rehberde, RPC'ler ile property'ler arasındaki yürütme sırası (execution order) uyumsuzluklarının dünya durumlarını (world states) nasıl bozduğunu analiz ediyoruz. Benzer şekilde, oyuncu konumlandırmasını ele alırken de geliştiriciler, how to fix player location desync in Uefn and Unreal Engine multiplayer kılavuzumuzda ayrıntılarıyla açıklandığı gibi benzer tutarsızlıklarla sıkça karşılaşırlar.
Client sırasız (out-of-order) replication paketini işlediğinde, actor'ın server tarafında hayatta olduğunu görür ve actor'ı aktif havuza (active pool) geri dönmeye zorlar. Client, actor'ı kalıcı olarak silmek için kanal kapatma (channel closure) paketinin nihayet ulaşmasını beklemek zorunda kalır; bu da genellikle 0.4 saniye sürer.
Arka planda replication paketleri, tipik olarak 1400 bayt olan Maximum Transmission Unit (MTU) ile sınırlıdır. Oyun bağlantı hızınız sınırlandırılmışsa (örneğin MaxClientRate değeri 15000 bytes/sec olarak ayarlanmışsa), güncellemeler kuyruğa alınır ve birden fazla UDP paketine bölünür. Kanal kapatmaya yönelik kontrol mesajı güvenilir bir şekilde (reliable) gönderildiğinden onaylanması gerekir, oysa property güncellemeleri genellikle güvensiz (unreliable) olarak gönderilir. Network tıkanıklığı veya packet loss meydana gelirse, güvenilir kanal kapatma mesajı daha eski ve unreliable property paketlerinin gerisinde kalarak gecikebilir; bu da client'ın actor'ı yeniden oluşturmasına neden olan bir uyumsuzluğa yol açar.
C++ ile Predictive State Buffer Uygulaması
Ghost build sorununu çözmek için, client üzerinde predicted destroyed olarak işaretlenen actor'lar için gelen replication güncellemelerini durdurmalıyız. Bir client-side prediction buffer uygulayarak, property reconciliation işlemini belirli bir zaman penceresi boyunca (örneğin 500ms) bastırabiliriz; böylece server'ın kanal kapatma paketine ulaşması için yeterli zaman tanımış oluruz. Aşağıda, predictive destructible actor sınıfının eksiksiz ve çalışan bir C++ uygulaması yer almaktadır. Bu kod, replication davranışını override eder ve replication işleminin bastırılıp bastırılmayacağını belirlemek için yerel bir zaman damgası (local timestamp) kullanır.
// PredictedDestructibleActor.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "PredictedDestructibleActor.generated.h"
UCLASS()
class MULTIPLAYERGAME_API APredictedDestructibleActor : public AActor
{
GENERATED_BODY()
public:
APredictedDestructibleActor();
protected:
virtual void BeginPlay() override;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
// Server-authoritative health variable
UPROPERTY(ReplicatedUsing = OnRep_Health)
float Health;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Destruction")
float MaxHealth;
// Triggered when health changes on the client
UFUNCTION()
void OnRep_Health();
// Client-side prediction tracking flags
bool bClientPredictedDestroyed;
float ClientPredictionTime;
// Maximum time (in seconds) the client will suppress server updates
UPROPERTY(EditAnywhere, Category = "Networking")
float PredictionTimeout;
// Visual effect helper function
void TriggerDestructionEffects();
public:
// Called when the local player destroys the structure client-side
UFUNCTION(BlueprintCallable, Category = "Destruction")
void PredictDestruction();
// Resets the predicted state if the server rejects the destruction
void ResetPredictionState();
virtual void Tick(float DeltaTime) override;
};
Gelen server durumlarının nasıl filtreleneceğini gösteren ilgili implementasyon dosyası şu şekildedir:
// PredictedDestructibleActor.cpp
#include "PredictedDestructibleActor.h"
#include "Net/UnrealNetwork.h"
#include "Kismet/GameplayStatics.h"
APredictedDestructibleActor::APredictedDestructibleActor()
{
PrimaryActorTick.bCanEverTick = true;
bReplicates = true;
// Set a moderate update frequency to balance bandwidth and responsiveness
NetUpdateFrequency = 33.0f;
MaxHealth = 100.0f;
Health = MaxHealth;
bClientPredictedDestroyed = false;
ClientPredictionTime = 0.0f;
PredictionTimeout = 0.5f; // 500ms safety window
}
void APredictedDestructibleActor::BeginPlay()
{
Super::BeginPlay();
}
void APredictedDestructibleActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(APredictedDestructibleActor, Health);
}
void APredictedDestructibleActor::OnRep_Health()
{
// If the client has predicted this actor's death, suppress server property updates
if (bClientPredictedDestroyed)
{
return;
}
if (Health <= 0.0f)
{
TriggerDestructionEffects();
}
}
void APredictedDestructibleActor::PredictDestruction()
{
// Prediction only runs on the client that simulated the event
if (GetNetMode() == NM_Client)
{
bClientPredictedDestroyed = true;
ClientPredictionTime = GetWorld()->GetTimeSeconds();
// Hide the actor and disable collision immediately for responsive local feedback
SetActorEnableCollision(false);
SetActorHiddenInGame(true);
// Spawn local particles and audio instantly
TriggerDestructionEffects();
}
}
void APredictedDestructibleActor::ResetPredictionState()
{
bClientPredictedDestroyed = false;
SetActorEnableCollision(true);
SetActorHiddenInGame(false);
}
void APredictedDestructibleActor::TriggerDestructionEffects()
{
// Spawn local visual effects (e.g. wood splinters, dust clouds)
// and play destruction audio.
}
void APredictedDestructibleActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (GetNetMode() == NM_Client && bClientPredictedDestroyed)
{
float CurrentTime = GetWorld()->GetTimeSeconds();
// If the timeout expires and the server hasn't torn down the channel,
// the server must have rejected the damage. We must roll back.
if (CurrentTime - ClientPredictionTime > PredictionTimeout)
{
ResetPredictionState();
}
}
}
Bu predictive buffer'ı kullanarak, gelen OnRep_Health callback fonksiyonunun actor'ın görsel görünürlüğünü sıfırlamasını engelleriz. Bu, kanal kapatma (channel close) paketi gelene kadar client-side actor'ın gizli ve collision'sız kalmasını sağlar. Eğer server yıkımı onaylamazsa (örneğin bir Anti-Cheat doğrulama uyumsuzluğu nedeniyle), timeout bir rollback gerçekleştirilmesini tetikler ve simülasyonun kalıcı olarak desenkronize olmasını önler.
Rollback ve Validation Reddi Durumlarının Yönetimi
Bu replication fix'in kritik bir bileşeni de server'ın oyuncunun eylemini reddettiği durumları yönetmektir. Server'ın doğrulama (validation) mantığı, oyuncunun yapıya vurmuş olamayacağına karar verirse hasarı reddeder. Bu senaryoda client'ın, kalıcı bir desenkronizasyonu önlemek adına tahmin edilen yıkımı (predicted destruction) geri alması (roll back etmesi) gerekir; bu nedenle 500ms içinde server doğrulaması gelmezse timeout süresi actor'ın collision'ını ve görünürlüğünü eski haline getirir.
Manuel Implementasyon Yükü vs. Hazır Backend Çözümleri
Yukarıdaki C++ çözümü tekil actor'ların ghosting durumunu çözse de, bunu oyunun tüm haritasına/çevresine uygulamak karmaşıktır. Oyun geliştiricileri, her bir destructible nesne türü için manuel olarak prediction ve rollback kodları yazmalı, aktif prediction buffer'larını izlemeli, actor tick rate'lerini optimize etmeli ve network replication önceliklerini yönetmelidir. Bağımsız (indie) bir ekip için bu uç durumları (edge cases) oluşturup test etmek, kolaylıkla 4 ila 6 haftalık özel bir network mühendisliği mesaisi alabilir.
Bunu kendi başınıza inşa etmek; load balancer'lar, database sharding yapıları ve karmaşık WebSockets/UDP sunucuları kurmanızı gerektirir. horizOn ile bu backend hizmetleri hazır konfigüre edilmiş olarak gelir; böylece network altyapısını yönetmekle uğraşmak yerine oyununuzu yayınlamaya odaklanabilirsiniz. horizOn'un gerçek zamanlı lobi yönetimi ve oturum orkestrasyonu (session orchestration), oyuncu durumlarının ve maç özelliklerinin 50ms altı gecikme (latency) ile güvenilir bir şekilde senkronize edilmesini sağlayarak ghost build'lere yol açan replication gecikmelerini azaltır.
Multiplayer Network Replication Desync Fix için Uygulanabilir En İyi Pratikler
Destructible nesneler için netcode'unuzu optimize ederken, dünya durumunuzu senkronize tutmak için şu kuralları takip edin:
- Görsel Asset'leri Actor Lifecycle'dan Ayırın: Görsel geri bildirim için doğrudan
AActor::Destroy()fonksiyonunun yürütülmesine güvenmekten kaçının.bIsDeadgibi bir boolean replication flag'i tanımlayın ve yerel partikül sistemlerini anında tetikleyin. Bu, server'ın temizleme rutinlerini beklemeden client üzerinde collision'ı devre dışı bırakmanızı sağlar. - Channel Destruction İşlemini Property Güncellemelerine Göre Önceliklendirin: Yıkım güncellemelerinin network driver tarafından önceliklendirilmesini sağlamak için destructible nesnelerde
bOnlyRelevantToOwnerayarını yapın veyaNetPrioritydeğerini artırın. Bu sayede güncellemelerin, standart ambient property replication işlemlerinin arkasında kalıp gecikmesini önlersiniz. - Aktif Bir Prediction Timeout Penceresi Belirleyin: Client-side prediction'ın süresiz çalışmasına asla izin vermeyin. Server bir eylemi reddettiğinde client tarafında rollback yapılmasını zorunlu kılmak için her zaman bir güvenlik timeout'u (genellikle kabul edilebilir maksimum RTT değerinizin 1.5 ila 2 katı ve server tick sapması payı) uygulayın. Bu, bir paket düştüğünde actor'ın kalıcı olarak gizli kalmasını önler.
- NetUpdateFrequency Ayarlarını Optimize Edin: Normal koşullar altında destructible yapılarınızın güncelleme hızlarını düşük tutun (örneğin 10-15Hz). Tepkiselliği korurken boştaki bant genişliğini (idle bandwidth) azaltmak için güncelleme sıklığını yalnızca hasar aldıklarında dinamik olarak 33Hz'e yükseltin. Bu, yoğun oyuncu etkileşimleri sırasında network kullanımını dengeler.
- Server Validation Pipeline'larını Optimize Edin: Server-side hasar doğrulama işleminin hızlı ve hafif olmasını sağlayın. Server'ın bir vuruşu doğrulaması 100ms'den uzun sürerse, client prediction buffer'ı büyük olasılıkla timeout süresini aşacak ve belirgin bir titremeye (jitter) neden olacaktır. İşlem gecikmelerini en aza indirmek için doğrulama kodlarını sadeleştirin.
Özet ve Sonraki Adımlar
Replication desync sorunlarını çözmek, oyun motorunuzun network pipeline'ı hakkında derin bir anlayış gerektirir. Client-predicted destroyed olarak işaretlenmiş actor'larda server property güncellemelerini bastırarak ghost build'leri ortadan kaldırabilir, oyunculara kesintisiz ve akıcı bir deneyim sunabilirsiniz.
Multiplayer backend yapınızı ölçeklendirmeye ve senkronizasyon sorunlarını azaltmaya hazır mısınız? horizOn'u ücretsiz deneyin veya bir sonraki projenizde düşük gecikmeli (low-latency) session management uygulamasını nasıl gerçekleştireceğinizi öğrenmek için API docs sayfamızı inceleyin.
Kaynak: Ghost builds appear shortly after breaking wooden player build structures