aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Oyun Sunucusu

Oyun State Senkronizasyonu: Sunucuda Tutarlılık

Çok oyunculu bir oyunda iki oyuncu aynı odada dururken birinin ekranında düşman tam karşıda, diğerinin ekranında ise üç adım solda görünüyorsa, sorun neredeyse her zaman oyun state senkronizasyonu katmanındadır. Sunucu ile istemcilerin paylaştığı "dünya durumu" zaman içinde birbirinden ayrışır ve bu ayrışma görünür hale gelir. Bu yazıda istemciler arası durum tutarlılığını nasıl kurduğumuzu, otoriter sunucu modelini, tick tabanlı güncellemeleri ve interpolasyonu pratik bir çerçevede anlatıyorum.

State nedir, neden senkronize edilmesi gerekir?

Oyun açısından state, dünyanın belirli bir andaki anlık görüntüsüdür: oyuncuların konumu, hızı, canı, envanteri, NPC'lerin davranışı, açık kapılar, düşen eşyalar. Tek oyunculu bir oyunda bu tablo tek bir bellek bölgesinde yaşar ve hiçbir tutarsızlık olamaz. Ağ üzerinden oynanan bir oyunda ise her istemci kendi state kopyasını tutar, sunucu da kendi kopyasını tutar. Işık hızı sonsuz olmadığı için bu kopyalar asla aynı anda güncellenemez; amacımız tutarsızlığı yok etmek değil, fark edilmeyecek kadar küçük ve geçici tutmaktır.

Senkronizasyon iki temel soruyu çözer: kimin söyledikleri "doğru" sayılır ve ara kareler nasıl doldurulur? İlk soru otorite, ikincisi interpolasyon konusudur.

Otoriter sunucu: tek bir gerçeklik kaynağı

Modern çok oyunculu mimarinin temeli otoriter (authoritative) sunucu modelidir. Burada gerçeğin tek kaynağı sunucudur. İstemci bir tuşa bastığında "şu an X konumundayım" demez; "ileri gitmek istiyorum" gibi bir girdi (input) gönderir. Sunucu bu girdiyi kendi simülasyonunda işler, sonucu hesaplar ve güncellenmiş state'i tüm istemcilere geri yayar.

Bu yaklaşımın en büyük faydası hile direncidir. İstemci "düşmanın canı 0" ya da "duvarın içindeyim" diye doğrudan state yazamaz; yalnızca niyet bildirir, kararı sunucu verir. Metin2 gibi MMORPG sunucularında da bu prensip geçerlidir: hasar, drop ve konum doğrulaması sunucu tarafında yapılır, aksi halde istemci bellek düzenlemesiyle her şey kırılır.

  • İstemci → sunucu: yalnızca girdi/komut gönderir (hareket yönü, atak, eşya kullan).
  • Sunucu: simülasyonu çalıştırır, çarpışmaları ve kuralları uygular.
  • Sunucu → istemciler: yeni state snapshot'larını yayınlar.

Tick tabanlı simülasyon

Sunucu dünyayı sürekli değil, sabit aralıklarla ilerletir. Her bir adıma tick denir. 20 tick/sn bir sunucu, dünyayı saniyede 20 kez (her 50 ms'de bir) günceller. Sabit tick kullanmanın nedeni belirlenebilirliktir: aynı girdiler aynı sırada ve aynı zaman adımıyla işlendiğinde sonuç her makinede aynı çıkar.

// Sabit zaman adımlı sunucu döngüsü (basitleştirilmiş)
const double TICK_RATE = 20.0;            // saniyede tick
const double DT = 1.0 / TICK_RATE;        // 0.05 sn
double accumulator = 0.0;
double previous = now_seconds();

while (running) {
    double current = now_seconds();
    accumulator += current - previous;
    previous = current;

    while (accumulator >= DT) {
        process_inputs();   // istemci girdilerini uygula
        step_world(DT);     // fizik + kurallar
        accumulator -= DT;
    }
    broadcast_snapshot();   // state'i istemcilere yay
}

Tick hızı bir denge meselesidir. Yüksek tick (örn. 60) daha akıcı ve hassas oynanış verir ama bant genişliği ve CPU maliyetini artırır. Hızlı tempolu nişancı oyunları yüksek tick ister; bir MMO için 10–20 tick çoğu durumda yeterlidir.

Snapshot, delta ve bant genişliği

Her tick'te tüm dünyayı tüm istemcilere göndermek savurganlıktır. İki yaygın optimizasyon vardır:

  • Delta sıkıştırma: Bir önceki snapshot'a göre yalnızca değişen alanları gönder. Hareket etmeyen bir NPC için veri akmaz.
  • İlgi yönetimi (area of interest): Bir oyuncuya yalnızca görüş/etki menzilindeki varlıkları gönder. Haritanın öbür ucundaki savaşı bilmesine gerek yok. Bu, MMO ölçeğinde olmazsa olmazdır.

Ayrıca her varlık için tüm alanları değil, ağ üzerinden anlamlı olanları (konum, rotasyon, animasyon durumu, can) işaretleyip serileştirmek gerekir. Konumu tam hassasiyetle göndermek yerine sınırlı bir aralıkta kuantize etmek (örn. 16 bit) paketleri ciddi şekilde küçültür.

Gecikme gizleme: interpolasyon ve tahmin

Snapshot'lar saniyede yalnızca 20 kez gelse bile oyuncu 144 Hz ekranında pürüzsüz hareket görmek ister. Boşlukları doldurmanın iki tekniği vardır.

Interpolasyon (uzak oyuncular için): İstemci, gelen son iki snapshot arasında konumu yumuşakça örnekler. Bunu yapabilmek için kasıtlı olarak küçük bir tampon (örn. 100 ms) geriden render eder; böylece her zaman aralarında geçiş yapacağı iki gerçek veri noktası olur.

// İki snapshot arasında doğrusal interpolasyon
Vec2 lerp(const Vec2& a, const Vec2& b, float t) {
    return { a.x + (b.x - a.x) * t,
             a.y + (b.y - a.y) * t };
}
// t: iki snapshot zamanı arasında render anının oranı (0..1)

İstemci tarafı tahmin (kendi oyuncun için): Kendi karakterinin sunucu yanıtını beklemesi can sıkıcı bir gecikme (input lag) yaratır. Bunun yerine istemci, girdiyi sunucuya gönderirken aynı anda yerel olarak da uygular. Sunucudan resmi state geldiğinde, tahmin doğruysa fark yoktur; yanlışsa yeniden uyumlama (reconciliation) ile düzeltilir: istemci sunucunun onayladığı state'e geri döner ve henüz onaylanmamış girdileri yeniden oynatır. Tutarsızlık varsa ufak bir "ışınlama" görülür; bunu yumuşatmak için düzeltme birkaç kare içinde harmanlanır.

Tutarsızlık kaynakları ve pratik öneriler

  • Paket kaybı: UDP üzerinde snapshot'lar kaybolabilir. Kritik olaylar (ölüm, eşya alımı) için güvenilir bir kanal/onay mekanizması kullan; konum gibi sürekli veriyi güvenilir göndermeye çalışma, zaten bir sonraki snapshot taze veriyi getirir.
  • Saat farkı: İstemci ve sunucu saatleri kayar. Snapshot'lara sunucu tick numarası/zaman damgası ekle; interpolasyonu duvar saatine değil bu zamana göre yap.
  • Float belirsizliği: Farklı derleyici/platformlarda kayan nokta sonuçları azıcık değişebilir. Tam belirlenebilirlik gerekiyorsa sabit nokta aritmetiği düşün; çoğu otoriter sunucuda buna gerek yoktur çünkü gerçeğin kaynağı tektir.
  • Test: Geliştirme sırasında yapay gecikme ve paket kaybı enjekte et (Linux'ta tc netem ile). 200 ms gecikmede iyi hissettiren sistem, 20 ms'de kusursuz olur.

Sık Sorulan Sorular

TCP mi UDP mi kullanmalıyım?

Hızlı tempolu, konum ağırlıklı oyunlarda genelde UDP tercih edilir; çünkü TCP'nin sıralı teslim garantisi, kaybolan eski bir paketi beklerken yeni paketleri de bekletir (head-of-line blocking) ve gecikmeyi büyütür. Sıra tabanlı veya gecikmeye duyarsız oyunlarda TCP fazlasıyla yeterlidir. Pek çok motor UDP üzerine kendi güvenilir katmanını kurar.

Tick hızını ne kadar yapmalıyım?

Oynanışa bağlıdır. Reaksiyon süresinin kritik olduğu nişancı oyunlarında 30–64 tick yaygındır; MMO ve strateji oyunlarında 10–20 tick yeterli ve daha ölçeklenebilirdir. Önce oynanış hissini, sonra sunucu maliyetini ölçerek karar ver.

İstemci tarafı tahmin hile riski yaratır mı?

Hayır, çünkü tahmin yalnızca görsel bir kestirimdir; nihai karar her zaman otoriter sunucuda verilir. İstemcinin yerel tahmini sunucuyla çelişirse sunucunun state'i geçerli sayılır ve istemci düzeltilir.

Çok oyunculu bir sunucu mu kuruyorsun? Otoriter mimari, tick tasarımı ve gecikme gizleme stratejilerini projenin türüne göre birlikte planlayalım. Benimle iletişime geç ve ihtiyacını konuşalım.

Bu kategorideki tüm yazılar →

Devamı için