NMORPG Notları: Netcode
6 min read

NMORPG Notları: Netcode

NMORPG'de, acaba yapabilir miyim dediğim kısım netcode, ya da bilinen adıyla "multiplayer" olunca, mümkün olduğunca hazır çözümlerden uzak durmaya karar vermiş, lakin işin tahmin ettiğimden daha zor olduğunu görünce pes edip Unity'nin çatısı altında geliştirilen MLAPI (yeni adıyla Netcode for GameObjects) ve daha sonra da Mirror deneyerek işin zor kısmını "outsource" etmeye karar vermiştim.

Sorumluluklardan kaçan oyun geliştiricisi

Bu kararın ardından, Mirror ve Netcode GO ile çözemediğim, çok fazla oyuncunun bir arada bulunduğu ortamlarda Unity üzerindeki ağ yükünü azaltıp, ucuz hilelere başvurmadan, çok fazla oyuncunun bir arada ahenk içinde haritada çılgın atlar gibi koşacağı haritaları mümkün kılma problemi için aylar süren bir planlama, yazma, deneme ve çöpe atma döngüsünün içinde buldum kendimi.

Mirror ve MLAPI'nin en sevdiğim özelliği, farklı iletişim çözümlerini desteklemeleri. Paketlerin her oyuncuya mutlaka ulaşmasının gerekmediği oyunlarda UDP; büyük çoğunlukla gitsin ama gitmese ya da geç gitse denilen oyunlarda Reliable UDP; ve sıra hiç şaşmasın, geç olsun ama mutlaka ulaşsın denilen oyunlarda TCP kullanılsın diyebiliyor, ve buna uygun "transport" kitaplığını seçebiliyorsunuz.

En azından transport kısmında kod yazmam gerekmesin diye Mirror ile gelen TCP iletişim kitaplıklarından biriyle denemeler yapmaya başladım ve büyük zevk aldım.

Fazla heyecanlanıp, RPG öğelerinden arınmış, karakter özelleştirme, ayarlar ekranı, sunucu seçimi, XP, loot vs. içermeyen, sadece yeni netcode kodunun çok sayıda (10, 20, 100, ...) oyuncuyla nasıl çalışacağını görmek ve RPG'deki battleground tarzı mini oyunlar için deneme tahtası görevi görecek Playground Warfare adında bir oyunu NMORPG'den önce, birkaç haftada çıkarma planı da yaptım ama ne yazık ki yine hesap tutmadı.

Playground Warfare

Şimdi gelelim işin teknik kısmına. Mirror ve MLAPI tecrübesi sonrası, ikinci kendi netcode denememden (ilki gRPC'ydi) aklımda kalanlar şöyle:

Çok Fazla Kullanıcı

500 kullanıcılık haritalar mümkün olsa da, aynı ekranda o kadar animasyonlu modeli aynı anda göstermek çılgınlık. Çok iyi bir bilgisayar, ya da iyi optimize edilmiş modeller, animasyonlar ve materyaller gerekiyor. Materyal, 3B modellerin ekranda görünmesini sağlayan shader ve ona ait parametreleri tutan tanımlar gibi düşünülebilir.

O an ekranda görünmeyen modellerin GPU'da render edilmemesi, gölgelerinin hesaplanmaması, animasyonların boşuna karaktere ait kemik pozisyonlarını değiştirerek vakit kaybetmemesi, karakterlere ait verilerin boşuna istemcilere ulaştırılmaması için irili ufaklı birçok optimizasyon mümkün.

Lakin, bunca optimizasyonu yapsam bile Unity'de ulaşılabilecek kullanıcı sayısı, Mirror geliştiricilerinin yaptığı testlere göre tahminlerimin çok gerisinde. Ama elbette bunu eski hilelerle aşmak mümkün...

IRC'nin popüler olduğu yıllarda, onlarca sunucunun bir araya gelip bir IRC networkü oluşturduğunu hatırlayanlar vardır mutlaka. Tek sunucu ile binlerce kullanıcıyı bir araya getirmek mümkün olmadığında, her sunucuda N kullanıcı tutarak, ve kullanıcılardan gelen mesajları gruplar halinde diğer sunuculara ileterek yüzbinlerce kullanıcıya ulaşmak mümkün oluyordu.

25 sene sonra, neden mümkün olmasın? C#, ya da Golang ile yazacağım ufak, oyun öğeleri içermeyen ve tek işi Unity sunucusuna bağlanarak kullanıcılar ve Unity ile yazılmış oyundaki nesneler arasında veri taşımak olan proxy'ler ile, Unity büyük yükten kurutlabilir. Oyun dünyasını yaşatmak zaten oldukça ağır bir yük. Bu çözüm ile, artık oyun bilgisini sadece proxy sunuculara iletmesi gerekecek. 500 değil, 10 bağlantı yetecek.

Protokol

Projeye zaten Golang ile bu işi yapabilir miyim, gRPC bu işi görür mü diyerek girmiştim. Protokol için hiç uğraşmayayım, gRPC ile devam edeyim derdim ama Unity/.Net ve gRPC-net uyumsuzluğu yüzünden yeni sürümü kullanamıyorum, eskisi de uzun süredir tavsiye edilmiyor.

Düz TCP sunucu, sıradan bit/byte serialization ile ilerlemeye ve bu sırada veri sıkıştırma, sadece değişen değerleri gönderme, hatta onları da tam değer olarak değil, önceki değerle olan fark şeklinde gönderme gibi teknikleri tecrübe etmek istedim. Fena da olmadı.

Karaktere ait, her 100 milisaniyede gönderdiğim bilgiler şöyle:

  • Koşma hızı
  • Konum (x, y, z)
  • Yönelim açısı
  • Oynatılan animasyon

Bu bilgileri, aşağıdaki gibi bir bit serisine dönüştürüp gönderiyorum:

  • 1bit: Karakterin hızı önceki kareye göre değişti mi?
  • 32bit: Karakterin hızı. Optimize edilmemiş hali bu. Oyunda şimdilik sadece atlar var. Maksimum hızın saniyede 25 metreden fazla olamayacağı gerçeğinden hareketle, her 100 milisaniyede 2,5m hareket demek ve 1cm altında bir hata payıyla 8bit bile hız için yeterli. Hızlanma ve durma, aniden olmayacağı için önceki değere göre farkını gönderdiğimde bu tahminen daha da aşağı inebilir. Elbette, fark mı yoksa tam değer mi olduğunu belirtmek için 1bit ayırmak gerekebilir.
  • 1bit: Konumun X koordinatı değişti mi?
  • 3*32bit: X, Y, Z koordinatları. Burada da, 1cm altı yanılmalar umrumda değil. 10cm bile fark yaratmayabilir. Burada da hızdaki gibi bir optimizasyon yapılabilir.
  • ...

Oyuncu ilk defa bağlanıyorsa, ya da ilk defa bağlanan bir oyuncuya ait bilgiler gönderiliyorsa, sıkıştırma uygulamayıp tüm bilgileri olduğu gibi gönderen basit kontrol mekanizması var.

Hangi animasyon oynatılıyor, hangi animasyona geçiş yapılıyor, geçişin kaçıncı saniyesi gibi bilgilerin mutlaka yeni kullanıcılara gitmesi gerek. Oyuna bağlandığınız anda, havada duran bir karakterin animasyonunun, tam olarak olması gerektiği gibi görünmesi için şart bu.

Animasyonlar için sıkıştırma uygulamak mümkün, Unity her animasyon ve geçişe bir tamsayı atıyor, ve bu tamsayıları oyunu geliştirirken bir listede tutup, tamsayıların yerine onların listedeki indisleri gönderilebilir. Uykusuz günlerin ardından 4*32 bit için değmez diyerek bıraktığım bir optimizasyon oldu. Animasyonlara ait bilgileri, Unity animasyon sisteminden okumaya çalışmak zormuş.

Linear Interpolation

Paketler Unity'den proxy sunuculara geliyor, karakteri ekranında görmek isteyen kullanıcıya paket iletiliyor, peki oyuncu bu paketleri nasıl görüntü haline getiriyor?

Unity'de, her nesneye bir kod eklemek mümkün. Start metodu içine yazdığınız her kod nesne ilk kez sahnede görüntülendiğinde çalışırken, Update metodu her frame'de çalıştırılıyor. Her karede 1cm hareket ettirdiğiniz bir karakter; 30fps ile oynayınca akıcı bir şekilde ekranda hareket ediyor. Siz bir tuşa bastığınızda, o karede hangi tuşlara basılmış kontrol edebiliyor, duruma göre yeni nesneler oluşturabiliyor, olanlara müdahale edebiliyorsunuz.

Karakterleri ekranda hareket ettiren kod da, öyle bir Update metoduna sahip. Sunucudan gelen her paket, nesnelere ait ID ve sonrasında nesnenin o anki hareket ve konumuma ait N bitlik bilgiyi içeriyor. Her bilgi bloğu, bir kuyruğa atılıyor ve Update metodu bu kuyruktan bir durum bilgisi alıyor, sonraki 100ms saniye boyunca çalışan her Update, karakterin önceki konumundan, o pakette belirtilen konuma doğru kaymasını, başka bir deyişle "lineer olarak interpole olmasını :)" sağlıyor ve bir sonraki Update, sonraki paket için aynı işlemleri tekrarlıyor.

Tüm oyunlar için yaşam döngüsü, Update içinde yapılan pis işler üzerine kurulu.

Golang mı C# mı?

Aynı kodu iki dilde birden yazmaya çalışmak eğlenceli. Unity'de oluşturulan paketi Golang tarafında alıp, bit serisini inceleyip koordinat bilgisini ayıklamak ve iki oyuncu arasındaki mesafe birbirine yakınsa iletmek PHP ile kod yazdığım ilk gün gibi heyecanlandırdı beni.

Ama tek Unity penceresinde hem oyun, hem proxy sunucuyu, hem de istemciyi bir arada tutmanın sayısız avantajı var. Veri ayıklama kitaplığındaki hatayı, bir de Golang tarafında çözmek gerekmiyor mesela. İki ayrı yerde debugging gerekmiyor. Prototipi Golang ve Unity/C# ile yazdıktan birkaç gün sonra, vakit tasarrufu için tamamen C# kullanmaya karar verdim, ama kaynak tüketimini en aza indirmek için production'a mutlaka Golang ile çıkacağım. Proxy sunucusu çalıştırma maliyeti, neredeyse oyun dünyası sunucusunu çalıştırmak kadar.

Gelecek Planları

Twitch yayını yapılacak. İlk hedef bu. Kendi başıma olduğum sürece, netcode da, oyun da tekrar tekrar geliştirmenin ilk zamanlarına geri dönecek belli. Genel mimariye karar vermek, eksikleri de olsa oynanabilir bir sürüm çıkarmak; belirli aralıklarla güncellemeler yapmak; oyun çevresine oyundan para kazanabilecek bir topluluk nasıl oluşturulabilir düşünmek gerekiyor. Twitch yayını bu işin ilk adımı.

Eklenti yazacak, API sayesinde Python/Golang/PHP ile oyunu kodla yönetecek, oyuna interaktif içerik ekleyecek, oyun içi öğeleri ticaret platformlarına taşıyacak bir topluluk modeli oluşturmak ve bunu hayata geçirmek gerekiyor. Oyunun teknik kısımları kadar, bu konuyu da konuşmak istiyorum ilgilenenlerle.

Geliştirme ekibi olarak çook az kişiyiz, şurada gördüğünüz gibi. Bunu aşalım hele.

NMORPG Geliştirme Ekibi