Multiplayer Oyun Geliştirirken Bilmeniz Gerekenler — Bölüm 1
Eminim insanlık bir gün oyunlardaki LAG problemini çözecektir. O zamana kadar biz Netcode kabilesi oyunculara adil bir oyun sunabilmek için saçlarını beyazlatmaya devam edecek 🚬
Bilgisayar bilimlerinin en zor problemlerinden biridir. Gerçek zamanlı ve Multiplayer bir oyun geliştirmek. Sadece İnternetin kaotik yapısı ile uğraşmazsınız. Oyunun koştuğu donanım. Oyunun oynandığı işletim sistemi ve oyunun geliştirildiği motorun da detaylarını bilmeniz gerekir. Ağ katmanındaki gecikmeleri oyuncudan saklamak için de bir çok hile bulmalısınız. Ezcümle, giriş seviyesi yüksektir. Mücadele ister. 🖖
Bu seride çok oyunculu ve gerçek zamanlı oyun geliştirirken geçmemiz gereken kapıları gezeceğiz. Tüm her şeyi açıklama gibi bir kaygım yok çünkü her başlık kendi başına bir uzmanlık gerektiriyor o nedenle sadece kapıyı göstermeyi hedefliyorum.
haydi başlayalım 🪩 🕺
Kavramlar:
İlk olarak karşımıza çıkacak Network kavramlarından başlasak fena olmaz. Çünkü bütün mühendislik bilgimizi bu kavramların değerlerini en iyi hale getirmek için harcayacağız. İşte en çok duyacağımız kavramlar 👇
Lag (Network Latency)
Etimolojik anlamda Lag, geride kalan, geç gelen anlamları ile beraber bilgisayar bilimlerinde gecikme anlamına gelir. İki unsur arasında gerçekleşen bir olayın zamanını temsil eder. Bizim alanda ise bir ağ paketinin istemciden (Client), sunucuya ulaşması ve geri dönmesi sırasında geçen süre bizim ağ gecikme süremizdir.
Ping
Ping, ICMP (Internet Control Message Protocol) denen protokolün içindeki bir ölçüm aracıdır. İstemciden çıkan bir ICMP paketinin sunucuya ulaşması ve sunucudan istemciye tekrar gönderilmesi sırasındaki süreyi ölçer. Zamanla oyuncular arasında bu kavram bükülmüş Network Latency’i temsil eder hale gelmiştir.
RTT (Round Trip Time)
İstemciden çıkan bir paketin, sunucuya ulaşıp, tekrar sunucudan istemciye başladığı yere dönmesi arasında geçen süreyi temsil eder. Dikkat ederseniz Ping tanımının aynısı, farkı ise Ping sadece Network katmanındaki gecikmeyi hesaplar, RTT ise istemciden üretilen paketin süresi ile sunucudaki cevap zamanını gecikmelerini de içinde barındırır.
Jitter
Bildiğimiz gibi İnternet trafiği çoğu zaman tutarlı değildir. İstemciden gönderdiğimiz paketler, sunucuya giderken farklı zamanlarda gidebilir veya yolda kaybolabilir (Packet Loss) işte bu duruma Jitter deniyor. Online oyunlardaki en bela durumlardan biridir. Genelde bir sunucuya giderken kullandığımız Router’lar değiştiğinde yada yerel ağınızdaki Wifi geçişlerinde Jitter yaşanır.
Çok katmanlı bir olay. Kaçınmak donanımsal dünyada mümkün değil ancak istemci tarafında biz Lag Compensation ile dengelemeye çalışırız ki bu konuya yazının ilerleyen bölümlerinde değineceğiz.
Yukarıdaki şekile baktığımızda. Dikkat edeceğiniz üzere ilk paket transferi sıralı ve sabit zaman aralıkları ile sunucuya transfer ediliyor. Keşke dünya hep böyle olsa ama değil.
İkinci şekilde ise fark ettiğiniz gibi paketler arasındaki iletim zamanı farklılık gösteriyor ama daha da önemlisi önce atılan B paketi daha sonra atılan C paketinden geç sunucuya ulaşarak Jitter’a neden oluyor.
Not: Bu dalgalanmalar istemci tarafında Interpolation/Extrapolation ve Client Prediction gibi tekniklerle giderilmeye çalışılır. Bu konulara ileri ki bölümlerde değineceğiz ✌️
Packet Loss
Bir veri paketinin ağ biriminden, diğer ağ birimine giderken çeşitli nedenlerden dolayı kaybolmasına verilen isimdir.
Örneğin; Oyunda tekme tuşuna bastığımızda normalde sunucu üzerinden diğer bağlı istemcilere bu girdinin (Input) iletilmesi gerekir fakat Packet Loss nedeni ile bu girdi sunucuya ulaştırılamadığında Packet Loss olarak nitelendirilir.
Yapacağımız şey bu kaybın olduğunu anlayıp mümkünse tekrar göndermek. Buna Reliability deniyor. Örneğin TCP Reliable bir protokoldür fakat UDP değil ama UDP üzerinden KCP gibi Reliability’i öncelikleyen Protokoller de yazılabiliyor.
TCP’ye Reliable dedik ama bu Packet Loss olmayacağı anlamına gelmiyor. Reliability’i sağlayacağım diye protokol bazında düzeltmelerden dolayı da Packet Loss yaşanması mümkündür.
Packet Loss’un nedenleri çok!
Wifi dan GSM’e geçiş, Sunucudaki yüksek CPU, Ağ Bant Genişliğinin dolması, paketin geçtiği Route gibi temel nedenlerin yanı sıra oyun içindeki paketi taşıyan Socket katmanındaki bir mutex’in haddinden fazla beklemesi bile buna neden olabilir.
Topoloji:
Multiplayer oyun geliştirmeye başladığınızda ağ ile alakalı ilk karar vermemiz gereken konu topolojinin nasıl olacağı. Yani istemci-sunucu iletişiminin nasıl yerleşeceği ve şekli! 🍏
Bu karar tüm oyunu, kod yazış şeklinizi ve hatta oyun mekaniklerini etkiler. O nedenle ne yaptığınızı iyi bilmeniz önemlidir.
Aşağıda bilinmesi gereken ve günümüz oyunlarının kullandıkları ağ topolojilerine yer verdim.
Peer-to-Peer (P2P)
Eşler arası iletişim anlamına gelen Peer to Peer ana bir sunucuya gereksinim olmadan eşlerin birbiri ile direkt iletişim kurmasına olanak sağlayan ve çok kullanılan bir yöntemdir. Örneğin GTA Online veya F1 gibi oyunlar P2P çalışırlar. Diğer yandan Nintendo Switch’deki oyunlar da (Mario Kart) P2P oynandığı bilinen oyunlardır.
Avantajları:
- Sunucuya gereksinim duymaz.
- Altyapı olarak ucuzdur.
- Sunucu konumlandırma ve infra işleri ile uğraşmazsınız ki bu çok iyidir.
- Sunucu için ayrı kod yazmazsınız.
Dezavantajları:
- Hileye açıktır.
- Peer’ların bağlantısının iyi olması gereklidir.
- Oyuna bağlı Peer sayısı ile oyun deneyiminin kötüleşmesi doğru orantılıdır.
- Peer yönetimi için yazılacak kod daha karmaşıktır.
- Peer’ların donanım farklılıkları oyun deneyimine etki edebilir.
“Peer sayısı ile oyun deneyiminin kötüleşmesi doğru orantılı” dedik. Bu cümleyi biraz açmak istiyorum 👉 P2P oyunlarda oyuna bağlanan oyuncu sayısı arttıkça sizin diğer oyunculara göndereceğiniz ve alacağınız paket o kadar çok artar ki ister istemez Latency yaşarsınız. Bu da oyun deneyimini etkiler. Bir oyun için ölümcüldür.
Şu cümleyi de açmak isterim “Peer yönetimi için yazılacak kod daha karmaşıktır.” 👉 Şöyle ki; Basit bir İstemci-Sunucu haberleşmesinde sunucu için bir Socket nesnesi tanımlarsınız ve oradan async olarak iletişim kurarsınız o kadar. P2P’de ise bu Socket nesnesini bir Array’e çevirmeli ve bu Array’in index’lerini de oyun içinde doğru adreslemelisiniz. Yani tek bir Socket yerine oyunucu sayısı kadar bir Socket ile Code Base’in organizasyonunu yapmanız gerekir.
Yönetmemiz gereken başka bir konu da istemcilerin hangisinin otorite alınacağı ve oyun içinde otoritenin değişeceği senaryoları. Bu konuya Client-Authoritative kısmında değineceğim.
Ha unutmadan. P2P için Socket’imize Nat Hole Punch denen bir tekniği adapte etmeliyiz. Bu da yönetmemiz gereken başka bir kavram olarak ortaya çıkıyor. Az ve öz açıklamaya çalışayım;
Hole Punch
P2P için Code Base’e eklenen diğer bir kavramsal karmaşıklık ise NAT Traversal’in üzerinde dönen Nat Hole Punch dediğimiz tekniktir.
Bildiğimiz gibi NAT iç network (Intranet) ile dış network (Internet) arasındaki bağlantıları organize ederek içerideki bilgisayarların gerçek bir IP adresi yerine, yerel bir IP adresine sahip olmalarını sağlıyor ki IPv4 bitmesin.
Nat Hole Punch paketleri daha önce belirlenen iç ve dış IP’lerin ve Port’ların bilgisini geçici olarak tutarak paketlerin buna göre direkt ilgili Peer’a gönderilmesini sağlıyor. Önce her iki Peer kendini bir HTTP ucuna veya bir Game Server’a tanıtıyor (Rendezvous Server) ve hangi Port’tan iletişim kurduğunu söylüyor. Bu bilgiler iki Peer arasında paylaşılarak bağlantı sağlanmaya çalışılıyor. Yani üçüncü bir ağ birimi daha var işin içinde.
P2P Client-Authoritative
P2P oyunlarda karar vermemiz gereken bir husus da oyundaki kazanıma veya skorlara kimin karar vereceği.
Bu aşamada karşımıza Client-Authoritative denen bir kavram çıkıyor.
Client-Authoritative, oyunda bağlı olan Peer’lardan birini otorite seçilip oyunun fiziğini bu Master Peer’da çalışmasına dayanır. Skor, sağlık veya güç gibi değişkenleri bu Master Peer’dan diğer Peer’lara senkronize edilir. Diğer Peer’lar da buradan gelen verileri mutlak doğru kabul ederek oyun mekaniğini işletir. Yani Master Peer sanki bir DGS (Dedicated Game Server) gibi hareket eder.
Avantajları:
- Oyun fiziği bir yerde çalıştığından diğer Peer’ları senkronize etmesi kolaydır.
- Gerektiğinde çok kod değiştirmeden DGS’ye geçilebilir.
Dezavantajları:
- Master Peer’ın donanımı zorlanabilir yani CPU, Memory gibi kaynakları aşırı kullanabilir.
- Diğer Peer’lara adil olmayan bir oyun sunulabilir. (Host-Advantage)
- Master Peer disconnect olduğunda oyun durur.
Dedicated Server
Multiplayer oyunların çok kullandığı diğer bir topoloji de Dedicated Game Server (DGS) olarak karşımıza çıkıyor. CS:GO, Fortnite, COD gibi FPS oyunlarda tercih edilir. Burada tüm Peer’lar sadece oyun için özel ayrılmış sunucuya bağlanır, tüm Peer girdilerini, oyun mekaniğinin durumlarını sunucu bilir ve diğer Peer’lara yayınlar. Oyunun durumunu sunucu tuttuğu için kesin olarak Peer’lar senkronizedir ve otorite sunucudur.
Avantajlar:
- Hilelere karşı dayanıklıdır
- Code Base tek bir soket üzerinden döner, Peer tarafında geliştirmesi daha basittir.
- Sunucu bağlantısı daha kararlıdır.
- Gecikmeler (Latency) sadece Peer ve Server arasındaki iletişimi kapsar ve diğer Peer’lardan etkilenmez.
Dezavantajlar:
- Pahalıdır.
- Koca bir sunucu filosu yönetmek için altyapı bilgisi ve ekip gerektirir.
- Oyun mekanikleri için sunucu tarafında da kod yazılması gerekir.
Aslında DGS yapısı oyuna göre çok değişiklik gösteriyor. Örneğin bir MMORPG oyununda sunucuların sorumlulukları oyundaki dünyalara göre farklılık gösterebiliyor veya bölgelere göre farklı Game Server’lar birbirleri ile haberleşip bir yapı kurduğu topolojilerde kullanılıyor.
Dedicated Game Server tarafında diğer bir yöntem de oyun fiziğinin veya durumunun istemci tarafında tutulduğu yapı. Non-Authoritative Server veya Relay Server olarak geçer. Relay Server demek daha bir doğru geliyor bana.
DGS Relay Server:
Sunucuda fizik çalıştırmak elbet çok avantajlı ama maliyeti düşürmek için yapılacak ilk hareketlerden biri de sunucudaki fizik işlemlerini istemciye yüklemek. Bu aşamada Dedicated Game Server sadece oyun paketlerini alıp diğer Peer’lara yayın yapma sorumluluğu alır.
Avantajları:
- Sunucu yükü çok azdır
- Code Base düşüktür. Sadece socket ve game session katmanı vardır.
Dezavantajları:
- Güvenlik yüzeyini arttırır. Hileye daha açık hale getirir.
- Fizik, Peer’larda döndüğü için desync olasılığı artar.
- Deterministik yapılar ister. (cc Fatih Kahveci)
Protokol:
Ağ topolojisini belirlediniz, artık sıra bu ağ üzerinden istemcinin ve sunucunun birbirleri ile nasıl iletişim kuracağını belirlemeye geldi.
Multiplayer oyunlarda seçebileceğimiz iki temel protokol var UDP ve TCP.
Örneğin sıra tabanlı oyunlarda TCP üzerinden giden WebSocket protokolü yeterli olurken gerçek zamanlı bir futbol oyununda ise UDP protokolü tercih edilir. Halo’nun neden UDP kullandığını ve StarCraft’ın neden TCP kullandığını aşağıdaki detayları okuduktan sonra anlaşılacağını umuyorum.
Şimdi gelelim bilmemiz gereken protokollere ve detaylarına;
TCP
TCP yani Transmission Control Protocol, Internetin kutsal protokolüdür. Internet üzerinden bilgi taşımak için en güvenilir yol olarak karşımıza çıkar. TCP, istemci ve sunucu arasında seremoni ile (Three-way Handshake) bağlantı sağladıktan sonra her iki tarafta verilerin iletileceğinden emin olur. Bir tünel oluşur ve bu tünel üzerinden veriler aktarılır.
Avantajları:
- Güvenilirdir. Sunucuya gönderilen paketin kesin olarak iletildiğini bilirsiniz.
- Paketlerin sırasını garanti eder, bir önceki paket bir sonraki paketten sonra gitmez.
- Kolaylıkla uçtan uca güvenli hale getirilebilir. (TLS)
- Globaldir. Tüm dünya ile bu protokol üzerinden kolaylıklar iletişim kurabilirsiniz.
Dezavantajları:
- Kaynak açısından pahalıdır. İşletim sisteminde ve kullandığınız yazılım dilinde belirli bir memory alanı allocate edilir. Yüksek yüklerde farkedilir bir kaynak tüketimi vardır.
- İlk bağlantı yavaştır. Daha önce TCP’nin bağlanması için bir seremoni demiştik. Three-way Handshake yüzünden hep.
- Jitter’a daha açıktır çünkü TCP error check’i kendi yapar ve hataları düzeltmek için yavaşlar. Bu da Latency’e neden olur. Sadece protokolden latency’nin artması sinir bozucu olabiliyor.
Not: Oyununuzda Latency sizin için çok önemli ise TCP kullanmayın. TCP’nin protokol yetenekleri gereği ve yukarıdaki saydığımız diğer nedenlerden dolayı paketler istediğiniz süreler içerisinde gitmeyecektir. Bunun için en iyi yolumuz UDP 🕺
UDP
Gerçek zamanlı oyunların vazgeçilmez protokolü, UDP yani User Datagram Protocol. Tercih edilmesinin nedeni ise oldukça şuursuz olması. Veri göndermek için bağlantı seremonisine gerek yok, veri doğrulama yok, state yok iletişimi geciktiren gereksiz tüm yetenekler atılmış. Eğer biraz yetenekli olmasını isterseniz kendiniz ufak adaptorler ile genişletebiliyorsunuz.
Avantajları:
- Basit, hızlı ve hafiftir (header sadece 8byte)
- Stateless’dır bu yüzden sunucu ve istemci de kaynak tüketimi azdır.
- Güvenilirlik (Reliability) önemsenmediğinden protokol Jitter yaratmaz 😐 .
- Tek yönlüdür. Sadece mesajı karşı tarafa ulaştırmaya çalışır ve bu onu hızlı yapar.
Dezavantajlar:
- Packet Loss, Packet Ordering gibi konularda kendi başınızasınız. Eğer gerekirse bu implementasyonları siz eklemelisiniz.
- UDP protokolü Unreliable’dır. Verinin karşı tarafa iletildiğinden hiç bir zaman emin olamazsınız bunu göz önüne alıp tasarım yapmalısınız.
UDP, paketleri diğer tarafa iletmede güvensizdir (Unreliable) tamam ama bu aynı zamanda UDP’nin güçlü yanıdır. Ayrıca paketleri sırası ile göndermeyi garanti etmez ama bunlar güzel sorunlar ve bir çok kütüphane bunları zaten çözüyor. Burada esas meydan okuma Paket Loss! Fakat oyun tarafında Client Prediction, Rollback gibi tekniklerle bu kayıpları oyuncuya farkettirmeden aşabiliyoruz.
Port
UDP ve TCP protokollerinin uygulama katmanında iletişim kurması için 16bit’lik unsigned bir sayıya ihtiyacı vardır (0 ile 65535) buna Port diyoruz. IP adresi paketin gideceği sunucuyu belirtir, kullanılan protokolün header’ından çözülen Port numarası ise o sunucunun üzerinde çalışan uygulamayı temsil eder.
Game Server belirli bir portu kullanarak socketi dinler ve gelen paketler ile iletişim başlar.
Tabi onlarca yıllardan beri o Port’a çökmüş yazılımlar yüzünden Port numaralarını kafamıza göre seçemiyoruz. 0-1024 arası Port’lar da DNS, DHCP vb. ağın çalışmasını sağlayan servisler için ayrıldığından bize insanın okutabileceği az bir dilim kalıyor.
Peki nasıl özgün bir Port numarası seçeceğiz?
Öncelikle Well-known Ports diye geçen belli başlı Port numaralarından uzak duracağız. Listeye buradan ulaşabilirsiniz. Buradaki boşluklardan beğendiğiniz bir Port bulmak daha kolay olur.
Neden bilinen bir Port seçmemeliyiz?
Bir çok Router’da bilinen Port’larla ilgili ön tanımlı koşullar vardır. Örneğin TCP/5060 VOIP portunda direkt Port Mapping vardır, QoS kuralları vardır. Eğer Game Server’ı bu Port numarası üzerinden çalıştırırsanız VOIP protokolü davranışı beklenebilir, rate limit uygulanabilir, beklenmedik semptomlar gösterebilir. Bunun farkedilmesi de uzun sürebilir.
Ufak bir tecrübemi paylaşayım 👨🦳
Biz bir Game Server’ımızın Portunu TCP/1433 yapmıştık. Bildiğiniz gibi bu çok bilindik MSSQL Sunucusunun Port numarası. Derken yanlış hatırlamıyorsam Taiwan’da bir ISP TCP/1433 Port’undan yayılan Malware’ı engellemek için tüm ağında bu portu kapattı ve bizim Game Server’lar down oldu. Bulana kadar baya debelenmiştik.
Özetlersek;
- MSSQL, MySQL vb. çok bilinen Port numaralarından uzak durun.
- 6000–9000 arası Port numaralarında resmi olmayan bir çok alan vardır. Buralardan bir numara seçilebilir. Ben MaestroPanel’in portunu 9715 yapmıştım. Unity veya Unreal Game Server’ların 7777 kullanır. Photon 27000 kullanır. Feyizlenilebilir.
Kapanış
Bu yazıda temel seviyede oyunlardaki ağ yapısını ve kullanılan protokolleri keşfettik. Bu işi yönetirken de takip etmemiz gereken Latency, Packet Loss gibi metriklerimizi tanıdık. Bu işin biraz daha kolay tarafı geliyor bana hep, bundan sonrası daha çok oyun içi olaylar olarak devam edecek.
Yazının ikinci bölümünde görüşmek üzere…