Unreal Engine RPC Optimization: Her Tick'te Network'ünüzü Boğmayı Nasıl Durdurursunuz?
Özet olarak
Bu rehber, Unreal Engine projelerinde Tick bazlı RPC çağrılarının yarattığı network darboğazlarını önlemek için kullanılan Accumulator Pattern stratejisini ele almaktadır. C++ üzerinden hız sınırlama (rate-limiting), veri biriktirme ve struct batching teknikleriyle bant genişliği kullanımının nasıl %85 oranında azaltılabileceği detaylandırılmaktadır. Geliştiriciler için performanslı bir Multiplayer deneyimi sunmanın ve sunucu maliyetlerini optimize etmenin yolları teknik bir bakış açısıyla sunulmuştur.
Her Multiplayer oyun geliştiricisi eninde sonunda aynı network darboğazıyla (bottleneck) karşılaşır: 144 FPS ile çalışan bir client, her tick'te özel hareket durumunu (movement state) server'a göndermeye karar verir. Saniyeler içinde, server'ın network kuyruğu gereksiz Remote Procedure Call (RPC) çağrılarıyla tamamen dolar; bu da aşırı lag'e, paket kaybına ve kaçınılmaz bir bağlantı kopmasına (disconnect) neden olur. Client'ınız aslında kendi server altyapınıza bir Distributed Denial of Service (DDoS) saldırısı düzenlemektedir.
Bu senaryo, Multiplayer oyun mimarisindeki en yaygın hatalardan birini temsil eder. Geliştiriciler; özel oyuncu girdilerini, karmaşık araç fiziği durumlarını veya hızlı ateşleme mekaniklerini göndermek istediklerinde, akıcı bir tepkisellik için RPC'yi Tick() fonksiyonunun içine yerleştirmeyi mantıklı bir seçim olarak görürler. Ancak, Unreal Engine'in networking katmanı ara RPC'leri otomatik olarak ayıklamaz (cull). Eğer oyununuz her tick'te bir RPC gönderiyorsa, bunların tümü sıraya alınır ve iletilir.
Movement ve pozisyon güncellemeleri için, aradaki 143 frame neredeyse hiçbir zaman umurunuzda olmaz; diğer client'lara replicate etmek için yalnızca en son durum bilgisine ihtiyacınız vardır. Bu kapsamlı rehberde, unreal engine rpc optimization konusuna derinlemesine dalacağız; bu tick bazlı network çağrılarını nasıl kısıtlayacağınızı (throttle), akıllı durum biriktirmeyi (state accumulation) nasıl uygulayacağınızı ve Multiplayer bant genişliği yükünüzü nasıl ciddi oranda azaltacağınızı göstereceğiz.
Tick-Bound Network Event'lerin Tehlikesi
Bir çözüm uygulamadan önce, problemin anatomisini anlamak kritik önem taşır. Unreal Engine'de bir RPC tanımladığınızda (Server, Client veya NetMulticast), engine'in network driver'ına fonksiyon parametrelerini serialize etmesini ve bunları giden paket kuyruğuna (outgoing packet queue) eklemesini söylersiniz.
Kuyruğa Almanın (Queueing) Problemi
Unreal Engine, giden RPC'leri bağlantının NetUpdateFrequency değerine ve bant genişliği limitlerine göre paketler halinde gruplar. Eğer bir client yüksek bir frame rate'te her tick'te bir Server RPC çağırıyorsa, engine bu çağrıların her birini işlemeye çalışacaktır.
Eğer RPC Reliable olarak işaretlenmişse durum tam bir felakettir. Reliable RPC'ler teslimatı ve yürütme sırasını garanti eder. Network kanalı hızla dolacak ve eğer buffer taşarsa bağlantı engine tarafından zorla kapatılacak, sonuçta oyuncu disconnect olacaktır.
Eğer RPC Unreliable olarak işaretlenmişse, engine kuyruk dolduğunda paketleri düşürecektir (drop). Bu, sert bir bağlantı kopmasını önlese de devasa rubber-banding sorunlarına yol açar. Server 1. frame'i ve 2. frame'i alabilir, 3-100 arasındaki frame'leri düşürebilir ve ardından 101. frame'i işleyebilir. Sonuç, gameplay deneyimini mahveden düzensiz ve sarsıntılı hareketlerdir. Bu durum, ekiplerin fixing the Unreal Engine RPC replication issue breaking your states makalesinde olduğu gibi sorunları çözmeye çalıştığı yaygın bir kök nedendir.
Bant Genişliği Hesabı (Bandwidth Math)
Somut rakamlara bakalım. Bir Server RPC üzerinden basit bir vector (12 byte) ve bir rotator (12 byte) gönderdiğinizi varsayalım. RPC header yüküyle birlikte çağrı başına yaklaşık 32 byte tahmin edelim.
- 30 FPS'te:
30 * 32 byte = 960 byte/saniye(client başına kabaca 1 KB/s). - 144 FPS'te:
144 * 32 byte = 4.608 byte/saniye(client başına kabaca 4.6 KB/s). - 240 FPS'te:
240 * 32 byte = 7.680 byte/saniye.
Bunu bir battle royale oyunundaki 64 oyuncuyla çarpın; server'ınız aniden saniyede yarım megabyte'a yakın saf RPC yükünü işlemeye başlar — hem de sadece temel movement takibi için. Bu ölçeklenemez bir durumdur.
Adım 1: Accumulator Pattern ile Tick Bağımlılığını Kırmak
Unreal engine rpc optimization için en etkili strateji, network gönderim hızınızı client'ın rendering frame rate'inden ayırmaktır. RPC'yi Tick() içinde göndermek yerine, her tick'te yerel bir değişkeni güncellemeli ve ardından bu veriyi sabit ve öngörülebilir bir aralıkla (örneğin saniyede 10 veya 20 kez) server'a iletmek (flush) için bir timer kullanmalısınız.
Buna Accumulator Pattern diyoruz. Client, en son durumu sürekli olarak biriktirir ancak veriyi yalnızca network kapısı açıldığında iletir.
Hedef Frekansı Belirlemek
You do not need 144 updates per second for a smooth multiplayer experience. Most modern competitive shooters tick their servers at 30Hz or 60Hz. Therefore, sending client updates 15 to 30 times a second is usually more than enough, provided you are using proper client-side prediction and server-side interpolation.
Gönderim hızını sınırsız bir 144Hz'den sabit bir 20Hz'e düşürerek, söz konusu eylem için network trafiğinizi anında %85'ten fazla azaltmış olursunuz.
Adım 2: Implementing the Rate-Limiter in C++
Bunu C++'ta nasıl etkili bir şekilde uygulayacağımıza bakalım. Client'ın her tick'te hedef lokasyonunu ve rotasyonunu takip ettiği, ancak Server_UpdateTransform RPC'sini yalnızca önceden tanımlanmış bir network gönderim hızına göre gönderdiği bir sistem oluşturacağız.
Header Dosyası (.h)
Öncelikle değişkenlerimizi ve fonksiyonlarımızı özel APawn veya ACharacter class'ımızda tanımlıyoruz. Bir timer handle'a, bir güncelleme hızına ve gönderilmemiş verilerimizi tutacak değişkenlere ihtiyacımız var.
UCLASS()
class MYGAME_API AMyCustomPawn : public APawn
{
GENERATED_BODY()
public:
AMyCustomPawn();
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
protected:
virtual void BeginPlay() override;
// Verileri server'a göndermek için RPC. Hızlı ve sürekli güncellemeler için Unreliable olarak işaretlendi.
UFUNCTION(Server, Unreliable, WithValidation)
void Server_SendTransformUpdate(FVector NewLocation, FRotator NewRotation);
private:
// Network flush işlemi için Timer handle
FTimerHandle NetworkUpdateTimerHandle;
// Server'a saniyede kaç kez güncelleme göndermek istediğimiz
UPROPERTY(EditDefaultsOnly, Category = "Network")
float NetworkSendRate;
// Henüz gönderilmemiş yeni verimiz olup olmadığını takip eden flag
bool bHasPendingNetworkUpdate;
// Gönderilmeyi bekleyen biriktirilmiş veri
FVector PendingLocation;
FRotator PendingRotation;
// Veriyi flush etmek için timer tarafından çağrılan fonksiyon
void FlushNetworkUpdate();
};
Source Dosyası (.cpp)
Şimdi mantığı uygulayalım. Timer'ı BeginPlay içinde kuruyoruz, bekleyen değişkenlerimizi Tick içinde güncelliyoruz ve asıl network iletimini timer'ın yönetmesine izin veriyoruz.
#include "MyCustomPawn.h"
#include "TimerManager.h"
AMyCustomPawn::AMyCustomPawn()
{
PrimaryActorTick.bCanEverTick = true;
// Varsayılan olarak saniyede 20 güncelleme gönder
NetworkSendRate = 20.0f;
bHasPendingNetworkUpdate = false;
}
void AMyCustomPawn::BeginPlay()
{
Super::BeginPlay();
// Sadece yerel olarak kontrol edilen client network flush timer'ını çalıştırmalıdır
if (IsLocallyControlled())
{
float UpdateInterval = 1.0f / NetworkSendRate; // Örn: 1.0 / 20.0 = 0.05 saniye
GetWorld()->GetTimerManager().SetTimer(
NetworkUpdateTimerHandle,
this,
&AMyCustomPawn::FlushNetworkUpdate,
UpdateInterval,
true // Sürekli döngü (loop)
);
}
}
void AMyCustomPawn::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// Özel client-side movement mantığınızı burada çalıştırın
// Örn: FVector NewLoc = ...; FRotator NewRot = ...;
// SetActorLocationAndRotation(NewLoc, NewRot);
if (IsLocallyControlled())
{
// Burada RPC'yi çağırmak yerine, sadece en son durumu saklıyoruz
PendingLocation = GetActorLocation();
PendingRotation = GetActorRotation();
// Gönderilmeyi bekleyen taze veri olduğunu işaretle
bHasPendingNetworkUpdate = true;
}
}
void AMyCustomPawn::FlushNetworkUpdate()
{
// Eğer yeni veri yoksa (örneğin oyuncu hareketsiz duruyorsa), bant genişliğini boşa harcama
if (!bHasPendingNetworkUpdate)
{
return;
}
// En son biriktirilmiş durumu server'a gönder
Server_SendTransformUpdate(PendingLocation, PendingRotation);
// Bir sonraki tick durumu tekrar değiştirene kadar flag'i sıfırla
bHasPendingNetworkUpdate = false;
}
bool AMyCustomPawn::Server_SendTransformUpdate_Validate(FVector NewLocation, FRotator NewRotation)
{
// Buraya anti-cheat doğrulaması ekleyin. Lokasyon makul mü?
return true;
}
void AMyCustomPawn::Server_SendTransformUpdate_Implementation(FVector NewLocation, FRotator NewRotation)
{
// Server hız sınırlı veriyi alır ve uygular
SetActorLocationAndRotation(NewLocation, NewRotation);
// Not: Server daha sonra bunu diğer client'lara replicate edecektir,
// genellikle Multicasting ile DEĞİL, standart Replicated property'ler aracılığıyla.
}
Bu Mimari Neden Çalışır?
Bu kurulum, network flood problemini zarif bir şekilde çözer. Client ister 30 FPS ister 300 FPS ile çalışıyor olsun, server'ın saniyede tam olarak NetworkSendRate kadar güncelleme alması garanti edilir (paket kaybı olmadığını varsayarsak).
Ayrıca, bir erken çıkış kontrolü (!bHasPendingNetworkUpdate) uyguladık. Eğer oyuncu kahve almak için bilgisayar başından ayrılırsa, client RPC göndermeyi tamamen durdurur ve aktif oyuncular için kritik bant genişliğini boşa çıkarır. Bu, tutarlı server performansı sağlamak adına devasa bir kazanımdır.
Adım 3: Diğer Client'larda State Interpolation Yönetimi
Network gönderim hızını düşürdüğünüzde, server'daki hareket ve dolayısıyla bağlı diğer client'lardaki hareket kesik kesik hale gelecektir. Eğer güncellemeleri 10Hz ile gönderirseniz, karakter 60 FPS bir monitörde saniyede 10 kez ışınlanıyor gibi görünecektir.
Bunu düzeltmek için karakteri doğrudan yeni lokasyona "snap" edemezsiniz. Interpolation (ara değer bulma) kullanmalısınız. Server, NewLocation verisini simulated proxy'lere (oyuncuyu gözlemleyen diğer client'lar) replicate ettiğinde, bu client'lar zaman içinde mevcut pozisyonlarından replicate edilen hedef pozisyona doğru FMath::VInterpTo ile pürüzsüzce geçiş yapmalıdır.
Bu, saniyede 5 veya 10 güncelleme gibi agresif bir limit olsa bile, görsel temsilin akıcı kalmasını sağlar. Eğer interpolation sırasında karakterlerin yanlış şekilde "snap" olmasıyla ilgili sorun yaşıyorsanız, how to fix player location desync in UEFN and Unreal Engine multiplayer makalesini inceleyebilirsiniz.
Adım 4: Karmaşık RPC'ler için Struct Batching
Eğer oyununuz birden fazla farklı değişken göndermeyi gerektiriyorsa, ayrı ayrı birden fazla RPC göndermeyin. Her RPC'nin temel bir header yükü vardır (genellikle en az 1-2 byte, ancak payload serialization dikkate alındığında pratikte daha fazladır).
Aynı network flush içinde Server_SendHealth(), Server_SendArmor() ve Server_SendPosition() çağrılarını yaparsanız, header maliyetini üç kez ödersiniz.
Bunun yerine, network payload'larınız için özel bir struct oluşturun.
USTRUCT()
struct FPlayerNetworkState
{
GENERATED_BODY()
UPROPERTY()
FVector Location;
UPROPERTY()
FRotator Rotation;
UPROPERTY()
uint8 CurrentWeaponIndex;
UPROPERTY()
bool bIsCrouching;
};
Bu tek struct'ı timer tabanlı RPC'niz aracılığıyla geçirin. Unreal Engine'in reflection sistemi, bu değişkenleri tek bir paket payload'ı içine verimli bir şekilde paketleyecek ve bağlantınızdaki byte ayak izini minimize edecektir.
Unreal Engine RPC Optimization İçin 5 En İyi Uygulama
Oyununuzun yerel testlerden binlerce eşzamanlı oyuncuya ölçeklenebilmesini sağlamak için network mimarisinde şu temel kuralları benimseyin:
- Asla Bir Geçit (Gate) Olmadan Tick İçinde RPC Göndermeyin: Bunu katı bir kural olarak kabul edin. Eğer bir RPC
Tick()içindeyse, bir zaman kontrolüyle (örn.if (TimeSinceLastRPC > 0.1f)) korunmalı veya döngüsel bir timer aracılığıyla yönetilmelidir. - Unreliable'ı Reliable'a Tercih Edin: Sürekli güncellenen veriler (hareket, etrafa bakma, sürekli ışın silahları) için her zaman Unreliable RPC'leri kullanın. Bir paket düşerse, bir saniyenin küçük bir kısmı sonra gelen bir sonraki paket zaten onun üzerine yazacaktır. Reliable RPC'ler kesin durum değişiklikleri (örn. silah ateşlendi, eşya alındı, oyuncu öldü) için kesinlikle ayrılmalıdır.
- Float'lar ve Vector'ler İçin Quantization Kullanın:
FVectorverisi gönderirken nadiren tam floating-point hassasiyetine ihtiyaç duyarsınız. Unreal Engine, RPC'lerde vektörleri quantize etmenize olanak tanır (örn.FVector_NetQuantize100); bu, değerleri iki ondalık basamağa yuvarlar ve onları göndermek için gereken bant genişliğini önemli ölçüde azaltır. - Downstream Veriler İçin Standart Replication'ı Tercih Edin: Client'lar verileri server'a göndermek için RPC'leri kullanmak zorunda olsa da, server sürekli verileri aşağıya göndermek için nadiren Multicast RPC'leri kullanmalıdır. Server bir
UPROPERTY(Replicated)değişkenini güncellemeli; Unreal'ın yerleşik replication manager'ının bant genişliği optimizasyonunu, önceliklendirmeyi (prioritization) ve alaka düzeyi sıralamasını (relevancy sorting) otomatik olarak yönetmesine izin vermelidir. - Erken ve Sık Profil Oluşturun: RPC'lerinizin frame başına tam olarak kaç byte tükettiğini görselleştirmek için
net.DumpRelevantActorskomutunu ve Network Profiler aracını (Engine binary'lerinde bulunan NetworkProfiler.exe) kullanın. Optimizasyon kazanımlarınızı asla tahmin etmeyin; onları ampirik olarak ölçün.
Altyapı ve Backend Ölçeklendirmesi
Unreal Engine'in netcode karmaşıklıklarında ustalaşmak devasa bir çabadır. Sırf dedicated server'larınızın bant genişliği limitlerini aşmadan sorunsuz çalışmasını sağlamak için timer handle'larını ayarlamak, vector'leri quantize etmek ve desync'leri hafifletmek için saatler harcıyorsunuz.
Oyun içi kodunuz nihayet optimize edildiğinde, bu server'ları hala küresel olarak yaymanız (deploy) ve ölçeklendirmeniz gerekir. Bunu kendiniz inşa etmek; fleet manager'lar, load balancer'lar, database sharding ve SSL sertifika yönetimi kurmanızı gerektirir — bu da sizi asıl oyun tasarımından uzaklaştıran kolayca 4-6 haftalık yoğun bir altyapı işi demektir.
horizOn ile bu backend hizmetleri, oyun geliştiricileri için özel olarak önceden yapılandırılmış olarak gelir. Kutudan çıktığı haliyle ölçeklenebilir dedicated server hosting, gerçek zamanlı veritabanı senkronizasyonu ve sağlam analitikler elde edersiniz; bu da altyapınızı değil, oyununuzu yayınlamanıza olanak tanır.
Son Düşünceler
Unreal engine rpc optimization konusunun anahtarı, network bant genişliğinin sonlu ve oldukça değişken bir kaynak olduğunu fark etmektir. Network katmanına standart bir frame buffer gibi davranamazsınız. Tick odaklı yürütmeden uzaklaşıp Accumulator Pattern'ı benimseyerek, oyununuzun veri çıkışı üzerinde tam kontrol sahibi olursunuz. Server yükünü azaltır, paket kaybını hafifletir ve dalgalı internet bağlantılarına sahip oyuncular için çok daha akıcı bir deneyim yaratırsınız.
Oyununuzu optimize etmenin devam eden bir süreç olduğunu unutmayın. Sizi network flood'larından kurtarması için varsayılan engine davranışlarına güvenmeyi bırakın. Veri akışınızın kontrolünü açıkça elinize alın. Bu hız limitlerini mevcut prototipinizde uygulayın, Network Profiler kullanarak öncesi ve sonrası metriklerini izleyin ve server performansınızın yükselişini izleyin.
Yeni optimize edilmiş Multiplayer backend'inizi ölçeklendirmeye hazır mısınız? horizOn'u ücretsiz deneyin veya profesyonel oyun altyapısının ne kadar basit olabileceğini görmek için API dokümantasyonuna göz atın.