ASP.NET Core版 タイピングゲームをつくる(1)の続きです。今回はAspNetCore.SignalRにおける処理を実装します。
TypingHubクラスの定義
| 1 2 3 4 5 6 7 8 9 10 | using System.Collections.Generic; using Microsoft.AspNetCore.SignalR; using System.Timers; namespace Typing {     public class TypingHub : Hub     {     } } | 
以降は名前空間を省略して以下のように書きます。
| 1 2 3 | public class TypingHub : Hub { } | 
接続時の処理
最初に接続時の処理を示します。辞書にContext.ConnectionId, Clients.CallerとTypingGameのインスタンスを登録します。これはこれまでやってきたASP.NET Core版アプリと同じです。
TypingGameのインスタンスを生成したらイベント発生時に対する処理ができるようにイベントハンドラを追加します。
| 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 | public class TypingHub : Hub {     static Dictionary<string, IClientProxy> ClientProxyMap = new Dictionary<string, IClientProxy>();     static Dictionary<string, TypingGame> Games = new Dictionary<string, TypingGame>();     public override async Task OnConnectedAsync()     {         await base.OnConnectedAsync();         // 辞書にClients.Callerを登録         ClientProxyMap.Add(Context.ConnectionId, Clients.Caller);         // TypingGameのインスタンスを生成しイベントハンドラを追加         TypingGame game = new TypingGame(Context.ConnectionId);         game.CorrectAnswerEvent += Game_CorrectAnswerEvent;         game.IncorrectAnswerEvent += Game_IncorrectAnswerEvent;         game.SendQuestionEvent += Game_SendQuestionEvent;         game.UpdateEvent += Game_UpdateEvent;         game.MissEvent += Game_MissEvent;         game.ResumeEvent += Game_ResumeEvent;         game.GameOverEvent += Game_GameOverEvent; ;         Games.Add(Context.ConnectionId, game);         await Clients.Caller.SendAsync("SuccessfulConnectionToClient", "接続成功", Context.ConnectionId);     } } | 
切断時の処理
切断時の処理を示します。このときはClientProxyMapとGamesからContext.ConnectionIdをキーとする要素を削除します。また削除されたTypingGameオブジェクトのStopGameメソッドを呼び出し更新処理が行なわれないようにします。また切断処理によって接続されているユーザー数が0になったらTypingGame.StopTimerメソッドを呼び出してTypingGameクラス内にあるタイマーを停止します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class TypingHub : Hub {     public override async Task OnDisconnectedAsync(Exception? exception)     {         await base.OnDisconnectedAsync(exception);         if (ClientProxyMap.ContainsKey(Context.ConnectionId))             ClientProxyMap.Remove(Context.ConnectionId);         if (Games.ContainsKey(Context.ConnectionId))         {             TypingGame game = Games[Context.ConnectionId];             game.StopGame();             Games.Remove(Context.ConnectionId);             if (Games.Count == 0)                 game.StopTimer();         }         try { System.GC.Collect(); } catch { Console.WriteLine("GC.Collect失敗"); }     } } | 
クライアントサイドでゲーム開始の処理が行なわれたときはGameStartメソッドが呼び出されます。この処理が成功したときはクライアントサイドに”EventGameStartToClient”を送信します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | public class TypingHub : Hub {     public void GameStart(string id, string playerName)     {         if (!ClientProxyMap.ContainsKey(id) || !Games.ContainsKey(id))             return;         Task.Run(async () => {             Games[id].GameStart();             await ClientProxyMap[id].SendAsync("EventGameStartToClient");         });     } } | 
イベントハンドラ
追加されたイベントハンドラに解説します。
クライアントサイドからサーバーサイドに送られてきた文字列がお題の答えとして正しければ”CorrectAnswerEventToClient”を、間違っていれば”IncorrectAnswerEventToClient”をクライアントサイドに送信します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class TypingHub : Hub {     private void Game_CorrectAnswerEvent(TypingGame game)     {         Task.Run(async () => {             await ClientProxyMap[game.ConnectionId].SendAsync("CorrectAnswerEventToClient");         });     }     private void Game_IncorrectAnswerEvent(TypingGame game)     {         Task.Run(async () => {             await ClientProxyMap[game.ConnectionId].SendAsync("IncorrectAnswerEventToClient");         });     } } | 
クライアントサイドからサーバーサイドに送られてきた文字列がお題の答えとして正しければ、TypingGameクラス内で新しいお題がイベントとして送られてきます。この場合はクライアントサイドにお題、制限時間、残り時間mスコア、ライフの引数とともに”SendQuestionToClient”を送信します。またタイマーによって残り時間が変動したときは、クライアントサイドに同引数とともに”UpdateEventToClient”を送信します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public class TypingHub : Hub {     private void Game_SendQuestionEvent(TypingGame game)     {         Task.Run(async () => {             await ClientProxyMap[game.ConnectionId].SendAsync(                 "SendQuestionToClient",                 game.Question, game.TimeLimitMax, game.TimeLimitMax, game.Score, game.Life);         });     }     private void Game_UpdateEvent(TypingGame game)     {         Task.Run(async () => {             await ClientProxyMap[game.ConnectionId].SendAsync(                 "UpdateEventToClient",                 game.Question, game.TimeLimit, game.TimeLimitMax, game.Score, game.Life);         });     } } | 
ミスをしたとき、ミスに伴うゲーム中断から復帰するときは、それぞれクライアントサイドに”MissEventToClient”と”ResumeEventToClient”が送信されます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class TypingHub : Hub {     private void Game_MissEvent(TypingGame game)     {         Task.Run(async () => {             await ClientProxyMap[game.ConnectionId].SendAsync("MissEventToClient");         });     }     private void Game_ResumeEvent(TypingGame game)     {         Task.Run(async () => {             await ClientProxyMap[game.ConnectionId].SendAsync("ResumeEventToClient");         });     } } | 
クライアントサイドからお題の答えを受信する
クライアントサイドからお題の答えが送信された場合は、その文字列を引数にしてTypingGame.CheckAnswerメソッドに渡します。するとTypingGameクラス内で正誤判定が行なわれ、CorrectAnswerEventイベントまたはIncorrectAnswerEventイベントが発生し、上記のイベントハンドラの処理が実行されます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public class TypingHub : Hub {     // お題の答えだけでなくゲームしている最中プレイヤー名が変更されるかもしれないので対応できるようにする     public void SendStringToServer(string str, string name)     {         // 長大なデータが送りつけられるかもしれないので対策         if (str.Length > 64)             return;         if (!Games.ContainsKey(Context.ConnectionId))             return;         if (!ClientProxyMap.ContainsKey(Context.ConnectionId))             return;         var game = Games[Context.ConnectionId];         game.Name = name;         game.CheckAnswer(str);     } } | 
Program.csを編集します。
Program.cs
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); builder.Services.AddSignalR(); var app = builder.Build(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); app.MapHub<Typing.TypingHub>("/TypingHub"); // この行を追加 app.Run(); | 
