Slither.io (スリザリオ)のようなオンラインゲームを作りたい(4)の続きです。今回はサーバーサイドとクライアントサイドでデータのやりとりをするためのGameHubクラスを定義します。
準備
Microsoft.AspNetCore.SignalR.Hubクラスを継承してGameHubクラスを定義します。
1 2 3 4 5 |
using Microsoft.AspNetCore.SignalR; public class GameHub : Hub { } |
VisualStudioのプロジェクト作成でASP.NET Core Webアプリを選択を選択するとコードが自動生成されるのですが、Program.csに以下のコードを追加します。追加するのは2行だけでそれ以外は既存のコードです。
Program.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); builder.Services.AddSignalR(); // これを追加 var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); } app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); app.MapHub<SnakeGame.GameHub>("/snake-game-hub"); // これを追加 app.Run(); |
接続時の処理
ハブ メソッドの各呼び出しは、新しいハブ インスタンスで実行されるため、変数はすべて静的変数として定義しています。
1 2 3 4 5 6 7 8 9 |
public class GameHub : Hub { static Game Game = new Game(); // 前述のGameクラスのインスタンス static bool IsFirstConnection = true; // OnConnectedAsync()がはじめて呼び出された static System.Timers.Timer Timer = new System.Timers.Timer(); // 更新処理用のタイマー static Dictionary<string, IClientProxy> DicClientProxy = new Dictionary<string, IClientProxy>(); // ASP.NET SignalRで使われる一意の接続IDからIClientProxyを求めるための辞書 static object syncGame = new object(); // 非同期処理でforeachを実行したときに例外が発生しないようにするための同期オブジェクト } |
はじめてユーザーが接続してきたときにタイマーの初期化をおこないます。Elapsedイベントが1秒間に60回発生するようにしてイベントハンドラを追加します。
そのあとキーをASP.NET SignalRで使われるの一意の接続ID、値をIClientProxyにして辞書に登録します。そのあとGame.AddPlayerAsDemoメソッドを呼び出してゲームにデモ画面表示用のプレイヤーとして追加します。これらの処理が滞りなく完了したらクライアントサイドにSendToClientConnectionSuccessfulイベントをフィールドの大きさとともに送信します。
またユーザーが接続したときに接続人数が1人のときはゲームの初期化と停止していたタイマーをスタートさせる処理も実行します。
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 |
public class GameHub : Hub { public override async Task OnConnectedAsync() { if (IsFirstConnection) { IsFirstConnection = false; Timer.Interval = 1000 / 60; Timer.Elapsed += Timer_Elapsed; Game.GameOveredEvent += OnPlayerGameOvered; Game.KillEvent += OnKillPlayer; } await base.OnConnectedAsync(); lock (syncGame) { DicClientProxy.Add(Context.ConnectionId, Clients.Caller); if (DicClientProxy.Count == 1) { Game.Init(); Timer.Start(); } _ = Game.AddPlayerAsDemo(Context.ConnectionId); } // 処理が成功したらフィールドの大きさとともに接続成功のイベントを送信する await Clients.Caller.SendAsync("SendToClientConnectionSuccessful", Context.ConnectionId, Constant.FIELD_RADUUS); } } |
イベントハンドラを追加する処理を示します。他のプレイヤーを倒したときはSendToClientKillPlayerイベントを、ゲームオーバーになったときはSendToClientGameOveredイベントを送信します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class GameHub : Hub { private void OnKillPlayer(object sender, string id) { if (DicClientProxy.ContainsKey(id)) { IClientProxy client = DicClientProxy[id]; client.SendAsync("SendToClientKillPlayer"); } } private void OnPlayerGameOvered(object sender, string id) { if (DicClientProxy.ContainsKey(id)) { IClientProxy client = DicClientProxy[id]; client.SendAsync("SendToClientGameOvered"); } } } |
切断時の処理
ユーザーがページから離脱したときにおこなわれる処理を示します。このときはContext.ConnectionIdを参照してASP.NET SignalRで使われる一意の接続IDを取得し、辞書からこのキーを削除します。またGameオブジェクト内にある辞書からも対応するPlayerを削除します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class GameHub : Hub { public override async Task OnDisconnectedAsync(Exception? exception) { await base.OnDisconnectedAsync(exception); lock (syncGame) { DicClientProxy.Remove(Context.ConnectionId); Game.RemovePlayer(Context.ConnectionId); if (DicClientProxy.Count == 0) // 誰も接続していないならタイマーを停止させる Timer.Stop(); } System.GC.Collect(); } } |
ゲーム開始時の処理
ユーザーがゲームを開始しようとした時におこなわれる処理を示します。
一度ゲームオーバーになっている場合は古いPlayerオブジェクトがそのままになっているので先にこれを取り除きます。その結果、プレイヤー数が0になった場合はGameオブジェクトを初期化します。
そのあと新しいPlayerを追加してクライアントサイドに自分の位置のデータを送信します。これらの処理が滞りなく完了したらSendToClientGameStartSuccessfulイベントを送信します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class GameHub : Hub { public async void GameStart(string playerName) { string myStatusText = ""; lock (syncGame) { string connectionId = Context.ConnectionId; Game.RemovePlayer(connectionId); if (Game.Players.Count(_ => _.ConnectionId != "") == 0) Game.Init(); Player? player = Game.AddPlayer(connectionId, playerName); myStatusText = Game.GetStringForUpdateMyStatus(player); } await Clients.Caller.SendAsync("SendToClientUpdateMyStatus", myStatusText); await Clients.Caller.SendAsync("SendToClientGameStartSuccessful"); } } |
旋回・ダッシュ時の処理
ユーザーが旋回やダッシュしようとしたときは対応するPlayerオブジェクトのフラグを変更させます。
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 |
public class GameHub : Hub { public void TurnLeft(bool b) { lock (syncGame) { Player? player = Game.GetPlayer(Context.ConnectionId); if (player == null) return; player.TurnLeft(b); } } public void TurnRight(bool b) { lock (syncGame) { Player? player = Game.GetPlayer(Context.ConnectionId); if (player == null) return; player.TurnRight(b); } } public void Dash(bool b) { lock (syncGame) { Player? player = Game.GetPlayer(Context.ConnectionId); if (player == null) return; player.Dash(b); } } } |
更新時の処理
更新時の処理を示します。
1 / 60秒おきにElapsedイベントが発生してイベントハンドラTimer_Elapsedが呼び出されます。このときは Game.Update()を呼び出してGameオブジェクトを更新して各クライアントに送信する文字列を取得して送信します。ただしフィールドの状態(プレイヤー数とNPC数)や各プレイヤーの順位情報の送信はデータ送信量を減らすために2秒に1回の割合でおこなうこととします。
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 |
public class GameHub : Hub { static long _countTimerElapsed = 0; static private void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) { try { lock (syncGame) { if (DicClientProxy.Count == 0) return; _countTimerElapsed++; Game.Update(); string gameText = ""; string playersStatusText = ""; if (_countTimerElapsed % 120 == 0) { gameText = Game.GetStringForUpdateFieldStatus(); playersStatusText = Game.GetStringForUpdatePlayersStatus(); } foreach (string id in DicClientProxy.Keys) { Player? player = Game.GetPlayer(id); if (player == null) continue; string playerText = Game.GetStringForUpdateMyStatus(player); _ = DicClientProxy[id].SendAsync("SendToClientUpdateMyStatus", playerText); string addCirclesText = ""; string removeCirclesText = ""; Game.GetStringForUpdateCircles(player, ref addCirclesText, ref removeCirclesText); _ = DicClientProxy[id].SendAsync("SendToClientUpdateCircles", addCirclesText, removeCirclesText); if (_countTimerElapsed % 120 == 0) { _ = DicClientProxy[id].SendAsync("SendToClientUpdateFieldStatus", gameText); _ = DicClientProxy[id].SendAsync("SendToClientUpdatePlayersStatus", playersStatusText); } } } } catch { Console.WriteLine("クライアントにデータ送信中に例外発生"); } } } |
次回はラスト。クライアントサイドの処理です。
Slither.io (スリザリオ)のようなオンラインゲームを作りたい(4)の続きです。今回はサーバーサイドとクライアントサイドでデータのやりとりをするためのGameHubクラスを定義します。