Month: February 2021

Authoritative Server (Turkish)

Merhabalar. Multiplayer oyun sistemleri konusunda türkçe kaynak kıtlığı çektiğimiz bir gerçek. Kaynak kıtlığını az da olsa azaltmak için bu yazıyı yazıyorum. Şunu baştan belirtmeliyim ki bu yazı Multiplayer dünyasına yeni giriş yapanlara yönelik değildir. Client-Server, Master Server, Latency/Lag terimlerinin teknik anlamlarını biliyorsanız yazıyı okumaya devam edebilirsiniz.

Genel olarak Client-Server mimarisinden, Pozisyon interpolasyonundan, Lag ile savaşmaktan, Client tabanlı olay tahmini ve yapılan tahminlerin server tarafından teyitlendirilmesiden bahsedeğiz. Ve ayrıca yazı sadece unity ya da başka bir oyun motoru için değil, genel yazılmıştır. İngilizce bir kaynağı türkçe’ye çevirip, gereksiz bazı kısımları kesip kendi yorumlarımı da katarak bu yazıyı yazıyorum.

Faydalandığım kaynak: http://www.gabrielgambetta.com/client-server-game-architecture.html


Client-Server Mimarisi

Her harikülade de oyun geliştirmek bizi zorluyor. Çok oyunculu(multiplayer) oyunları ele alırsak tamamen yeni bir takım problemler ortaya çıkmaktadır. İlginçtir ki temel problemler insan doğası ve fiziksel konumdur!

Hile Problemi

Her şey hile ile başlar.

Bir oyun geliştiricisi olarak, genellikle bir oyuncunun Single Player bir oyunda hile yapıp yapmadığını pek önemsemezsiniz. Çünkü oyuncunun yaptığı eylemler diğer oyuncuları etkilemez.

Çok oyunculu Multiplayer bir oyunda; hile yapan oyuncu sadece kendi deneyimlerini daha iyi hale getirmez, aynı zamanda diğer oyuncuların deneyimini kötüleştirir. Bir oyun geliştiricisi olarak muhtemelen bunu engellemek istersiniz çünkü bu diğer oyuncularınızın oyununuzdan uzaklaşmalarına neden olacaktır.

Hileleri önlemek için yapabilecek pek çok şey vardır. Burda en önemli olan şey: Oyuncuya güvenmeyin. Her zaman en kötü durumu düşünün – oyuncularınız hile yapmaya çalışacaktır!

Yetkili Servarlar ve *Sersem Clientlar

Görünüşte basit bir çözüm: Oyununuzdaki her şeyi kontrolünüz altında sunucuda gerçekleştirin ve clientları sadece ayrıcalıklı seyirciler yapın.

Clientlar sunucuya girdiler(tuş komutları, joystick değerleri vs) gönderir ve sunucu clientlardan aldığı bu girdilere göre clienları yönlendirip sonuçları clientlara geri gönderir. Bu sisteme genel olarak authoritative server(Yetkili Sunucu) denir.

Sunucunuzda güvenlik açığı olabilir tabi; bu probleme yazıda değilmemektedir. Yetkili bir sunucu kullanmak geniş bir yelpazede hileleri önler. Örneğin client’ınızın sağlık değerine(can değerine) güvenmiyorsunuz; client hile yapıp local’da(client ın cihazı) tutulan can değerini düzenleyebilir ve %10000 gibi uçuk bir değer yapabilir. Ancak sunucu o client ın o an sağlığının %10 olduğunu bilir. Olaylar sunucuda gerçekleşeceği için o oyuncu o an başka bir oyuncu tarafından saldırıya uğrarsa %10 olan can’ı %0’a düşecek ve oyuncunun local deki sağlık değerlerine bakmaksızın oyuncu ölecektir.

Ayrıca oyuncunun oyundaki pozisyonun koordinatılarına güvenmeyebilirsiniz; oyuncu sunucuya önce “Ben (10,10) konumundayım” diyecek, yarım saniye sonra “Ben (20,10) konumundayım” diyecek bu sefer; muhtemelen oyuncunuz bir duvardan geçecek ya da diğer oyunculardan çok daha hızlı hareket edecektir çünkü oyuncu hile yaptı! Bunun için şöyle bir çözüm yoluna başvurabilirsiniz; Sunucu zaten başlangıçta oyuncunun (10,10) pozisyonunda olduğunu biliyor. Client hareket etmek istediği zaman sunucuya yeni konumunu göndermek yerine “Ben bir birim sağ’a taşınmak istiyorum” komutunu  gönderecek, sunucu gelen komutu işleyecek ve oyuncuyu bir birim sağ’a taşıyacak, ardından güncellenen yeni pozisyonunu  “Yeni konumun (11,10)” mesajıyla client’a gönderecek. Mesajı alan client kendi konumunu mesajdaki konum ile güncelleyecek. Artık client pozisyon değiştirme konusunda hile yapamayacak!

Basit bir Client-Server etkileşimi.

Kısaca oyunun durumu sadece server tarafından yönetiliyor. Clientlar eylemlerini sunucuya gönderir, sunucu periyodik olarak oyun durumunu günceller ve sonra yeni oyun durumunu  tekrar client’lara gönderir. 

Ağlar ile Uğraşmak

Clientlar sıra tabanlı oyunlarda(satranç, puzzle, kart oyunlar vs) problemsizdir. Ayrıca LAN ortamında iletişim pratik ve hızlı olduğunu için burada da problemsizdir. Ancak internet gibi bir ağ üzerinden hızlı tempolu bir  oyun için kullanıldığında client lar problemler çıkartmaktadır.

Fiziksel konum hakkında biraz konuşalım. San Francisco da olduğunuzu farzedelim, New York’taki bir sunucuya bağlısınız. Sunucu bizden yaklaşık 4000 km uzaktadır. Hiçbir şey ışıktan daha hızlı hareket edemez, internetteki baytlar bile. Işığın yaklaşık 300.000 km/s hızda seyrettiğini hesaba katarsak 4 ms’lik(milli saniye) yolculuk yapmak 13 ms sürüyor.

Gerçek hayatta veri, yönlendiriciden yönlendiriciye bir dizi atlayışından(Ağ terminolojisinde atlamalar) geçer. Bunların çoğu ışık hızında yapılamaz. Yönlendiriciler, paketlerin denetlenmesi ve yeniden yönlendirimesi gerektiğinden biraz gecikmeye neden olur.

Bu bağlamda, verilerin client’tan server’a ulaşmasını 50 ms olarak var sayalım. Bu en iyi durum senaryosuna yakındır. Eğer New York’taysanız ve Tokyo daki bir sunucuya bağlıysanız ne olur? Ağ tıkanıklığı olursa ne olur? 100,200, hatta 500 ms’lik gecikmeler meydana gelebilir.

Örneğe geri dönelim. Client, server a “Ben sağ yön(ok) tuşuna bastım” girdisini gönderdi. Sunucuya bu girdi 50 ms sonra ulaştı. Sunucu aldığı bu isteği işleme koydu ve güncellenen durumu client’a geri gönderdi. Client 50 ms sonra “Yeni konumun (11,10)” mesajını alıyor.

Client bakış açısından; sağ yön(ok) tuşuna basıyor ama bastıktan 50 ms sonraya kadar hiç bir değişiklik olmadığını fark ediyor client. Tam 50 ms sonunda karakteri bir birim sağ a taşınacaktır. Girdileriniz ve sonuçlarınız arasında yaşanan bu gecikme çok fazla gelmeyebilir size ama dikkat çekicidir. Eğer 50 ms değil de 500 ms yani yarım saniyelik bir gecikme olursa sadece fark edilmez ayrıca oyunu oynanılmaz hale getirir. Hadi bunun için çözüm üretelim!

Client Tabanlı Tahmin ve Server Uzlaştırma

İnternet gibi bir ağ ortamında gecikmelerin saniyenin onda biri olduğu bir oyunda (bile),  gecikmeler hissedilebilir. Bu problem oyunu oynanmaz hale getirebilir.

Client Tabanlı Tahmin

Bazen hileci oyuncularınız olsa da neticede sunucunuz çoğu zaman geçerli istekleri işler. Bu, sunucunun aldığı girdileri onaylayıp oyunun durumunu beklendiği gibi güncelleneceği anlamına gelir. Yani eğer karakteriniz (10,10) pozisyonundaysa ve sağ ok tuşuna basılırsa yeni pozisyon (11,10) olacaktır.

Eğer oyun dünyası yeterince deterministikse, bunu avantaj olarak kullanabiliriz.(Deterministikten kasıt; oyunun durumu ve gönderilen girdilerin tahmin edilebilmesi).

100 ms Gecikme olduğunu ve bir pozisyondan diğer bir pozisyona hareket eden karakterin animasyonunun 100 ms sürdüğünü varsayalım. Bunu çok basit bir oyun motorunda(ya da herhangi bir geliştirme ortamında) kullanırsak(sistemi) tüm eylem 200 ms sürer:

Ağ gecikmesi + animasyon

Oyun dünyamız deterministik olduğundan, sunucuya gönderdiğimiz girdilerin başarılı bir şekilde gerçekleşeceğini varsayabiliriz. Bu bağlamda; client, girdilerini işledikten sonra(gönderdikten sonra) oyunun durumunu tahmin edebilir(Burada oyunun durumundan kasıt, local client ın davranışlarıdır). Ve bu tahminler çoğu zaman doğru olacaktır.

Girişleri göndermek ve yeni oyun durumunu serverdan beklemek yerine; girişleri gönderebilir ve aynı zamanda bu girdilerin sonucunu başarıya ulaşmış gibi gösterebiliriz serverdan “doğru” sonuçları beklerken(başarıya ulaşmış gibi göstermekten kasıt; sunucuya gönderdiğimiz girdiyi oluduğu gibi local de karaktere uygulamaktır. Ve bu yapılırken serverdan bir sonuç beklenmiyor. Sonuç serverdan geldiği zaman, gelen girdi tekrar karaktere uygulanacak). 

Server dan bir sonuç beklerken animasyon oynatılır

Artık oyuncunun eylemleri(girdileri) ile ekrandaki sonuçlar arasında hiç bir gecikme yok ve üstelik sunucumuz hala yetkili! Hile yapan bir oyuncu hükümsüz girdiler uygulayıp karakterinin pozisyonunu değiştirebilir, kendi ekranında(cihazında) istediğini o an oluşturabilir ama sunucu bu durumdan etkilenmez(Yukarıda yazıldığı gibi, biz sunucuya koordinat göndermiyoruz, “sağ ok(yön) tuşuna bastım” gibi mesajlar gönderiyoruz). Tabi diğer oyuncular da, bu hile yapan oyuncunun koordinatlarını serverdan aldığı için, hile bu oyunculara da işlemeyecektir. Bu arada Hile yapan oyuncu çok geçmeden serverdan sonuç alacak ve olması gereken pozisyona gerı ışınlanacak 🙂

Senkronizasyon Sorunları

Yukarıdaki örnekte, her şeyin iyi çalışmasını sağlamak için sayıları dikkatlice seçtim. Biraz değiştirilmiş bir senaryo düşünün: 250 ms gecikmemizin var olduğunu ve bir pozisyondan diğer pozisyona geçmenin 100 ms sürdüğünü var sayalım. Ayrıca oyuncunun üst üste 2 kez sağ tuşuna bastığını ve karakterini iki birim sağa hareket ettirdiğini varsayalım.

Şimdiye kadarki tekniklerimizi uygularsak:

Tahmin edilen durum ve yetkili(server) durum uyuşmazlığı

t = 250 ms Anında ilginç bir problem ile karşılaşıyoruz. Client ta tahmin edilen durum x = 12 dir ama serverdan gelen yanıt, client ın x = 11 pozisyonunda olduğunu söylemesi. Sunucu yetkili olduğundan dolayı, client ı x = 11 e taşıması gerekir. Ama çok kısa bir süre sonra t = 350 ms anında server, client ın  x = 12 pozisyonunda olduğunu söyler. Bu sefer karakter bir birim ileri atlayacak(bir öncekinde bir birim geriye atmıştı yanıt geç geldiğinden dolayı, bir diğer yanıtta t=350ms anında geldiği için karakteri bir birim ileri taşıyor )

Oyuncu bakış açısından: sağ ok tuşuna iki defa bastı; karakter iki birim sağa kaydı(buraya kadar henüz serverdan yanıt gelmemiş). Orda 50 ms durdu ve bir kare geriye(sola) atlayacal, oyuncu 100 ms daha hiç bir şey yapmadan bekliyor, hemen sonra bir birim sağa kayacak. Bu elbetteki kabul edilemez! 

Server’ı Client ile Uzlaştırma

Bu sorunu çözmenin anahtarı; client’ın, oyun dünyasını şimdiki zamanda görmesini sağlamaktır. Ama gecikme(lag) nedeniyle, sunucudan aldığı sonuçlar aslında geçiş oyunun durumudur(sonuçlarıdır). Sunucu güncellenen oyun durumunu gönderdiği zaman, client tarafından gönderilen tüm komutları işlememiştir(gecikmeden dolayı).

Yinede bunu çözmek çok zor değil. İlk olarak; client, gönderdiği her isteğe/mesaja bir sıra numarası ekler. Örneğimizde; ilk tuşa basma isteğinin sıra numarası #1 ve ikinci tuşa basma isteğinin sıra numarası ise #2 dir. Ardından sunucu yanıt verdiğinde; her verdiği yanıta, client’ın gönderdiği sıra numarasını ekler:

Client tabanlı tahmin + Server uzlaştırma

t = 250 anında client “#1 numaralı isteğine göre pozisyonun x = 11” mesajını alır sunucudan. Sunucu yetkili olduğundan karakterin pozisyonunu x = 11 e ayarlar. Şimdi; client’ın sunucuya gönderdiği isteklerin bir kopyasını saklı tuttuğunu varsayalım. Yeni oyun durumunda; sunucunun zaten #1 numaralı isteği işleme koyduğunu biliyor, bu neden bu kopyayı silebilir. Aynı zamanda sunucuya gönderdiği #2 numaralı isteğin sunucu tarafından işleyip geri dönüş yapılması gerektiğini de biliyor. Client tabanlı tahmini bu durumda da uygulayarak, sunucu tarafından gönderilen son isteği(mesajı) işleyerek oyunun mevcut/gerçek durumunu ve sunucunun henüz işleme koymadığı girdileri hesaplayabilir.

Yani; t = 250 anında client “x = 11, son işlenen istek = #1” mesajını alır. #1′e kadar olan girişlerin kopyasını siler. Ancak sunucu tarafından henüz onaylanmayan #2 numaralı kopyayı saklar. Sunucunun gönderdiği veri ile oyunun durumunu günceller ve daha sunucu tarafından görülmeyen tüm girdileri uygular. Bu durumda; #2 girdisi “sağ a bir birim kay” olduğu için client x= 12 ye hareket edecek.

Örneğe devam edersek;  t = 350 anında sunucudan yeni bir oyun durumu gelir. bu seferki mesaj: “x = 12, son işlenen istek = #2”. Bu noktada client, #2’ye kadar olan girişlerin kopyalarını siler ve oyun durumu x = 12 ye göre güncellenir. Yeniden oynatılacak işlenmemiş giriş yoktur bu nedenle işlem burada doğru sonuçla biter.

Ekleme: Peki bu durumda oyuncu, pozisyonunu hile ile değiştirirse buna nasıl engel olacağız? Cevap basit aslında; bütün hareket eylemlerimiz numaralandırılıp servera gönderiliyor. Bu numaraların kopyaları bizde duruyor ta ki sunucudan aynı numaranın teyidini aldığımız zaman. Mesela atıyorum #5 numaralı bir sonuç geldi bize server tarafından, bu #5 numara ve altı numaraların kopyalarını hafızadan silmeden önce, bizdeki kopyalardaki #5 numaralı istek ile serverdan gelen #5 numaralı isteği karşılaştıralım. Numaraları kayıt ederken yanlarında bide o anki karakter pozisyonunu kayıt ettirelim(bu pozisyon verisi local de tutulacak, servera gönderilmeyecek). Karşılaştırmayı yaparken; kopyalarımızdaki  #5 numaralı pozisyon ile serverdan gelen  #5 numaralı istekteki pozisyonun farkını alın. Sonucun ya 0 ya da çok düşük bir bir değere eşit olması gerekiyor. Eğer fark fazlaysa bilin ki oyuncunuz hile yapmıştır. karakterin pozisyonunu serverdan gelen isteğin pozisyonuna eşitleyip onu geri ışınlayabilirsiniz doğru konuma.

Varlık İnterpolasyonu (Ara Değer Tespiti)

Server Zaman Adımı

Şimdiye kadar bahsettiğimiz sunucunun davranışı oldukça basitti – Sunucu client girdilerini okudu, oyun durumunu güncelledi ve client’a geri gönderdi. Birden fazla oyuncu bağlandığında, ana sunucu döngüsü biraz daha farklıdır.

Bu senaryoda, birkaç oyuncu aynı anda ve hızlı bir şekilde veri gönderiyor olabilir (oyuncunun ok tuşlarına basarak, fareyi hareket ettirerek komut verebildiği kadar hızlı). Oyun dünyasını, her bir oyuncudan veri geldiğinde güncellemek ve daha sonra oyunun durumunu yayınlamak çok fazla CPU ve bant genişliği tüketecektir.

Oyuncu verilerini, geldiklerinde işlemeden sıraya koymak daha iyi bir yöntemdir. Oyun dünyası düşük sıklıkta düzenli olarak, örneğin saniyede 10 defa, güncellenir. Her güncelleme arasındaki gecikme(bu örnekte 100ms) zaman adımı olarak adlandırılır. Her güncelleme döngüsü yinelemesinde, tüm işlenmemiş client verileri uygulanır (oyun fiziğini daha tahmin edilebilir hale getirmek için muhtemelen zaman adımından daha küçük zaman aralıklarında) ve yeni oyun durumu oyunculara gönderilir.

Özetle; oyun dünyası, oyuncu verisi sayısı ve varlığından bağımsız olarak tahmin edilebilir bir oranda güncellenir.

Düşük Frekanslı Güncellemelerin Üstesinden Gelmek

Oyuncunun (client ın) bakış açısından; bu yöntem önceki kadar pürüzsüz çalışır – client tabanlı tahmini, güncelleme gecikmesinden bağımsız olarak çalışır böylece (göreceli olarak daha seyrek olsa da) tahmin edilebilir durum güncellemeleri altında çalışır. Bununla birlikte, oyun durumu düşük bir frekansta yayınlandığından (aynı örnekle devam edersek her 100 ms’de bir), oyuncu, oyun dünyasında hareket eden diğer varlıklar hakkında çok seyrek(aralıklılar halinde) bilgiye sahiptir.

İlk uygulama, bir durum güncellemesi aldığında/geldiğinde diğer karakterlerin konumunu günceller; bu durum çok düzensiz harekete, yani, her 100 ms’de bir yumuşak hareketler yerine farklı sıçramalara yol açar.

Client 2 tarafından Client 1 in görünümü

Geliştirmekte olduğunuz oyunun türüne bağlı olarak, bu durumla başa çıkmanın birçok yolu vardır; genel olarak, oyun varlıklarınız ne kadar öngörülebilirse(tahmin edilebilirse), her şeyin yolunda gitmesi o kadar kolay olur.

Konum Hesaplama (Başlangıca Göre)

Bir araba yarışı oyunu yaptığınızı varsayalım. Gerçekten hızlı giden bir otomobil oldukça öngörülebilirdir – örneğin, saniyede 100 metre gidiyorsa, bir saniye sonra, başladığı yerden tahminen 100 metre ileride olacaktır.

Neden “tahminen”? Bu bir saniye zarfında otomobil biraz hızlandırılmış veya yavaşlatılmış veya biraz sağa veya biraz sola döndürülmüş olabilir – buradaki anahtar kelime “biraz”dır. Bir aracın manevra kabiliyeti, oyuncunun gerçekte ne yaptığından bağımsız olarak, yüksek hızlarda, zamanın herhangi bir noktasında pozisyonunun, önceki konumuna, hızına ve yönüne büyük ölçüde bağımlıdır. Diğer bir deyişle, bir yarış arabası birden bire 180º dönüş yapamaz.

Bu durum, her 100 ms’de bir güncelleme gönderen bir sunucu ile nasıl işler? Client, her bir rakip arabanın yetklili(gerçek) hız ve yön bilgisini alır; sonraki 100 ms boyunca yeni bir bilgi almayacak, ancak yine de arabaların yarışa devam ettiğini göstermesi gerekir. Yapılacak en basit şey, aracın yönünün ve hızının 100 ms boyunca sabit kalacağını varsayıp arabanın konumunu bu parametrelerle yerel olarak hesaplamaktır. 100 ms sonra, sunucu güncellemesi geldiğinde, aracın konumu düzeltilir.

Düzeltme, birçok faktöre bağlı olarak büyük veya nispeten küçük olabilir. Oyuncu arabayı düz bir çizgide tutuyorsa ve arabanın hızını değiştirmiyorsa, tahmini konum tam olarak düzeltilmiş konum ile aynı olacaktır. Öte yandan, eğer oyuncu bir şeye çarparsa, tahmin edilen pozisyon oldukça hatalı olacaktır.

Başlangıca göre konum hesaplamanın düşük hız koşullarında uygulanabileceğini unutmayın, örneğin savaş gemileri için.  Aslında, “başlangıca göre konum hesaplama” terimi, eskiden denizde konum tayin etmek için kullanılırdı.

Varlık İnterpolasyonu

Başlangıca göre konum hesaplamanın hiç bir şekilde uygulanamayacağı bazı durumlar vardır – özellikle oyuncunun yönünün ve hızının birdenbire değişebileceği tüm senaryolarda. Örneğin, bir 3D silahla ateş etme oyununda, oyuncular genellikle koşar, durur ve köşeleri çok yüksek hızlarda dönerler, bu da başlangıca göre konum hesaplamayı esasen işe yaramaz hale kılar, çünkü pozisyonlar ve hızlar artık önceki verilere dayanarak tahmin edilemez.

Sunucu, yetkili(gerçek) verileri gönderdiğinde oyuncu konumlarını öylece güncelleyemezsiniz; her 100 ms’de kısa mesafelere ışınlanan oyuncular ortaya çıkar ve oyun oynanamaz hale gelir.

Elinizde olan her 100 ms’de bir gelen yetkili pozisyon verisidir; esas iş oyuncuya bu arada neler olduğunu göstermektir. Çözümün anahtarı, diğer oyuncuları, kullanıcının oyuncusuna göre geçmişte göstermektir.

t = 1000 anında konum verilerini aldığınızı varsayalım. t = 900 anında zaten veri almıştınız, bu yüzden oyuncunun t = 900 ve t = 1000 anlarında nerede olduğunu biliyorsunuz. Yani, t = 1000 ve t = 1100 arasında, diğer oyuncunun t = 900 ve t = 1000 arasında yaptıklarını göstereceksiniz. Bu şekilde daima oyuncunun gerçek hareket verisini (sadece 100 ms geç olarak) gösterebilirsiniz.

Client 2 nin, Client 1’i bilinen son geçmiş pozisyonuna interpolasyon ile taşıması

t = 900‘den t = 1000‘e interpolasyon yapmak için kullandığınız konum verileri oyuna göre değişir. İnterpolasyon genellikle gayet iyi çalışır. Şayet çalışmazsa sunucunun her güncellemede daha detaylı hareket verisi göndermesini sağlayabilirsiniz; örneğin client tarafından takip edilen bir düz segmentler(doğrusal bir çizgi gibi düşünebilirsiniz) dizisi veya her 10 ms’lik konum verilerinin interpolasyon yapıldığında daha iyi görünür (10 kat fazla veri göndermenize gerek yok – zaten küçük hareketler için deltaları(veri aralıkları) gönderdiğinizden dolayı, iletim formatı, bu özel durum için büyük ölçüde optimize edilebilir).

Bu tekniği kullanarak, her oyuncunun, oyun dünyasının biraz farklı bir versiyonunu gördüğünü unutmayın. Çünkü her oyuncu kendinin şu anki (şimdiki) halini ama diğer varlıkların ise geçmişteki halini görür. Hızlı tempolu bir oyunda bile, diğer varlıkları 100 ms’lik bir gecikme ile görmek genellikle farkedilemez.

Bu durumun istisnaları da mevcuttur; çok fazla mekansal ve zamansal kesinliğe/doğruluğa ihtiyacınız olduğunda, mesela oyuncu başka bir oyuncuya ateş ettiğinde, diğer oyuncular geçmişte görüldüğünden, 100 ms’lik bir gecikme ile nişan alırsınız. Yani hedefinizin 100 ms önce bulunduğu yere ateş ediyorsunuz!

Ekleme: Interpolasyon; basitçe iki bilenen değer arasında bir bilinmeyen değeri bulmaktır. Konu üzerinden gidersek; diyelim ki A client’ıyız düşmanımız ise B client’ı, B yi şu anda (10,10) pozisyonunda gördüğümüzü farz edelim. Sunucudan gelen yeni bir mesaj, B client ının (20,40) pozisyonunda olduğunu söylüyor(Oyun atmosferinin hızlı olduğunu ve client’ın hile yapmadığını düşünelim). B nin x pozisyonunu 10 dan 20 ye ve y pozisyonunu 10 dan 40 a ışınlamak göze çok batar, onun yerine smooth(yumuşak) bir şekilde adımlarla x’ini 10 dan 20 ye ve y’sini 10’dan 40’a yükseltmek çok daha makul. Bunu ise interpolasyon ile yapabiliriz. (10,10) pozisyonundan (20,40) pozisyonuna client’ı toplamda 10 adımda hedefine ulaştırabiliriz(Tabi adımlar arası çok az da olsa bir bekleme süresi olmalı ki oyuncu ışınlamayı fark etmesin). 

İnterpolasyonun genel formülü:

Bu formüldeki x ve y aslında tam olarak bize gerekli olan x ve y değildir! İnterpolasyon hakkında daha fazla bilgi için şuraya bakabilirsiniz.

Bu formülden yola çıkarak adımlarımızı hesaplarsak şöyle sonuçlar elde ederiz:

Şuanki pozisyon => (10,10)

1.Adım => (11,13)

2.Adım => (12,16)

3.Adım => (13,19)

4.Adım => (14,22)

5.Adım => (15,25)

6.Adım => (16,28)

7.Adım => (17,31)

8.Adım => (18,34)

9.Adım => (19,37)

10.Adım => (20,40)

Artık B clientı daha stabil ve smooth(yumuşak) hareket ediyor! Adım sayısını daha da artırarak yumuşaklığı artırabilirsiniz. Unity oyun motorunun kütüphanesine dahil edilen Lerp komutu tam da bu işlemi yani interpolasyonu yapmaktadır.

Lag İle Savaşma

Keskin nişancı tüfeğinizle hedefin başına nişan alıyorsunuz…ateş ettiniz tam onikiden…Çok güzel bir atıştı kaçırmadınız!

Ama bir dk, düşman hala hareket halinde, ne yazık ki kaçırmışsınız…

Ama bu nasıl olur? Onun başından vurdunuğunuza eminsiniz?

Bütün eylemler server da gerçekleşiyor dedik, siz ateş ederken server a şu mesajı gönderdiniz: “mevcut pozisyonumdan ve rotasyonumdan ateş etmek istiyorum”. Server bu mesajı aldığında hemen gerekli hazırlıkları yapıp mevcut pozisyonunuzdan sizin yerinize ateş ediyor. Ateş ederken merminin düşmana değmediğini, boşa gittiğini görüyor. Yani aslında bir duvara sıktınız! Sizin ya da düşman client ın ağ gecikmesinden dolayı, onu yanlış yerde gördünüz.

Bu bir bakıma oyunu ışık hızının çok yavaş olduğu bir dünayada oynamak gibidir. Düşmanın geçmiş pozisyonunu hedefliyorsunuz, ateş ettiğiniz zaman düşman nişan aldığınız bölgeden çoktan gitmiştir aslında.

Neyse ki çoğu oyuncu için çoğu zaman hoş olan basit bir çözüm var:

Atış yaptığınız zaman, client bu olayı sunucuya iletir atışınızın tam zamanın ve silahın tam amacı ile beraber. İşte önemli adım; Oyuncu tüm atış bilgilerini zaman bilgisi ile aldığından dolayı, geçmişte herhangi bir anda dünyayı otoriter(yetkili) olarak yeniden yapılandırabilir. Özellikle dünyayı her hangi bir zamanda, her hangi bir müşteriye olduğu gibi tam olarak yeniden düzenleyebilir.

Diyelimki siz local de t = 5000 anında bir düşmana ateş ettiniz. Server a t = 5000 anında ateş ettiğinizi söylüyorsunuz. Server bu bilgiyi aldığında, sizin 250 ms gecikmeniz olduğunu görür. Server o an sizin için bütün oyun dünyasını 250 ms geriye götürebilir, siz hariç diğer bütün client ların pozisyonları 250 ms önceki pozisyonlarına dönerse ve server o an sizin yerinize ateş ederse, düşmanı gerçekten vurup vurmadığınızı öğrenebilir. Bu kontrolü yaptıktan hemen sonra server, zamanı tekrar eski haline yani 250 ms sonrasına geri döndürmelidir. Aksi durumda bütün oyunun akışı bozulur! 

Ve herkes mutlu! Çünkü artık clientların gecikmelerinden ötürü bir problem yaşama ihtimali çok düşük.

Unity – Update vs LateUpdate vs FixedUpdate (Turkish)

Merhabalar. Unity MonoBehavior deki “Update”, “LateUpdate” ve “FixedUpdate” fonksiyonları arasındaki farkı kısaca anlatacağım. Pek türkçe kaynak olmadığı için bu konuda yetersiz bilgiye sahip olanlara çok şey kazandıracağını düşünüyorum bu anlatacaklarımın.

1-Update

Bu fonksiyon, saniyede gösterilen frame(kare) sayısına yani FPS ye bağlıdır. FPS nin 60 olduğunu varsayarsak, Update fonksiyonu saniyede 60 defa çalışacaktır.

2-LateUpdate

Bu fonksiyonun Update fonksiyonundan tek farkı; Update ten sonra çalışmasıdır. Yine FPS nin 60 olduğunu varsayalım, Update fonkisyonu birkez çalıştıktan hemen sonra LateUpdate çağırılacak. Oda bir kez çalıştıktan sonra tekrar Update e geçecek. Böyle sırayla çalışırlar. Bu arada Update ile LateUpdate arası geçişlerde frame farkı yoktur. Yani diyelim ki oyun yeni açıldı ve daha 5.frame deyiz, Update 5.kez çalıştıktan sonra tekrar LateUpdate ye geçtiğinde bu fonksiyonda da hala 5.frame de olacağız.

3-FixedUpdate

Bu fonksiyon FPS den bağımsız çalışır. Default çalışma aralığı 0.02 saniye de birdir(Yani saniyede 50 defa çalışır). Bu default değeri, Edit/ProjectSettings/Time/FixedTimestep ten değiştirebilirsiniz. İster FPS 1 olsun ister 100 olsun, bu fonksiyon her durumda 0.02(Default değere göre konuşuyorum) saniyede bir çalışır.

Performans açısından bu fonksiyonları kullanmaktan olabildiğince kaçınmanızı tavsiye ediyorum. Gerek olmadıkça kullanmamaya çalışın. Oyununuzda score u ekrana yazdırmak gerektiğinizi varsayalım, eğer bu score zamana bağlı artan ya da azalan bir score değilse bunu ekrana yazdırmak için bu yukarıda açıkladığım loop lu fonksiyonları kullanmak yerine sadece score değiştiğinde ekrana bir seferliğine yazdıran bir fonksiyonu çalıştırmak çok daha mantıklı ve performanslıdır.

Unity – DeltaTime (Turkish)

Merhabalar. Bu yazıda, kodlarda çok sık kullandığımız deltaTime ı ayrıntılı bir şekilde anlatmaya çalışacağım.

Delta Time Nedir?

Adından da anlaşılacağı üzere iki durum arasındaki zaman değişikliğidir. Gelelim bunun Unitydeki yerine.
Unitynin kendi dokümanlarına baktığımızda deltaTime için “time passed since last frame” yani “son kareden bu yana geçen zaman” açıklamasını görürüz. Bu bir nevi iki kare arasındaki zaman farkı anlamına gelmektedir. Bu demek oluyorki deltaTime değiştirilemez sadece okunabilir. Şöyle bir örnekle açıklayayım.
Play buttonuna bastık oyunu başlattık, çok az bir zaman geçiyor ve şuan 36.karenin görüntülendiğini görüyoruz. 37.Kareye tam geçtiğimizde deltaTime güncelleniyor, yeni değeri basit “(şimdiki zaman)-(36.kare ilk görüntülendiği zaman)” formülü hesaplanıp çıkan sonuç ile değişiyor.
Yani kısaca deltaTime iki kare arasındaki zaman farkı diyebiliriz. Eğer 1 i FPS ye bölersek Time.deltaTime ı buluruz. Örnek verecek olursak; FPS 40 ise bu durumda deltaTime 1/40=0.025 olur
(*)”deltaTime” ile “fixedDeltaTime” ı birbiri ile karıştırmamak gerekiyor. fixedDeltaTime adından da anlaşılacağı üzere değeri sabittir, FPS ye bağlı değişmez. Edit/ProjectSettings/Time daki “FixedTimestep” değerini alır her zaman. deltaTime ise iki kare arasında geçen zamana eşit olduğu için tamamen FPS ye bağlıdır ve değişkendir.

Ne Amaçla Kullanılır?

Bu fonksiyonun tek görevi; işlemlerinizi, FPS den yani gösterilen/gösterilecek kare sayısından bağımsız yapmanız içindir. Animasyon tabansız bir karakter kontrol scriptinde hız değişkenlerinizi Time.deltaTime ile çarpmazsanız; FPS düşük olunca karakter yavaş hareket edecek, yüksek olursa da karakter hızlı hareket edecektir. Bu özellikle Multiplayer bir oyunda adaletsizliklerin doğmasına neden olacaktır.
Sadece hız değişkenleri ile mi çarpılıyor bu deltaTime? Tabiki hayır, Zaman ile ilgili bir çok konuda kullanabilirsiniz. Örneğin kare başı çalışan bir tekrarlı fonksiyonda(Update gibi) bir float değere sürekli Time.deltaTime eklerseniz bu size saniye cinsinden artan bir sayaç verecektir.

Peki Ya Nasıl?

deltaTime’ın ne amaçla kullanıldığını bir çoğunuz biliyordunuz aslında, peki çalışma mantığını biliyor musunuz? FPS leri farklı iki cihazda karakterlerin yürüme hızlarını nasıl eşitliyor? Bunu en iyi şekilde aşağıdaki örneklerle açıklayabilirim.

Öncelikle Time.deltaTime kullanmadan bir objeyi çok basit bir şekilde hareket ettirelim.


 void Update()
 {
	  transform.Translate(10,0,0);
 }
 

Bu kodda, “scriptin bulunduğu objeyi +x yönünde her kare de 10 birim kaydır” dedik. Projeyi, biri 20 FPS de diğeri de 50 FPS de çalıştırıcak iki farklı cihaza derlediğimizi düşünelim.

Projeyi;
20 FPS de çalıştıran cihazda obje, kare başı 10 birim kayarsa saniyede 20*10=200 birim,
50 FPS de çalıştıran cihazda obje, kare başı 10 birim kayarsa saniyede 50*10=500 birim kayacak.

Gördüğünüz gibi ortada bir adaletizlik var. Düşük özellikli cihazda obje saniyede 200 birim kayacakken yüksek özellikli cihazda ise saniyede 500 birim kayacak. Tam 2.5 kat fark var!

Şimdi ise Time.deltaTime kullanarak hareket ettirelim objeyi.


 void Update()
 {
	  transform.Translate(10 * Time.deltaTime,0,0);
 }
 

Yine aynı şekilde projeyi, biri 20 FPS de diğeri de 50 FPS de çalıştırıcak iki farklı cihaza derlediğimizi düşünelim.

20 FPS veren cihazda:
Time.deltaTime = 1/20 = 0.05
Kodda deltaTime ı 10 ile çarptığımız için kare başı 10*(0.05) = 0.5 birim kayacak.
FPS 20 ise bu kod saniyede 20 defa çalışacak demektir. Yani 1 saniyede toplam (0.5)*20 = 10 birim kayacak bu obje.

50 FPS veren cihazda:
Time.deltaTime = 1/50 = 0.02
Kodda deltaTime ı 10 ile çarptığımız için kare başı 10*(0.02) = 0.2 birim kayacak.
FPS 50 ise bu kod saniyede 50 defa çalışacak demektir. Yani 1 saniyede toplam (0.2)*50 = 10 birim kayacak bu obje.

Gördüğünüz gibi FPS ler farklı olmasına rağmen aynı sürede aynı yolu katletti iki cihazdaki objeler. Son olarak Time.deltaTime ı hesaplarken neden 1 i FPS ye böldüğümüzü açıklıyayım.
FPS nin anlamının saniyede gösterilen kare sayısı olduğunu biliyorsunuz. 1 Saniyede X tane kare gösteriliyorsa, iki kare arasındaki zaman farkı 1/X ile bulunur. Bu ise deltaTime a eşit oluyor.
Ben olayı örneklediğim için bunu açıklama gereksiniminde bulundum, siz deltaTime kullanırken tabiki 1/FPS işlemini uygulamayacaksınız. Bunun yerine direk Time.deltaTime ı kullanacaksınız. Okuduğunuz için teşekkürler.

Unity – UI Optimizasyonu (Turkish)

Merhabalar. Bu yazıda geliştirdiğimiz oyunların büyük bir parçası olan UI’ların optimizasyonuna değineceğiz. Yabancı bir kaynağı Türkçeye tercüme ettiğim bu yazıda sıkça dirty kelimesinden bahsediliyor. Kelimeyi olduğu gibi Türkçeye çevirince kirli anlamı çıkıyor. Kirli kelimesinin bu aşamada çok yetersiz ve anlamsız olduğu düşündüğüm için kelimeyi olduğu gibi, tercüme etmeyip yazıda kullandım. Ama dirty’nin yazılımdaki tanımına kısaca; sisteme/yapıya/veri tabanına/diske senkronize edilmemiş içeriği değiştirilmiş nesne diyebiliriz.

Faydalandığım kaynak: https://tappable.co.uk/talking-dirty-unity-ui-optimisation/
 

—————————

Oyununu geliştirdin ve her şey bitti. Ama doğru gitmeyen bir şeyler var. Düşük FPS, yüksek CPU kullanımı, aşırı pil tüketimi vs. gibi bir takım optimizasyon problemlerinin olduğunu fark ediyorsun. Ve ardından “Oyunu optimize etmem gerek!” diyorsun…

Bugün; Unity’de performans kaybına yol açan az bilinen bazı yolları ve bunların nasıl düzeltileceği hakkında yazacağım.

UI sistemine derinlemesine dalmadan önce şunu bilin; oyununuzu her optimize ettiğinizde yapmanız gereken ilk şey her zaman sorunu bildiğinizden emin olmanızdır. Unity’nin Profiler’ı bu konuda size yardımcı olmakta mükemmel bir araçtır. Profiler sayesinde; oyunun nerelerde yavaşladığını, hangi senaryolarının çalıştığını, oyun fiziğinin nerede nasıl hesaplandığını, render’lanan UI’lar  ve diğer her şey hakkında bilgi sahibi olabilirsiniz.

“Ama bir dakika…Demek istediğim oyunun ana menüsündeyim ve hiç bir şey yapmadan oturuyorum, ona rağmen CPU deliriyor!”

Pekâlâ dostum, korkarım oyununda bir problem olabilir…

Dirty hakkında konuşalım

Aslında önce batching hakkında biraz konuşalım, batching nedir ve neden bunla ilgilenmeliyiz?

Batching, Unity’de oluşturduğunuz Canvas’lara dayanarak görsel sonuçlar üretme sürecidir. Canvas içinde oluşturduğunuz Image, Text vb. tüm UI component’lerinin mesh’leri birleştirilir ve ekranda işlemek için unity’nin grafik hattına gönderilir(Şunu asla unutmayın; UI componentlerinin canvas içinde oluşturduğu görsel olan her şey aslında mesh’tir. Örneğin siz oyun esnasında bir InputField’ı yani bir yazı kutusunu düzenlemeye çalıştığınızda, eklediğiniz ya da sildiğiniz her karakterde eski mesh silinip orda yeni bir mesh oluşturuluyor o text için). Bu işlem akıllıcadır, herhangi bir canvas değişikliği gerçekleşinceye kadar sonuçları önbelleğe alır. Yani canvas’ta ana menü’yü render’ladıktan sonra siz canvas’ta bir değişiklik yapmayıp, batching yapmayana dek sonuç hep aynı kareyi(frame’i) kullanır. Eğer bunun nasıl çalıştığı konusunda bilgi sahibi değilseniz, her frame’de batching işemi yapıp gereksizce UI’ların tekrar tekrar render’lanmasına sebep olabilirsiniz.

Canvas’ta batching işlemi ne zaman meydana gelir? –Ne zaman Canvas dirty olarak işaretlenirse(belirlenirse) o zaman.

Canvas’ın dirty olarak işaretlenmesine ne sebep olur? –Herşey!

Kelimenin tam anlamıyla, bir canvas’ta veya canvas’ın child’larında yapacağınız herhangi bir değişiklik(bu değişiklik bir text düzenlenmesinden tutun, bir image değişimine ordan da tutun bir RectTransform değişikliğine her şeyi kapsar), hierarchy’de en üste yani ilgili canvas’a ulaşıp o canvas’ı dirty olarak işaretleyeceğini söylüyorum. Canvas içindeki; bir Text değerini, RectTransform pozisyonunu, renkleri, scale’i, rotation’ı değiştirmek veya bir game object’i aktif etmek ana canvas’ı (Ana dediği parent canvas) dirty olarak işaretler. Şuan haklı olarak “Aman Allahım!” diye düşünebilirsiniz. Karmaşık bir UI’ın bir parçası olarak dönen süslü bir döndürücünüz varsa(Loading dönücüsü gibi), bu döndürücü bütün canvas’ınızın her frame’de yeniden render’lanmasına sebep oluyor olabilir. PC’de bu belki pek problem yaratmayabilir ama mobil’de cihazlarda pil’i etkiler.

Muhtemel Çözümler

Neyse ki bunu düzletmenin veya en aza indirmenin yolları var.

Bir UI’da yaptığınız değişikliklerin, parent canvas’ı dirty olarak işaretlediğini unutmayın. Bunun çözümü iç içe canvas’lar kullanmaktır. Ama gidip sahneneizdeki her game object’e canvas eklemeye çalışmayın çünkü bunu yaparsanız batching işleminin sürecini uzatırsınız. Bunu kullanmanın en iyi yolu canvas’taki obejct’leri statik ve dinamik olarak sıralamaktır. Örneğin; kenarlıklardan, text’ten, dolgudan, arka plan görüntüsünden oluşan bir sağlık bar’ınız varsa, text’i ve dolgu image’ını(bar) diğer statik’lerden(kenarlıklar ve arka plan) ayırır ve buna bir canvas eklersiniz(oluşturursunuz). Bu şekilde dolgu image’ı(bar) ve sağlığı gösteren text ile her oynama yaptığınızda bütün UI’ın sadece küçük bir kısmını dirty olarak işaretleyecek ve tüm UI’ların bunun için yeniden render’lanmasını engellemiş olacaksınız.

CPU’dan tasarruf etmenin bir başka yolu da game object’i devre dışı bırakmak yerine Canvas componentini devre dışı bırakmaktır. Çünkü böyle yapmanız durumunda canvas ı tekrar aktif ettiğiniz zaman canvas yeniden oluşturulmaz(yani dirty olarak işaretlenmez), devre dışı olmadan önceki son bıraktığı sonuçları kullanır. Bununla birlikte; canvas altında çalışan çok sayıda MonoBehaviour’ünüz(script) varsa ve devre dışı olarak ayarlamazsanız, onları tek tek manuel olarak devre dışı bırakmanız gerekebilir. Bunu aklınızda bulundurun.

Sonuncusu ama bir o kadar önemlisi, milisaniyelerce performans sağlamak için

Envanter sistemi gibi, oyun esnasında kullanıcı tarafından sürekli doldurulan/karışılan karmaşık UI’lar üzerinde çalışıyorsanız, genellikle kendinizi yeni bir oyun nesnesi(game object) oluşturma ve o nesneyi yapılandırma sürecinde bulacaksınız(Burada yeni oyun nesnesinden kasıt, örneğin envanter tablosunda herhangi bir kutucuğa bir item’in gelmesi gibi düşünebilirsiniz, bu ve bu gibi örnekler). Bilmelisiniz ki aktif olmayan bir game object, parent canvas’ı dirty olarak işaretlemek için rahatsız etmeyecektir canvas’ı. Ama eğer aktifse; hierarchy ağacına mesajlar gönderir, her bir parent’ı canvas componentleri için kontrol eder ve her değişiklikle canvas’ı dirty olarak işaretler.

Yani kodunuzu bu şekilde yapılandırmayın;

MonoBehaviourInventoryClass uiElement = Instantiate(prefab,layout).GetComponent();
uiElement.gameObject.SetActive(true); //Obje aktifleşti ve canvas dirty olarak işaretlendi
uiElement.image.fillAmount = 0.2f; //UI elementinde bir değişiklik yapıldı, canvas tekrar dirty olarak işaretlendi
uiElement.header.text = “Sword”; //Tekrar başka bir UI elementinde bir değişiklik yapıldı ve canvas tekrar dirty olarak işaretlendi

Gördüğünüz gibi canvas’ı tam 3 defa dirty olarak işaretledik, yani 3 defa canvas yeniden render’landı. Ama kodunuzu bu şekilde yapılandırırsanız canvas’ı sadece bir defa dirty olarak işaretlemiş olursunuz;

MonoBehaviourInventoryClass uiElement = Instantiate(prefab,layout).GetComponent();
uiElement.image.fillAmount = 0.2f;
uiElement.header.text = “Sword”;
uiElement.gameObject.SetActive(true); //Obje daha yeni aktif olduğu için sadece burada canvas’ı dirty olarak işaretler

Umarım bu sizin için verimli bir okumaydı. Şimdi gidin ve şu kare hızlarını(fps) artırın!

*Bu aşırı tepki, dramatik amaçlar içindi

Unity – Mobil İçin Güvenlik (Turkish)

İnternet çağı boyunca güvenlik her zaman sorun olmuştur ve olmaya devam edecektir. Bu yazımda; mobil uygulamanızı, kötü niyetli insanların uygulamanıza yapmak istedikleri kötü şeylerden(*) korumanız için güvenlik tavsiyeleri vereceğim. Yalnız şunu baştan belirtmeliyim ki bu yazdıklarımın hepsini harfiyen uygulasanız bile uygulamanız %100 güvenli olmaz, sadece kötü niyetli insanların işlerini olabildiğince zorlaştırırsınız. Unutmayın; hiç bir sistem güvenli değildir 🙂

1-) IL2CPP İle Build Alın

Normalde Mono(C#) ile build aldığınız apk(android) ve ipa(ios) dosyalarını decompile ederseniz içlerinde Assembly-CSharp.dll gibi bir takım dll dosyası görürsünüz. Bu dll dosyalarını ILSpy gibi yazılımlarla decompile ederseniz, projenizde kullandığınız bütün c# kodlarını biraz düzensiz bir biçimde görürsünüz. Düzensiz ama okunamaz değil:) Ama IL2CPP ile build alırsanız, projenizdeki bütün C# scriptleri c++ a dönüştürülür ve öyle build alınır. C++ tan oluşturulmuş dll dosyalarının decompile edilmesi çok zordur, nedeni ise direk makine kodu içermesi.

2-) Kritik Verilerinizi Olabildiğince Değişkenlerde Tutmayın

Örneğin oyuncunun altın sayısını değişkende tutmak yerine, her lazım olduğunda nerde tutuyorsanız ordan çekin. Mesela PlayerPrefs kullanıyorsanız, altın artırırken direk PlayerPrefs ile altın değişkenini önce çekip sonra üstüne ekleme yapıp sonra da kayıt edin PlayerPrefs ile. Altını değişkende tutup ta, o değişkeni artırıp daha sonra PlayerPrefs ile kayıt etmeyin, çünkü değişkenler ram’de tutulur ve o değişkenlere üçüncü taraf yazılımlarla erişmek gerçekten çok kolay:)

3-) Verilerinizi Local’de Tutuyorsanız Mutlaka Şifreleyin

Normal tek yönlü şifrelemeden bahsetmiyorum, simetrik şifreleme yöntemlerinden birini kullanın. DES, Triple DES, RC2 ve Rijndael simetrik şifreleme yöntemleridir. Bunlar arasında önerdiğim Rijndael’dir çünkü mono’da en hızlı çalışan o. Rijndael ile bir veriyi, bir kullanıcı adı ve şifre kullanarak encypt(şifrelemek) edebilir ve şifrelenmiş veriyi sadece aynı kullanıcı adı ve şifreyle decrypt(şifre çözme) edebilirsiniz. Mesela PlayerPrefs kullanıyorsanız hem key’i hem de data’yı Rijndael ile şifreleyip öyle kayıt ederseniz, kötü niyetli kullanıcı hafızadan PlayerPrefs’lerin kayıtlı olduğu dosyayı bulsa bile şifreli veriden hiç bir şey anlamayacaktır. Şifreli veriye müdahale ederse ve oyuna girerse, oyun şifrelenmiş data’yı çözemeyecek. Basit bir Try/Catch ile bunu yakalayıp kullanıcının hile yapıp yapmadığını anlayabilirsiniz. Artık ne ceza vereceğiniz size kalmış 🙂

4-) Asset Store’daki Güvenlik Araçlarını Kulanın

Bunun için iki asset önereceğim; Obfuscator ve Anti-Cheat Toolkit. Obfuscator asseti; yazdığınız kodlardaki bütün değişken, class, method, parametreler vs hepsini yeniden adlandırır ve random saçma isimler verir. Sadece bunu değil, ek olarak fake kodlar dahil eder build e. Bu şekilde kötü niyetli kullanıcının sizin kodları okumasını epey bir zorlaştırırsınız. Anti-Cheat Toolkit asseti ise, ram’deki değişkenlerin değerlerini şifreleyip ram’de gizlemeye yarıyor. Bütçeniz varsa bu iki asset’i projenizde kullanmanızı şiddetle tavsiye ediyorum.

Bundan sonraki maddeler sadece sunucu’ya/host’a bağlı uygulamalar için gereklidir(Sadece Online ya da Multiplayer’dan bahsetmiyorum). Ekstra güvenlik için sunucu tabanlı çalışmak çok büyük artılar kazandırır size!

5-) Veritabanına Bağlanmak İçin PHP/ASP Köprüsü Kullanın

Asla C#’tan doğrudan veritabanına bağlanmayın. Kötü niyetli kullanıcılara çok büyük bir açık vermiş olursunuz. Nedeni ise, veritabanı bilgilerinizin açık açık c#’ta kayıtlı kalması. Bütün veritabanınızı PHP ya da ASP’de sunucu içinde halletmeniz ve kullanıcının sadece sunucuya PHP yada ASP yolu ile talimat vermesi doğru bir yoldur.

6-) Kullanıcı, Verileri Sunucuya GET İle Göndermesin

POST ile göndersin. GET’te gönderilen veriler çok açıkta oluyor.

7-) Sunucuda SSL Sertifikası Kullanın

SSL; kullanıcı ile sunucu arasında güvenli bir bağlantı sağlar. Haberleşme şifreli olduğu için güvenlik için yine önemli bir unsur.

😎 Kullanıcının Sunucuya Attığı ve Sunucudan Aldığı Veriler Şifreli Olsun

Burada da yine simetrik şifreleme yöntemlerinden biri kullanılmalı. Kullanıcı veriyi sunucuya göndermeden önce simetrik şifreleme ile şifreleyip öyle göndermeli sunucuya. Sunucu şifreli veriyi aldıktan sonra şifreyi çözecek ve ona göre işlem yapacak. Şunu asla unutmayın ki kullanıcı-sunucu arasındaki veri alışverişi trafiği çok kolay bir şekilde üçüncü taraf yazılımlarla tespit edilebiliyor.

9-) Ayrıca Kullanıcının Attığı Veri’ye Zamanı da Dahil Edin

Veriyi şifreleyip sunucuya öyle atmak, bütün güvenlik problemini çözdüğümüz anlamına gelmiyor. Şöyle bir örnek vereyim; kullanıcı 10 altın ödülünü hak kazandı ve bunu sunucuya bildirmesi gerek o altınları alması için. Kullanıcı, “Ahmet_10_altin_kazandi”(tamamen örnek!) verisini çok güzel bir şekilde şifreleyip sunucuya attı, sunucu şifreyi çözüp Ahmet üyesine 10 altın vermesi gerektiğini anlıyor ve veriyor altınları. Peki ya Ahmet o an network trafiğini takip edip, sunucuya attığı şifreli veriyi bulup, o veriyi başka herhangi bir yazılımla ilgili post url’sine post ederse ne olur? Tebrik ederiz Ahmet, sınırsız altın toplama bug’ı buldun! Bunun ise üstesinden şöyle gelebiliriz; kullanıcı, sunucuya attığı her isteğe/veriye ek olarak anlık tarihi timestamp olarak atmalı. Timestamp sürekli değişeceği için dolaysıyla şifreli veri de değişmiş olacak her seferinde. Sunucu kullanıcıdan aldığı şifreli veriyi çözdükten sonra; kullanıcıdan aldığı timestamp’i, kullanıcının id’si ile birlikte veritabanına kayıt eder. Eğer o kullanıcı aynı şifreli data’yı göndermeye çalışırsa, sunucu veritabanından timestamp i ve o kullanıcının id’sini kontrol eder ve zaten o timestamp in daha önce veritabanına kayıtlı olduğunu görür. Bu durumda işlem yapmaz sunucu ve bu olayı da çözmüş oluruz 🙂

Güvenlik ile ilgili vereceğim tavsiyeler bu kadar. Bu güvenlik önlemlerinin projenizin performansına negatif yönde etki edeceğini unutmayın. Özellikle de 8 ve 9.madde. Yapacak bir şey yok, güvenlik istiyorsanız performastan ödün vereceksiniz 🙂 Okuduğunuz için teşekkürler.

Unity MySQL Score Sistemi (Turkish)

Merhaba arkadaşlar. İlk blogumla karşınızdayım. Bu ilk blog da, Unity için mysql score sistemi nasıl yapılır onu anlatacağım. Şunu başta söylemeliyim ki yapacağımız sistem online olacağı için mutlaka bir hosting e ihtiyacımız olacak. Deneme amaçlı WampServer ya da Xampp gibi local sunucu programlarını kullanabilirsiniz.

Veritabanında tablomuzu oluşturma

İlk yapacağımız şey MySql veritabanımızda tablomuzu oluşturmak. Ben phpMyAdmin kullandığım için tablo oluşturmayı onun üzerinden göstereceğim. phpMyAdmin panelimize giriş yaptıktan sonra hemen soldan kullanacağımız veritabanımızı seçip tablo oluşturuyoruz. Benim yapacağım score panelinde sadece sıra, kullanıcı adı ve score olacak. Bu yüzden Sütun sayısı kısmına 3 yazıp Git diyorum.

Şimdi karşımıza 3 satırdan oluşan bir tablo çıkıyor. Bu tablonun birinci satırın birinci sütununa(yani Adı sütunu) id yazıyoruz. Bu satırımızın ikinci sütunu yani Türü sütunumuzun INT olduğuna dikkat edelim.
MySql’in bize sunduğu AUTO_INCREMENT özelliği sayesinde tabloya veri kayıt ederken sıra numarasını biz yazmadan otomatik sıra numarası verip kayıt edebiliriz. Bu özelliği kullanmak için birinci satırımızın son sütunundan bir önceki sütun olan AUTO_INCREMENT(A_I olarak ta geçebilir)‘ı aktif ediyoruz.
Şimdi ikinci satırımıza geçtik. Adı sütununa username yazıp Türü sütunundan ise VARCHAR’ı seçiyoruz. Bunu seçmemizin sebebi bu kısma string ifadelerin gelmesidir.
Üçüncü satırımıza geçtik. Burada da Adı sütunumuza score yazıp Türü INT olarak seçiyoruz. Tablonun son hali:

(*)Bütün satırların uzunluk değeri 255 olacak.
Bilgileri doğru bir şekilde girdiyseniz Artık Git tuşuna tıklayıp tabloyu kayıt edebilirsiniz.

PHP Kısmı

Şimdi geldik oyun ile veritabanı arasındaki köprüyü kuracak olan PHP ye. İlk önce neden php kullanacağız onu cevaplıyayım. Arada köprü kurmadan C# ile doğrudan veritabanına bağlanamaz mıyız? Evet tabi ki yapabiliriz. Ama bu çok güvensiz olur. Veritabanına bağlanırken “sunucumuzun ip adresini”, “kullanıcı adımızı”, “şifremizi”, “veritabanımızın adını” ve”tablomuzun adını” kaynak kodlarımızda kullanmak zorunda kalacağız. Yayınlanmış uygulamanızın/programınızın kaynak kodlarını ele geçirmek bu dönemde çok basit. Veritabanı bilgileriniz kötü kişilerin eline geçerse, istediği her veriyi ekleyebilir, güncelleyebilir ve hatta silebilir. Veritabanı ile kodlarımız arasında php köprüsü kurarsak, bilgilerimiz php sayfasında şifrelenir ve bu buyuk sorunun üstesinden geliriz.
Şimdi masaüstünde yada başka bir dizinde bir php dosyası oluşturun(Boş bir not defteri oluşturup uzantısını .txt den .php ye değiştirerek yapabilirsiniz). Tavsiyem bir klasör içinde oluşturun. Maksat dosyayı hostingimize attığımızda diğer dosyalarla karışmasın. Php dosyamızın ismini ScoreTable olarak değiştirip alttaki kodları olduğu gibi içine yapıştırın:

<?php
//Veritabanımzıa bağlanmak için boşluklara sırasıyla hostumuzu, kullanıcı adımızı, şifremizi ve veritabanı adımızı yazıyoruz
$mysqli = mysqli_connect("localhost","KullaniciAdi","Sifre","VeritabaniAdi");
//Bağlantıda hata olup olamdığını kontrol ediyoruz
if (mysqli_connect_errno()) 
{
	//Hata varsa, hatayı ekrana basıp kodları durduruyoruz
	echo "Connection error: ".mysqli_connect_error();
    exit();
}

// 'action' adında bir veri postlanırsa ve bu verinin değeri 'AddScore' olursa
if ($_REQUEST['action'] == "AddScore")
{
	//Post edilmiş olan 'username' verisini bu değişkene atıyoruz
     $username = $_POST["username"];
	//Post edilmiş olan 'score' verisini bu değişkene atıyoruz
	$score = $_POST["score"];
	
	//Oyundan post edilen kullanıcı adının veritabanında daha önce kayıtlı olup olmadığını öğrenmek için sorgu yapıyoruz
	$QueryUser = $mysqli->query("SELECT * FROM ScoreTable WHERE (username = '$username')");
	
	if (mysqli_num_rows($QueryUser) > 0)
	{//Kullanıcı zaten kayıtlı, score u güncellenecek
		
		//Veritabanında kayıtlı olan score u değişkene atıyoruz
		 $cur_score = intval(mysqli_fetch_assoc($QueryUser)['score']);
		 
		 //Oyundan post edlien score değerin, veritabanda kayıtlı olan score dan büyükse
		 if($score>$cur_score)
		 {
			  if ($mysqli->query("UPDATE ScoreTable SET score = '$score' WHERE username = '$username'"))
			  {//Veri güncelleme başarılı, ekrana başarılı yazdır
				  echo "success";
			  } 
			  else 
			  {//Veri güncelleme başarısız, hatayı ekrana yazdır
                  echo "Error:".mysqli_error($mysqli);
              }
		 }
		 else
		 {//Score, veritabanında kayıtlı olan score dan küçük
			  echo "Score is less than registered score";
		 }
		 
	}
	else 
	{//Kullanıcı kayıt edilip score u eklenecek
		
	   if ($mysqli->query("INSERT INTO ScoreTable (username, score) VALUES ('$username','$score')")) 
	   {//Veri ekleme başarılı, ekrana başarılı yaz
         echo "success";
       } 
	   else 
	   {//Veri ekleme başarısız, hatayı ekrana yazdır
         echo "Error:".mysqli_error($mysqli);
       }
	}
}
// 'action' adında bir veri postlanırsa ve bu verinin değeri 'GetScores' olursa
else if ($_REQUEST['action'] == "GetScores")
{
	//Veritbanında score u büyükten küçüğe sıralayarak ilk '100' kişiyi çek
	$QueryScores= $mysqli->query("SELECT * FROM ScoreTable Order By score DESC LIMIT 100");
	
	//Veritbanamızda daha veri kayıt edildiyse
	if (mysqli_num_rows($QueryScores) > 0)
	{
		
	while ($row = mysqli_fetch_assoc($QueryScores))	
	{
		//Verileri "," ile ayırarak parse ediyoruz ve ekrana yazdırıyoruz
		echo $row['username'].",".$row['score'].",";
		
	}
		
	}
	else 
	{//Kayıtlı veri bulunamadı
		echo "no_data";
	}
	
	
}
?>

PHP dosyanızı hostinge atma vakti. FileZilla ya da herhangi bir FTP programı kullanarak php dosyanızı hostinge bir dizine atın(Benim php min yolu: https://yudegames.com/Tutorials/ScoreTable/ScoreTable.php). Sonra ise php nizi bir tarayıcıda çalıştırın. Eğer benim gibi boş bir sayfa ile karşılaşırsanız doğru yoldasınız.
(*)PHP dosyasında veritabanı bilgilerimizi değiştirmeyi unutmuyoruz!

Unity Kısmı

Geldik Unity kısmına. Şimdi Score Table adında yeni bir Unity projesi açıyoruz.

Proje açıldıktan sonra klavyeden Ctrl + S ye basarak sahneyi kayıt ediyoruz. Sonra boş bir Game Object oluşturup adını Score Manager yapıyoruz.
Bu oluşturduğumuz boş objeye, ScoreManager adında bir script oluşturup ekleyeceğiz.

Scripti oluşturduktan sonra alttaki kodları olduğu gibi içine yapıştırın:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class Score_Class
{
    public int _rank;
    public string _username;
    public int _score;

    public Score_Class(int _rank, string _username, int _score)
    {
        this._rank = _rank;
        this._username = _username;
        this._score = _score;
    }

}
public class ScoreManager : MonoBehaviour
{

    //Score ların toplantığı liste
    public List<Score_Class> Scores = new List<Score_Class>();

    //Mevcut Score un tutulduğu değişken
    private int score = 100;


    //GUI için kullanıcı adı
    private string usernameField = "Set username...";
    //GUI için scroll pozisyonu
    private Vector2 scrollPosition = Vector2.zero;

    //GUI paneller için
    private bool isMainPanel = true;
    private bool isUsernamePanel = false;
    private bool isScorePanel = false;
    private bool isLoading = false;

    // PHP nin bulunduğu yol. Burayı kendi PHP nizin yolu ile değiştireceksiniz
    private string PHP_path = "https://yudegames.com/Tutorials/ScoreTable/ScoreTable.php";

    private void Start()
    {
        //Score ların toplanacağı listeyi sıfırlama
        Scores.Clear();
        //Daha önce PlayerPrefs de score kayıtlıysa değişkeni kayıtlı veriye eşitle, değilse default(100)değeri kullan
        score = PlayerPrefs.GetInt("score") == 0 ? score : PlayerPrefs.GetInt("score");
    }
    private void OnGUI()
    {

        //GUI style ları
        var GS1 = new GUIStyle(GUI.skin.box);
        var GS2 = new GUIStyle(GUI.skin.button);
        var GS3 = new GUIStyle(GUI.skin.button);
        GS1.fontSize = FontSizeForAllResolations(60f);
        GS2.fontSize = FontSizeForAllResolations(43f);
        GS3.fontSize = FontSizeForAllResolations(35f);


        //Ana Panel
        if (isMainPanel)
        {
            //Score u ekrana yazdır
            GUI.Box(RectForAllResolations(new Rect(34.3f, 35f, 28.8f, 10.3f)), string.Format("Score : {0}", score), GS1);

            //Score ekleme buttonu
            if (GUI.Button(RectForAllResolations(new Rect(31.4f, 50.3f, 16.4f, 10.3f)), "(+)\nAdd", GS2))
            {
                AddScore(50);
            }
            //Score düşürme buttonu
            if (GUI.Button(RectForAllResolations(new Rect(49.5f, 50.3f, 16.4f, 10.3f)), "(-)\nReduce", GS2))
            {
                AddScore(-50);
            }
            //Scorları al butonu
            if (GUI.Button(RectForAllResolations(new Rect(31.37f, 62.3f, 35, 10.3f)), "Get Online Score Panel", GS2))
            {

                if (PlayerPrefs.GetString("RegisteredUsername").Length > 1)
                {
                    //Daha önce kullanıcı adı girildiyse 'ReportScore' fonksiyonunu çağır.
                    //Bu fonksiyonda mevcut score veritabanına gönderiliyor
                    isMainPanel = false;
                    isUsernamePanel = false;
                    isScorePanel = true;
                    StartCoroutine(ReportScore(PlayerPrefs.GetString("RegisteredUsername")));
                }
                else
                {
                    //Daha önce kullanıcı adı girilmediyse, kullanıcı adı panelini göster
                    isMainPanel = false;
                    isUsernamePanel = true;
                    isScorePanel = false;
                }

            }
        }
        //Kullanıcı adı paneli
        if (isUsernamePanel)
        {
            //kullanıcı adı metin kutusu
            usernameField = GUI.TextField(RectForAllResolations(new Rect(32.68f, 34.86f, 30.55f, 8.5f)), usernameField, GS2);

            //Submit(Gönder) buttonu
            if (GUI.Button(RectForAllResolations(new Rect(32.7f, 44.8f, 30.53f, 8.24f)), "Submit", GS2))
            {
                //kullanıcı adı boş değilse ve (,) içermiyorsa ve (.) içermiyorsa ve boşluk içermiyorsa
                if (usernameField.Length > 0 && !usernameField.Contains(",") && !usernameField.Contains(".") && !usernameField.Contains(" "))
                {
                    //'ReportScore' fonksiyonunu çağır. Bu fonksiyonda mevcut score veritabanına gönderiliyor
                    StartCoroutine(ReportScore(usernameField));

                }
                else
                {//Aksi durumda konsolda hata verdir
                    Debug.LogError("Username incorrect!");
                }

            }
            //Geri
            if (GUI.Button(RectForAllResolations(new Rect(0, 0, 15, 15)), "Back", GS2))
            {
                isUsernamePanel = false;
                isMainPanel = true;
                isScorePanel = false;
            }
        }
        //Score Paneli
        if (isScorePanel)
        {
            //GUI için için scroll ve pozisyonu
            scrollPosition = GUI.BeginScrollView(RectForAllResolations(new Rect(26.54f, 30.6f, 48.25f, 40.3f)), scrollPosition, RectForAllResolations(new Rect(0, 0, 45, 9f * (Scores.Count + 1))));

            for (int i = 0; i < Scores.Count; i++)
            {
                //'Scores' listesinde kayıtlı olan veri sayısı kadar button oluştur ve bu buttonlara score bilgilerini yazdır
                GUI.Button(RectForAllResolations(new Rect(1, i * 10, 45, 8.9f)), string.Format("{0})  {1}  >>>  {2}", Scores[i]._rank, Scores[i]._username, Scores[i]._score), GS3);

            }
            //Scroll sonu
            GUI.EndScrollView();

            //Geri
            if (GUI.Button(RectForAllResolations(new Rect(0, 0, 15, 15)), "Back", GS2))
            {
                isUsernamePanel = false;
                isMainPanel = true;
                isScorePanel = false;
            }

        }

        //Loading
        if (isLoading)
        {
            GUI.Box(new Rect(0, 0, Screen.width, Screen.height), "LOADING...", GS1);
        }


    }

    //Bu fonksiyonda mevcut score veritabanına gönderiliyor
    private IEnumerator ReportScore(string _username)
    {

        Scores.Clear(); //Listeyi sıfırla
        isLoading = true;//Loading i aktifleştir
        string url = PHP_path;// PHP nin bulunduğu yol
        WWWForm form = new WWWForm();//Verileri postlamak için Form
        form.AddField("action", "AddScore");//Postlanacak veri
        form.AddField("username", _username);//Postlanacak veri
        form.AddField("score", score);//Postlanacak veri
        WWW www = new WWW(url, form);//İsteği gönder
        yield return www;//Cevap bekle
        Debug.Log(www.text);//Cevabı konsola yazdır
        PlayerPrefs.SetString("RegisteredUsername", _username);//Kullanıcının kayıt yaptığını playerprefs ile hafızada tut

        //Kayıt başarılı, Score pencerisini aç
        isLoading = true;
        isMainPanel = false;
        isUsernamePanel = false;
        isScorePanel = true;

        //Score ları almak için 'GetScores' fonksiyonunu çağır
        StartCoroutine(GetScores());
    }

    //Bu fonksiyonda Veritabanından score lar çekiliyor
    private IEnumerator GetScores()
    {
        isLoading = true;//Loading i aktifleştir
        string url = PHP_path;// PHP nin bulunduğu yol
        WWWForm form = new WWWForm();//Verileri postlamak için Form
        form.AddField("action", "GetScores");//Postlanacak veri
        WWW www = new WWW(url, form);//İsteği gönder
        yield return www;//Cevap bekle
        Debug.Log(www.text);//Cevabı konsola yazdır
        isLoading = false;//Loading i deaktif et

        string received = www.text.Substring(0, www.text.Length - 1);// PHP den gelen verinin son karakterini(',') silerek bu değişkene aktar
        string[] received_array = received.Split(',');//verileri virgül(,) ile ayır
        int _count = received_array.Length / 2; //PHP den gelen veriler sadece 'username' ve 'score' olduğu için gelen score sayısını bulmak için 2 ye bölüyoruz
        int _index = 0;
        for (int i = 0; i < _count; i++)
        {
            //'Scores' listesine score ekle
            Scores.Add(new Score_Class(i + 1, received_array[0 + _index], int.Parse(received_array[1 + _index])));
            _index += 2;
        }
    }

    //Deneme amaçlı kendi score umuzu güncellemek için score ekleme/çıkarma fonksiyonu
    private void AddScore(int _value)
    {
        score += _value;
        PlayerPrefs.SetInt("score", score);
    }

    //GUI ların bütün çözünürlüklere uyması için
    private Rect RectForAllResolations(Rect _rect)
    {
        return new Rect(_rect.x * Screen.width * 0.01f, _rect.y * Screen.height * 0.01f, _rect.width * Screen.width * 0.01f, _rect.height * Screen.height * 0.01f);
    }
    //GUI ların yazı büyüklüklerinin bütün çözünürlüklere uyması için
    private int FontSizeForAllResolations(float _size)
    {
        return Mathf.RoundToInt(_size * Screen.width / (1920f));
    }

}

Kodlarımız buraya kadar. Scripti olduğu gibi hiç değişiklik yapmadan derleyip çalıştırırsanız, benim kurduğum score sistemine bağlanıp, score ekleyip, güncelleyip, veritabanında kayıtlı olan bütün score ları görebilirsiniz.
Kendi veritabanınızı kullanmak için scriptte üstlerde yer alan PHP_path değişkenini, kendi PHP yolunuz ile değiştirmeyi Unutmayın.
Metin ile zor izah edileceğinden dolayı UI yerine GUI kullandım. Bunu Canvas kullanarak UI a çevirmek gayet basit. Az kodlama bilginiz varsa uydurabilirsiniz. Tek yapmanız gereken Scores listesinin içeriğini UI elementlerine ve özellikle text lere dökmek.
Bu tutorial ımızın sonuna geldik. Yaptığımız score sistemi çok basit bir sistem di. Bunu geliştirip bir Login/Register sistemine entegre edebilir hatta facebook ve google gibi API ları kullanarak score sistemini onlara uyumlu çalışabilecek hale getirebilirsiniz.
Yaşayacağınız sorunları Facebook adresimizden bana ulaşarak danışabilirsiniz. Kolay gelsin.
Ekran görüntüleri:

Scroll to top