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.
Image Source: https://miro.medium.com/max/1196/1*v9K8wxfZCiZV_1355C-7Cw.png
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.
Image Source: https://miro.medium.com/max/1300/1*Hjt6KCkYPds0FE1u1fjUiQ.png
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.
|
1 |
dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.0.0-preview5-19227-01 |
“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.
|
1 |
dotnet new webapi -o blazor_server |
Startup.cs altında gerekli ayarlar aşağıdaki gibidir:
- 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.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public void ConfigureServices(IServiceCollection services) { services.AddSignalR(); services.AddCors(options => options.AddPolicy("DicePolicy", builder => { builder.AllowAnyMethod() .AllowAnyHeader() .AllowCredentials() .WithOrigins("http://localhost:5000"); })); services.AddMvc(option => option.EnableEndpointRouting = false); services.AddControllers() .AddNewtonsoftJson(); } |
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.
|
1 2 3 4 5 |
. . app.UseCors("DicePolicy"); app.UseSignalR(routes => routes.MapHub<DiceHub>("/dicehub")); app.UseMvc(); |
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.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class DiceHub : Hub { static ConcurrentDictionary<string, string> clientList = new ConcurrentDictionary<string, string>(); public override async Task OnConnectedAsync() { await Clients.Caller.SendAsync("GetConnectionId", this.Context.ConnectionId); } public bool AddList(string userName, string connectionId) { return clientList.TryAdd(userName,connectionId); } public override async Task OnDisconnectedAsync(Exception exception) { string connectionId = Context.ConnectionId; string userName = clientList.Where(entry => entry.Value == connectionId) .Select(entry => entry.Key).FirstOrDefault(); clientList.TryRemove(userName, out _); await base.OnDisconnectedAsync(exception); } } |
Ş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.
|
1 2 3 |
@page "/dicegame" <h1>Wellcome To Dice Game</h1> |
Shared folder altındaki NavMenu.razor altına, aşağıdaki “dicegame” sayfası menu’ye eklenir. Son görünüş, yukarıdaki gibidir.
|
1 2 3 4 5 |
<li class="nav-item px-3"> <NavLink class="nav-link" href="dicegame"> <span class="oi oi-list-rich" aria-hidden="true"></span> Dice Game </NavLink> </li> |
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.
|
1 |
dotnet add package Blazor.Extensions.SignalR --version 0.1.9 |
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.
1234public void ConfigureServices(IServiceCollection services){services.AddTransient<HubConnectionBuilder>();}
- 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.
-
1234567connection = _hubConnectionBuilder.WithUrl("http://localhost:1923/dicehub",opt =>{opt.LogLevel = SignalRLogLevel.Trace;opt.Transport = HttpTransportType.WebSockets;}).Build();
-
- “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.
-
123456Task OnGetConnectionId(string _connectionID){System.Console.WriteLine("ConnectionID:" + _connectionID);connectionID = _connectionID; return Task.CompletedTask;return Task.CompletedTask;}
-
- AddList() ile connect olan client’ın, server-side taraftaki AddList() methodunu tetiklemesi sağlanır. Böylece connect olan client, havuza dahil edilir.
-
1234async Task AddList(){await connection.InvokeAsync("AddList", "User Name", connectionID);}
-
DiceGame.razor (functions): Bir çeşit Blazor’ın, backend tarafı diyebiliriz.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
@functions { string connectionID; string userName; HubConnection connection; [Inject] private HubConnectionBuilder _hubConnectionBuilder { get; set; } protected override async Task OnInitAsync() { connection = _hubConnectionBuilder .WithUrl("http://localhost:1923/dicehub", opt => { opt.LogLevel = SignalRLogLevel.Trace; // Client log level opt.Transport = HttpTransportType.WebSockets; // Which transport you want to use for this connection }) .Build(); connection.On<string>("GetConnectionId", this.OnGetConnectionId); await connection.StartAsync(); } Task OnGetConnectionId(string _connectionID) { System.Console.WriteLine("ConnectionID:" + _connectionID); connectionID = _connectionID; return Task.CompletedTask; } async Task AddList() { await connection.InvokeAsync("AddList", "User Name", connectionID); } } |
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.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
@page "/dicegame" @using Blazor.Extensions; <form> <h1>Wellcome To Dice Game</h1> <div class="form-group row"> <label class="control-label" for="userName">User Name</label> <div class="col-sm-10"> <input type="text" id="userName" bind="@userName" /> <input type="button" id="addList" value="Connect" class="btn btn-primary" onclick="@AddList"/> </div> </div> </form> |
Ş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.
|
1 2 3 4 5 6 7 8 9 10 |
bool isConnect=false; async Task AddList() { bool result = await connection.InvokeAsync<bool>("AddList", userName, connectionID); System.Console.WriteLine("User Add Result:" + result); if(result){ isConnect=true; } } |
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.
|
1 2 3 4 5 6 7 8 |
<div class="form-group row"> @if(!isConnect) { <label class="control-label" for="userName">User Name</label> <div class="col-sm-10"> <input type="text" id="userName" bind="@userName" /> <input type="button" id="addList" value="Connect" class="btn btn-primary" onclick="@AddList"/> </div> } |
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.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
else { <label class="control-label" for="playDice"><h3><font color="red">@userName</font></h3></label> <div class="col-sm-10"> <input type="button" id="playDice" value="Play Dice" class="btn btn-primary" onclick="@PlayDice"/> @if(diceOne!=0) { <img src="/Images/@(diceOne).png" asp-append-version="true" width="50px" /> <img src="/Images/@(diceTwo).png" asp-append-version="true" width="50px" /> <label class="control-label"><h3>Score : @(diceOne+diceTwo)</h3></label> } </div> } |
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.
|
1 2 3 4 5 6 7 8 9 10 11 |
int diceOne=0; int diceTwo=0; void PlayDice() { System.Random rnd= new Random(); diceOne=rnd.Next(1,7); System.Threading.Thread.Sleep(1000); diceTwo=rnd.Next(1,7); System.Console.WriteLine("Dice 1:" + diceOne); System.Console.WriteLine("Dice 2:" + diceTwo); } |
Ş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.
|
1 2 3 4 5 6 7 8 9 |
public async Task<bool> AddList(string userName, string connectionId) { var result= clientList.TryAdd(userName, connectionId); if(result) { await GetUser(userName); } return result; } |
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.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public async Task GetUser(string userName) { if (clientList.Any(cl => cl.Key != userName)) { var player2 = clientList.First(cl => cl.Key != userName); var player1 = clientList.First(cl => cl.Key == userName); await Clients.Client(player1.Value).SendAsync("FetchUser", player2.Key, player2.Value); await Clients.Client(player2.Value).SendAsync("FetchUser", player1.Key, player1.Value); clientList.TryRemove(player1.Key,out _); clientList.TryRemove(player2.Key, out _); } } |
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.
|
1 2 3 4 5 |
string userName2; string connectionID2; bool isConnectPlayer2=false; int diceThree=0; int diceFour=0; |
- 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.
|
1 2 3 4 5 6 7 |
protected override async Task OnInitAsync() { . . connection.On<string,string>("FetchUser", this.OnFetchUser); . } |
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.
|
1 2 3 4 5 6 7 8 9 10 |
Task OnFetchUser(string userName, string connectionID) { System.Console.WriteLine("Player2 Name:" + userName); System.Console.WriteLine("Player2 ConnectionID:" + connectionID); connectionID2 = connectionID; userName2 = userName; isConnectPlayer2=true; StateHasChanged(); return Task.CompletedTask; } |
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.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@if(isConnectPlayer2) { <div class="form-group row"> <label class="control-label"><h3><font color="red">@userName2</font></h3></label> <div class="col-sm-10"> @if(diceThree!=0) { <img src="/Images/@(diceThree).png" asp-append-version="true" width="50px" /> <img src="/Images/@(diceFour).png" asp-append-version="true" width="50px" /> <label class="control-label"><h3>Score : @(diceThree+diceFour)</h3></label> } </div> </div> } |
Ş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.
|
1 2 3 4 |
public async Task SendDice(string connectionID, string userName, int diceOne, int diceTwo) { await Clients.Client(connectionID).SendAsync("GetDice", userName, diceOne, diceTwo); } |
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.
|
1 2 3 4 5 6 |
async Task SendDice() { await connection.InvokeAsync("SendDice", connectionID2, userName, diceOne,diceTwo); await CalculateResult(); System.Console.WriteLine("Dice Send"); } |
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.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
async Task CalculateResult(){ if(diceOne!=0 && diceTwo!=0 && diceThree!=0 && diceFour!=0) { if((diceOne+diceTwo)>(diceThree+diceFour)) { System.Console.WriteLine("Winner:" + userName); await ShowAlert("Kazanan :"+userName); } else { System.Console.WriteLine("Winner:" + userName2); await ShowAlert("Kazanan :"+userName2); } } } |
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.
|
1 2 3 4 5 |
@inject IJSRuntime jsRuntime private async Task ShowAlert(string message) { var result = await jsRuntime.InvokeAsync<object>("ShowAlert", message); } |
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.
|
1 2 3 4 5 6 7 |
<body> <script> window.ShowAlert = (message) => { setTimeout(function () { alert(message); }, 1000); } </script> </body> |
Ş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.
|
1 2 3 |
. . connection.On<string,int,int>("GetDice", this.OnGetDice); |
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.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
async Task OnGetDice(string userName, int _diceOne, int _diceTwo) { diceThree=_diceOne; diceFour=_diceTwo; if(diceOne!=0 && diceTwo!=0 && diceThree!=0 && diceFour!=0) { isplayAgain=true; } StateHasChanged(); await CalculateResult(); } |
İgili “Play Dice ve Play Again” buttonlarının html’i, aşağıdaki gibi değiştirilir.
|
1 2 3 4 5 6 7 8 |
@if(!isplayAgain) { <input type="button" id="playDice" value="Play Dice" class="btn btn-primary" onclick="@PlayDice"/> } else { <input type="button" id="playDice" value="Play Again" class="btn btn-primary" onclick="@PlayAgain"/> } |
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.
|
1 2 3 4 5 6 7 8 |
void PlayAgain(){ isplayAgain=false; diceOne=0; diceTwo=0; diceThree=0; diceFour=0; StateHasChanged(); } |
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):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
@page "/dicegame" @using Blazor.Extensions; <form> <h1>Wellcome To Dice Game</h1> @if(!isConnect) { <div class="form-group row"> <label class="control-label" for="userName">User Name</label> <div class="col-sm-10"> <input type="text" id="userName" bind="@userName" /> <input type="button" id="addList" value="Connect" class="btn btn-primary" onclick="@AddList"/> </div> </div> } else { <div class="form-group row"> <label class="control-label" for="playDice"><h3><font color="red">@userName</font></h3></label> <div class="col-sm-10"> @if(!isplayAgain) { <input type="button" id="playDice" value="Play Dice" class="btn btn-primary" onclick="@PlayDice"/> } else { <input type="button" id="playDice" value="Play Again" class="btn btn-primary" onclick="@PlayAgain"/> } @if(diceOne!=0) { <img src="/Images/@(diceOne).png" asp-append-version="true" width="50px" /> <img src="/Images/@(diceTwo).png" asp-append-version="true" width="50px" /> <label class="control-label"><h3>Score : @(diceOne+diceTwo)</h3></label> } </div> </div> } @if(isConnectPlayer2) { <div class="form-group row"> <label class="control-label"><h3><font color="red">@userName2</font></h3></label> <div class="col-sm-10"> @if(diceThree!=0) { <img src="/Images/@(diceThree).png" asp-append-version="true" width="50px" /> <img src="/Images/@(diceFour).png" asp-append-version="true" width="50px" /> <label class="control-label"><h3>Score : @(diceThree+diceFour)</h3></label> } </div> </div> } </form> @functions { string connectionID; string userName; HubConnection connection; bool isConnect=false; bool isplayAgain=false; string userName2; string connectionID2; bool isConnectPlayer2=false; int diceThree=0; int diceFour=0; [Inject] private HubConnectionBuilder _hubConnectionBuilder { get; set; } @inject IJSRuntime jsRuntime protected override async Task OnInitAsync() { connection = _hubConnectionBuilder .WithUrl("http://localhost:1923/dicehub", opt => { opt.LogLevel = SignalRLogLevel.Trace; // Client log level opt.Transport = HttpTransportType.WebSockets; // Which transport you want to use for this connection }) .Build(); connection.On<string>("GetConnectionId", this.OnGetConnectionId); connection.On<string,string>("FetchUser", this.OnFetchUser); connection.On<string,int,int>("GetDice", this.OnGetDice); await connection.StartAsync(); } Task OnGetConnectionId(string _connectionID) { System.Console.WriteLine("ConnectionID:" + _connectionID); connectionID = _connectionID; return Task.CompletedTask; } async Task AddList() { bool result = await connection.InvokeAsync<bool>("AddList", userName, connectionID); System.Console.WriteLine("User Add Result:" + result); if(result){ isConnect=true; } } async Task SendDice() { await connection.InvokeAsync("SendDice", connectionID2, userName, diceOne,diceTwo); await CalculateResult(); System.Console.WriteLine("Dice Send"); } Task OnFetchUser(string userName, string connectionID) { System.Console.WriteLine("Player2 Name:" + userName); System.Console.WriteLine("Player2 ConnectionID:" + connectionID); connectionID2 = connectionID; userName2 = userName; isConnectPlayer2=true; StateHasChanged(); return Task.CompletedTask; } async Task OnGetDice(string userName, int _diceOne, int _diceTwo) { diceThree=_diceOne; diceFour=_diceTwo; if(diceOne!=0 && diceTwo!=0 && diceThree!=0 && diceFour!=0) { isplayAgain=true; } StateHasChanged(); await CalculateResult(); } async Task CalculateResult(){ if(diceOne!=0 && diceTwo!=0 && diceThree!=0 && diceFour!=0) { if((diceOne+diceTwo)>(diceThree+diceFour)) { System.Console.WriteLine("Winner:" + userName); await ShowAlert("Kazanan :"+userName); } else { System.Console.WriteLine("Winner:" + userName2); await ShowAlert("Kazanan :"+userName2); } } } int diceOne=0; int diceTwo=0; async Task PlayDice() { System.Random rnd= new Random(); diceOne=rnd.Next(1,7); System.Threading.Thread.Sleep(1000); diceTwo=rnd.Next(1,7); StateHasChanged(); System.Console.WriteLine("Dice 1:" + diceOne); System.Console.WriteLine("Dice 2:" + diceTwo); if(diceOne!=0 && diceTwo!=0 && diceThree!=0 && diceFour!=0) { isplayAgain=true; } await SendDice(); } void PlayAgain(){ isplayAgain=false; diceOne=0; diceTwo=0; diceThree=0; diceFour=0; StateHasChanged(); } private async Task ShowAlert(string message) { var result = await jsRuntime.InvokeAsync<object>("ShowAlert", message); } } |
Server Side / DiceController.cs (Full):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; namespace blazor_server.Controllers { [Route("api/[controller]")] [ApiController] public class DiceController : ControllerBase { // GET api/values [HttpGet] public ActionResult<IEnumerable<string>> Get() { return new string[] { "value1", "value2" }; } } public class DiceHub : Hub { static ConcurrentDictionary<string, string> clientList = new ConcurrentDictionary<string, string>(); public override async Task OnConnectedAsync() { await Clients.Caller.SendAsync("GetConnectionId", this.Context.ConnectionId); } public async Task<bool> AddList(string userName, string connectionId) { var result = clientList.TryAdd(userName, connectionId); if (result) { await GetUser(userName); } return result; } public async Task GetUser(string userName) { if (clientList.Any(cl => cl.Key != userName)) { var player2 = clientList.First(cl => cl.Key != userName); var player1 = clientList.First(cl => cl.Key == userName); await Clients.Client(player1.Value).SendAsync("FetchUser", player2.Key, player2.Value); await Clients.Client(player2.Value).SendAsync("FetchUser", player1.Key, player1.Value); clientList.TryRemove(player1.Key, out _); clientList.TryRemove(player2.Key, out _); } } public async Task SendDice(string connectionID, string userName, int diceOne, int diceTwo) { await Clients.Client(connectionID).SendAsync("GetDice", userName, diceOne, diceTwo); } public override async Task OnDisconnectedAsync(Exception exception) { string connectionId = Context.ConnectionId; string userName = clientList.Where(entry => entry.Value == connectionId) .Select(entry => entry.Key).FirstOrDefault(); clientList.TryRemove(userName, out _); await base.OnDisconnectedAsync(exception); } } } |
Source Code: https://github.com/borakasmer/blazorsignalRDiceGame
Kaynaklar:















Merhaba Bora,
Blazor’un olgunlaştığında Angular, React, Vue gibi üçlü parti frameworkların sonunu getirebileceğini düşünüyormusun?
Yok düşünmüyorum. Çünkü Blazor’ın olayı, aslında daha farklı. En temel fark server side kodları client side tarafta yazabilmek.
Merhabalar,
yüksek frekanslı veri akışı olacak bir çok oyunculu oyun yapımında altyapı tercihi java-websocket tarafında mı olmalıdır yoksa signalr mı ?
dil kullanımı bakımından fark olmayacağını düşünürsek hangisi en az veri kaybı ve gecikme yaşatır ? signalr ın desteklediğini gördüğü browserlarda socket i yapıştırıp geçtiğini makalelerinizden öğrendim. Ancak shootr ı incelediğimde gecikmenin ne seviyede olduğunu gördüm. java tabanlı bir server oluşturup websocket ile bağlantı kurulan bir mimari gecikme konusunda daha faydalı olur mu?
Selamlar,
Para sorun değil dersen Azure SignalR service kullanmanı öneririm.
Yok hem ucuz hem hızlı olsun dersen “Go Socket” kullan.
https://github.com/googollee/go-socket.io
İyi çalışmalar.