Blazor Üzerinde SignalR Core İle Zar Oyunu

Selamlar,

Bu makalede Blazor ile SignalR kullanılarak, peer to peer işlemler yapılacaktır. Blazor hakkında daha detaylı bilgiye, bu makaleden erişebilirsiniz. Bu proje, Client ve Server Side :) olan Blazor üzerinde koşacaktır. Kısaca Blazor, Client-Side tarafda çalışsa da, DOM etkileşimi aynı process içerisinde, yani backend tarafında değil, client’ın browser’ı üzerinde yapılmaktadır.

Bugünkü örneğimizde, Blazor üzerinde .Net Core SignalR bir Hub sunucusuna bağlanıp, 2 client arasında zar oyunu oynayacağız. Bu 2 client, birbiri ile “one-to-one” haberleşeceklerdir. Kısacası bu makalede, ortaya yazılan “one-to-many” chat uygulaması yerine, connectionID’ye bağlı olarak, private bir haberleşme yapılacaktır.

Bu makalenin yazıldığı an itibari ile, son sürüm .Net Core 3.0’ın aşağıdaki linkden yüklenmesi gerekmektedir.

https://dotnet.microsoft.com/download/dotnet-core/3.0

Bu projedeki .Net Core’un en son versiyonu aşağıdaki gibidir:

.Net Core Blazor Şablonu aşağıdaki komut ile yüklenir.

dotnet new –help” komutu ile aşağıdaki proje templateleri, bu yüklemeden sonra listelenmektedir.

dotnet new blazor -o blazor_client” komutu ile ilgili proje oluşturulur. Aşağıdaki gibi bir eörnek bir proje ile karşılaşılır:

Blazor Uygulamasının backend tarafı, webapi projesi şeklinde aşağıdaki gibi oluşturulur.

Startup.cs altında gerekli ayarlar aşağıdaki gibidir:

ConfigureServices():
  • AddSignalR() : SignalR projeye eklenir.
  • AddCors() : Client Side taraftan, Server Side tarafa bir istekde bulunulabilmesi için, Cors Policy tanımlanır.
  • AddMvc(option => option.EnableEndpointRouting = false): Projeye Mvc eklenir.
    • Not: Mvc, Option olarak “EnableEndpointRouting” false şeklinde  kapatılmaz ise, .Net Core 3.0 Preview 5’de hata alınmaktadır.

Configure() : 

  • UseCors() : ConfigureServices’de tanımlanan, policy burada eklenir.
  • UseSignalR() : SignalR sınıfına erişilecek routing, burada tanımlanır. “dicehub”. Not: “DiceHub”  makalenin devamında yaratılacak olan hub sınıftır.
  • UseMvc() : Projenin Mvc tanımı burada yapılır.

Amaç: 2 kullanıcı Blazor bir web sayfasında Nicklerini yazıp oyun havuzuna dahil olacaklar. Eğer havuzda herhangi bir client var ise, bağlanan client havuzdaki client ile eşleşip, realtime zar oyunu oynayacaklar. Büyük atan kazanacak. Bir de yeniden oyna buttonu olacak. Bu button tıklandığı zaman, sayfa yenilenerek ilgili client’ın tekrar zar atması sağlanacak.

DiceHub: Aşağıda görüldüğü gibi, DiceHub adında signalR bir sınıfı vardır. İlk etapta connect olan clientların “connectionID“‘leri, kendilerine gönderilmektedir. Ayrıca disconnect olan clientların da, connectionID’leri, ilgili sistemde yakalanmaktadır. Son olarak “ConcurrentDictionary” bir Liste bulunmaktadır. Yukarıda bahsedilen havuz işte bu dictionarydir. Disconnect durumunda, ilgili connectionId’ye ait userName, bu concurrentDictionary’de bulunup listeden yani havuzdan çıkarılır.

Peki neden ConcurrentDictionary ? Çünkü, asenkron olarak gelen birden fazla client’ın aynı nick ile havuza dahil olması istenmemektedir. Asenkron durumlarda, dictionary ile çalışılabilecek ilgili lock mekanizmasını sağlayan, Concurrent listelerden faydalanılmıştır. Daha detaylı bilgiye bu makaleden erişebilirsiniz.

  • return clientList.TryAdd(userName,connectionId)” : İlgili client’ın kullanıcı adını girmesi durumunda, ConcurrentDictionary’de ilgli userName yok ise listeye eklenir.
  • clientList.Where(entry => entry.Value == connectionId) .Select(entry => entry.Key).FirstOrDefault()” : Client’ın disconnect durumunda, sadece yakalanabilen signalR ConnectionID’sidir. Bu nedenle ilgili dictionary’den client, value değeri ile yakalanmaktadır.
  • clientList.TryRemove(userName, out _)” : Client’ın disconnect olması durumunda yakalanan client, listeden yani havuzdan çıkarılır.

Şimdi gelin burada bir ara verelim ve ön yüz tarafında yani Blazor tarafında, oyun sayfasını oluşturmaya başlayalım.

BLAZOR_CLIENT projesi altında Pages Folder’ı altına DiceGames.razor adında yeni bir sayfa oluşturulur. Routing amaçlı @page “/dicegame” tanımlanır.

Shared folder altındaki NavMenu.razor altına, aşağıdaki “dicegame” sayfası menu’ye eklenir. Son görünüş, yukarıdaki gibidir.

Son olarak zar resimleri wwwroot/Images altına eklenir.

Blazor tarafında “Blazor.Extensions.SignalR” aşağıdaki komut ile projeye eklenir. Bu makale yazıldığı an itibari ile “0.1.9” versiyonu bulunmaktadır.

DiceGame.razor:

  • Öncelikle HubConnection connection, client’ın connectionID’si ve kullanılacak userName burada tanımlanır.
  • [Inject] private HubConnectionBuilder _hubConnectionBuilder { get; set; } ” : signalR Hub’sınıfına bağlanan nesne burada tanımlanır.
    • Not: Startup.cs: Blazor projesinde ConfigureServices altında, HubConnectionBuilder’ın tanımlanması gerekmektedir.
  • DiceGame.razor / OnInitAsync() methodunda, yani sayfa ilk yüklendiğinde, .Net Core signalR diceHub sınıfına bağlanılır. Opsiyonel olarak bağlantı tipinin WebSocket olmasına zorlanılır. Ayrıca Trace ile errorların izlenmesi sağlanır.
  • connection.On<string>(“GetConnectionId”, this.OnGetConnectionId)” : Her bir client diceHub sınıfına connect olduğu zaman, server side’dan – client side tarafa connect oldukları connectionID değeri, “GetConnectionId()” function’ına gönderilir. İşte bu function içinde, “OnGetConnectionId()” methodu çağrılmıştır.Yani sunucu tarafından tetiklenen client-side taraflı functiondır.
  • AddList() ile connect olan client’ın, server-side taraftaki AddList() methodunu tetiklemesi sağlanır. Böylece connect olan client, havuza dahil edilir.

DiceGame.razor (functions): Bir çeşit Blazor’ın, backend tarafı diyebiliriz.

DiceGame.razor (Html) : Yukarıda görülen Html’in kodları aşağıdaki gibidir :

  • <input type=”text” id=”userName” bind=”@userName” />” : Input text alan “userName” değişkenine bind edilmiştir.
  • <input type=”button” id=”addList” value=”Connect” class=”btn btn-primary” onclick=”@AddList”/>” : Connect Button’u tıklandığında “AddList()” function’ı çağrılmaktadır.

Şimdi gelin isterseniz kodları adım adım ilerletelim. Öncelikle Connect işlemi olduğu zaman, kullanıcı adı ve zar atan bir buttonu bir değişkene bağlayarak gösterelim (isConnect). Aşağıda, connect işlemi yapıldığı zaman çağrılan “AddList()” methodu değiştirilmiştir. Eğer aynı nick’li başka bir client yok ise “true” var ise “false” cevabı dönmektedir. Bu kontrol server side tarafta, “clientList.TryAdd()” methodu ile bakılarak denenmektedir.

Html tarafında “isConnect” değişkenine bakılarak ilgili kullanıcı adı girişi, ekrana basılır ya da kaldırılır.

Aşağıda connect işlemi olunduktan sonra ekrana basılan Zar Atma(Play Dice) ve zar atılmış ise Random gelen zarların ekrana basıldığı Html, gösterilmektedir. Player 1’e ait Random gelen zarlar, “diceOne” ve “diceTwo” değişkenleri ile tanımlanmaktadır. Ayrıca client’ın userName’i ve zarların toplam sayısı “Score” ekrana basılmaktadır.

Connect olunmuş ise:

  • Connect olan user’ın userName’i, gelen zar değerleri ve varsa Score gösterilir.

DiceGame.razor / PlayDice() : 

Sırada “PlayDice” butonuna basıldığı zaman çağırlan “PlayDice()” methodunda:

  • 1. ve 2. zarlar diceOne ve diceTwo olarak tanımlanmış ve başta “0” değeri atanmıştır. Eğer değerleri “0” ise, yukarıda görüldüğü gibi ekrana basılmamaktadırlar.
  • System.Random rnd= new Random()” : Random sayı üretecek nesne yaratılır.
  • diceOne=rnd.Next(1,7)” : 1. zara 1 ile 6 arasında bir sayı üretilmesi sağlanır.
  • “System.Threading.Thread.Sleep(1000)” : 2 zar sonuçlarının birbirine yakın veya aynı çıkmaması için, 2 zar arasında 1sn süre bekletilir.
  • diceTwo=rnd.Next(1,7)” : 2. zar için de 1 ile 6 arasında bir sayı üretilmesi sağlanır.

Şimdi kodu biraz daha şekillendirelim.

Amaç: Login olunulduğu zaman, havuzda login olan clientdan başka bir client daha var ise 2’sinin eşleştirilmesini sağlıyalım.

DiceController.cs / AddList() : Server Side tarafta AddList() methodu, aşağıdaki gibi değiştirilir.

  • AddList() methodu ==> Login olan UserName ve ConnectionID’sini parameter olarak almıştır.
  • clientList.TryAdd() ==> Eğer aynı isimde client yok ise ==> GetUser(userName) methodu çağrılmaktadır.

DiceController / GetUser() : Sistem içerisinde Login olan user’dan bir başkası var ise, havuzdaki ilk user(Player2) alınır ve Login olan user’ın(Player1) FetchUser() function’ına trigger edilerek gönderilir. Aynı şekilde, havuzdan çekilen User’a(Player2) da (Player1) “FetchUser()” function’ı trigger edilerek gönderilir. Böylece connect olan 2 client’a da, rakipleri gönderilmiş olunur.

Son olarak her 2 user da clientList havuzundan çıkarılır.

DiceGame.razor : Blazor ClientSide tarafta yapılan değişiklikler aşağıdaki gibidir.

  • Öncelikle Player2’ye ait yani rakip client’ın userName2 ve connectionID2 değerleri tanımlanır.
  • isConnectPlayer2: Player2 connect olduğunda, ilgili html’in gösterilmesi için tanımlanır.
  • diceThree, diceFour : Player2 için atılan zarların gösterilmesi için tanımlanan değişkenlerdir.

  • Player2 ile eşleşildiği zaman “GetUser()” methodunda çağrılan “FetchUser()” function’ı, aşağıda görüldüğü gibi tanımlanır. Bu function içinde OnFetchUser() methodu çağrılmıştır.

DiceGame.razor / OnInitAsync(): Sayfaya ilk gelinildiği zaman, Server Side taraftan tetiklenen “FetchUser()” function’ı, aşağıdaki gibi tanımlanmıştır.

DiceGame.razor / OnFetchUser(): Connect olan Player2’e ait connectionID2 ve userName2 burada atanır. isConnectPlayer2 değeri, Html’de Player2’ye ait bilgilerin gözükmesi için “true” değerine atanır.

Önemli Not: Blazor’da Html bir sayfanın parametreleri değiştiği zaman, tekrardan Render edilmesi için “StateHasChanged()” methodunun bazen çağrılması gerekmektedir.

DiceGame.razor / Html : Blazor Html sayfada Player2 bilgilerinin de yukarıdaki gibi gözükmesi için, aşağıdaki HTML’in eklenmesi gerekmektedir.

  • “@if(isConnectPlayer2)” : Player2’nin connect olduğu zaman gözükmesi sağlanmıştır.
  • “@userName2” : Player2’nin UserName’i gösterilmektedir.
  • “@if(diceThree!=0)” Player2’e ait herhangi bir zar bilgisi var ise ekrana basılır.

Şimdi son olarak sıra geldi, zar atıldığı zaman sonucun diğer player’a da iletilmesi ve kazananın belirlenmesine.

Server Side DiceHub / SendDice(): Öncelikle aşağıda görüldüğü gibi, ServerSide tarafında bir client zar attığı zaman, sonucun diğer rakip client’a da bildirilmesi için signalR Hub SendDice() methodu aşağıdaki gibi yazılır.

  • connectionID: Zar sonucunun gönderileceği client’ın ConnectionID’si.
  • userName: Zarı atan client’ın UserName’i.
  • diceOne, diceTwo: Atılan zar sonuçları.
  • Client Side tarafta “GetDice()” function’ı, ilgili paramtereler ile trigger edilir. Böylece karşı rakibin attığı zarlar, ekrana basılır.

Client Side DiceGame.razor/SendDice() : Aşağıda görüldüğü gibi Zar atılınca SendDice() function’ı çağrılır. Diğer client’ın connectionID’si ve zar bilgileri ile beraber, yukarıda tanımlanan signalR Hub sınıfındaki “SendDice()” methodu tetiklenir. Ayrıca kimin kazandığını belirleyen “CalculateResult()” methodu, inner function olarak çağrılır. Görüldüğü üzere, Blazor üzerindeki tüm methodlar, asenkron olarak çağrılmaktadır.

Client Side DiceGame.razor/CalculateResult(): Hangi client’ın kazandığını hesaplayan ve kazanan client’ın userName’ini, ekrana alert olarak basan function’dır. Burada önemli olan konu “ShowAlert()” function’ıdır.

Not: Bu da demek oluyor ki, Blazor üzerinde Javascript function’ı çağrılabilmektedir.

Client Side DiceGame.razor/ShowAlert():  Aşağıda görüldüğü gibi ShowAlert() javacript function’ı, kazanan User’ın ekrana yazılması için, @inject IJSRuntime  kütüphanesi kullanılarak tetiklenmiştir.

Client Side index.html / ShowAlert():  İlgili function, Blazor sayfası üzerinde tanımlandığında hata alınmaktadır. “Index.html” sayfasına aşağıdaki gibi tanımlanır. Ve böylece kazanan client’ın userName’i, ekrana alert olarak basılır.

Not: “alert()” function’ının 1sn geçtikten sonra tetiklenmesinin nedeni, zar sonucunun gelmesi anında,  ilgili değişkenlerin atanarak sayfanın yeniden render edilmesinin beklenmesidir. Aksi takdirde, zarların ekranda gözükmesinden önce, kazanan client’ın userName’inin ekrana “alert()” olarak basılması gerçekleşmektedir. Bu da tabii ki doğru bir durum değildir.

Şimdi sıra geldi Client Side tarafta tanımlanan, ilgili zar bilgisini karşılayan GetDice() function’ını yazmaya:

OnInitAsync() : İlgili methoda, “GetDice()” function’ı aşağıdaki gibi tanımlanır. İlgili function çağrılınca, OnGetDice() function’ı call edilir. Parametre olarak “<userName, diceOne, diceTwo>” gönderilir.

Client Side DiceGame.razor/ OnGetDice():

  • diceThree=_diceOne; diceFour=_diceTwo;” : Rakip client’ın zar bilgileri atanır.
  • if(diceOne!=0 && diceTwo!=0 && diceThree!=0 && diceFour!=0)” : Tüm zarlar seçili ise, “isplayAgain” değişkeni “true” olarak atanır. Böylece “Play Dice” button’u gizlenerek, “Play Again” buttonu gösterilir.
  • StateHasChanged()” :  İlgili değişkenleri değişmesi ile sayfanın tekrardan render edilmesi sağlanır.
  • “await CalculateResult();” : Kimin kazandığı ile ilgili hesaplama işlemi yapılarak, kazanan client’ın userName’i ekrana basılır.

 

İgili  “Play Dice ve Play Again” buttonlarının html’i, aşağıdaki gibi değiştirilir.

Client Side DiceGame.razor / PlayAgain() : Aşağıda görüldüğü gibi zar atılıp, kazanan belli olduktan sonra tekrar oynana bilmesi için, tüm zar değerleri “0”‘lanır ve sayfa tekrardan render’a zorlanır.

Geldik bir makalenin daha sonuna. Bu makalede Blazor’ın derinliklerine inerken, .Net Core signalR ile nasıl çalıştığını hep beraber ufak bir oyun yazarak inceledik. Belli ki Blazor’ın üzerinde çokça çalışılmış. Bence bu hali ile bile bir çok küçük projede, rahatlıkla kullanılabilir. Client-Side ve Server-Side kodların hepsinin aynı sayfa üzerinde yazılması, sunucu bazlı performans kayıplarının aradan çıkarılması ve derleme süresinin kısalığı, Blazor’ın önümüzdeki dönemlerde daha çok konuşulacağını gösteriyor.

Yeni bir makalede görüşmek üzere, hepinize hoşçakalın.

Client Side DiceGame.razor (Full):

Server Side / DiceController.cs (Full):

Source Code: https://github.com/borakasmer/blazorsignalRDiceGame

Kaynaklar:

Herkes Görsün:

Bunlar da hoşunuza gidebilir...

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir