ボスコニアンのようなオンライン対戦ゲームをつくる(3)の続きです。今回はリアルタイムのクライアントとサーバー間およびサーバーからクライアント間の通信を可能にするために必要な処理をおこなうHubクラスを定義します。
Contents
SignalR ハブを構成する
SignalR ハブで必要なサービスを登録するには、Program.cs で AddSignalR を呼び出します。
Program.cs
1 2 3 4 5 6 7 8 9 10 |
var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); builder.Services.AddSignalR(); var app = builder.Build(); app.UseStaticFiles(); app.UseRouting(); app.MapRazorPages(); app.MapHub<Bosconian.GameHub>("/BosconianHub"); app.Run(); |
次にBosconian.GameHubクラスを定義します。
GameHubクラスの定義
GameHubクラスを定義します。
1 2 3 4 5 6 7 8 9 10 |
using Microsoft.AspNetCore.SignalR; using System.Text.Json; using System.Timers; namespace Bosconian { public class GameHub : Hub { } } |
以降は名前空間の部分は省略して書きます。
1 2 3 |
public class GameHub : Hub { } |
静的フィールド変数として以下を定義します。
1 2 3 4 5 6 7 8 9 |
public class GameHub : Hub { static Game Game = new Game(); static bool IsFirstConnection = true; static System.Timers.Timer Timer = new System.Timers.Timer(); static System.Timers.Timer Timer2 = new System.Timers.Timer(); static Dictionary<string, IClientProxy> ClientProxyMap = new Dictionary<string, IClientProxy>(); } |
接続と切断のイベント処理
接続のイベント時におこなう処理を示します。
初回のみタイマーの初期化とGameオブジェクトにイベントハンドラを追加する処理をおこないます。そのあと辞書(ClientProxyMap)にContext.ConnectionIdをキーにしてIClientProxyを追加します。もし接続ユーザー数が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 |
public class GameHub : Hub { public override async Task OnConnectedAsync() { if (IsFirstConnection) { IsFirstConnection = false; Timer.Interval = 1000 / 30; Timer.Elapsed += Timer_Elapsed; Game.HitEnemy += OHitEnemy; Game.HitCore += OnHitCore; ; Game.PlayerDead += OnPlayerDead; Game.PlayerGameOvered += OnPlayerGameOvered; } await base.OnConnectedAsync(); ClientProxyMap.Add(Context.ConnectionId, Clients.Caller); if (ClientProxyMap.Count == 1) { Timer.Start(); Timer2.Start(); } await Clients.Caller.SendAsync("SendToClientConnectionSuccessful", Context.ConnectionId, Const.FIELD_SIZE); } } |
切断のイベント時におこなう処理を示します。
ClientProxyMapからContext.ConnectionIdをキーとするデータを削除します。もしプレイ中のユーザーが離脱したのであればPlayerオブジェクトのConnectionIdプロパティに空文字列をセットし、そのプレイヤーは切断されたものとして処理します。また接続ユーザー数が0になった場合はタイマーを停止させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class GameHub : Hub { public override async Task OnDisconnectedAsync(Exception? exception) { await base.OnDisconnectedAsync(exception); ClientProxyMap.Remove(Context.ConnectionId); Player? player = Game.Players.FirstOrDefault(player => player.ConnectionId == Context.ConnectionId); if (player != null) player.ConnectionId = ""; if (ClientProxyMap.Count == 0) Timer.Stop(); } } |
イベントハンドラの定義
追加したイベントハンドラを示します。
敵を倒したとき、要塞を破壊したとき、プレイヤーが死亡したとき、ゲームオーバーになったときはプレイヤーに対応するクライアントにイベントを送信します。
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 |
public class GameHub : Hub { private void OHitEnemy(object? sender, EventArgs e) { SendEventToClient(e, "HitEnemy"); } private void OnHitCore(object? sender, EventArgs e) { SendEventToClient(e, "HitCore"); } private void OnPlayerDead(object? sender, EventArgs e) { SendEventToClient(e, "PlayerDead"); } private void OnPlayerGameOvered(object? sender, EventArgs e) { SendEventToClient(e, "PlayerGameOvered"); } void SendEventToClient(EventArgs e, string eventName) { MyEventArgs myEventArgs = (MyEventArgs)e; // 対応するクライアントにイベントを送信する if (ClientProxyMap.ContainsKey(myEventArgs.ConnectionId)) ClientProxyMap[myEventArgs.ConnectionId].SendAsync("SendToClient" + eventName); } } |
更新処理
タイマーイベントが発生した場合は、プレイしているユーザーがいる場合のみ、更新処理をおこないます。
更新処理をおこなう場合はGame.Updateメソッドを実行したあと、Gameオブジェクトをjsonに変換して文字列をクライアントサイドに送信します。ゲームを開始していない場合やゲームオーバーになっているクライアントには送信しません。
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 |
public class GameHub : Hub { static private void Timer_Elapsed(object? sender, ElapsedEventArgs e) { Task.Run(async () => { try { if (!Game.Players.Any(player => player.ConnectionId != "") || ClientProxyMap.Count == 0) return; Game.Update(); JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = true }; string jsonString = System.Text.Json.JsonSerializer.Serialize(Game, options); foreach (string id in ClientProxyMap.Keys) { Player? player = Game.Players.FirstOrDefault(player => player.ConnectionId == id); if (player != null) await ClientProxyMap[id].SendAsync("SendToClientUpdate", jsonString); } } catch { } }); } } |
ユーザーがゲームを開始したときの処理
ユーザーがゲームを開始しようとしたときの処理を示します。
ユーザーがゲームを開始しようとしたときはGame.GetNewPlayerメソッドがnull以外を返すかどうか調べます。戻り値がnullでなければゲームに参加することができます。この場合はPlayer.GameStartメソッドを実行してクライアントサイドには”SendToClientGameStartSuccessful”を送信します。nullが返された場合は満員状態なので”SendToClientGameStartFailure”を送信します。
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 |
public class GameHub : Hub { public void GameStart(string playerName) { string connectionId = Context.ConnectionId; if (Game.Players.Any(player => player.ConnectionId == connectionId)) return; Player? player = Game.GetNewPlayer(); if (player != null) { player.GameStart(playerName, connectionId); Task.Run(async () => { if (ClientProxyMap.ContainsKey(connectionId)) await ClientProxyMap[connectionId].SendAsync("SendToClientGameStartSuccessful"); }); } else { Task.Run(async () => { if (ClientProxyMap.ContainsKey(connectionId)) await ClientProxyMap[connectionId].SendAsync("SendToClientGameStartFailure"); }); } } } |
ユーザーがプレイヤーを操作しようとしたときの処理
ユーザーがプレイヤーを操作しようとしたときの処理を示します。
移動方向を変更しようとしているときはChangeDirectメソッドが呼び出されます。ユーザーに対応するPlayerオブジェクトを取得してPlayer.ChangeDirectメソッドを呼び出します。弾丸を発射するときはPlayer.Shotメソッドを呼び出します。このときtrueが返されたときは発射の処理が行なわれたことを意味しています。この場合はクライアントサイドに”SendToClientShoted”を送信します。
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 |
public class GameHub : Hub { public void ChangeDirect(string direct, string playerName) { // 長大なデータが送りつけられるかもしれないので対策 if (direct.Length > 16) return; Player? player = Game.Players.FirstOrDefault(player => player.ConnectionId == Context.ConnectionId); if (player == null) return; player.Name = playerName; player.ChangeDirect(direct); } public void Shot(string playerName) { Player? player = Game.Players.FirstOrDefault(player => player.ConnectionId == Context.ConnectionId); if (player == null) return; player.Name = playerName; if (player.Shot()) { if (ClientProxyMap.ContainsKey(Context.ConnectionId)) ClientProxyMap[Context.ConnectionId].SendAsync("SendToClientShoted"); } } } |