ASP.NET Core版 対コンピュータ対戦できるぷよぷよをつくる(2)の続きです。今回はAspNetCore.SignalRにおける処理を実装します。
PuyoHubクラスの定義
1 2 3 4 5 6 7 8 9 10 |
namespace Puyo { using System.Collections.Generic; using Microsoft.AspNetCore.SignalR; using System.Timers; public class PuyoHub : Hub { } } |
以降は名前空間を省略して以下のように書きます。
1 2 3 |
public class PuyoHub : Hub { } |
静的なメンバーとして以下を宣言します。
1 2 3 4 5 6 7 |
public class PuyoHub : Hub { static Dictionary<string, IClientProxy> ClientProxyMap = new Dictionary<string, IClientProxy>(); static Dictionary<string, PuyoGame> Games = new Dictionary<string, PuyoGame>(); static System.Timers.Timer TimerForUpdate = new System.Timers.Timer(); static bool IsFirst = true; } |
接続されたときの処理
クライアントサイドからAspNetCore.SignalRで接続したときはContext.ConnectionIdをキーにしてClients.Callerを辞書に登録します。そしてPuyoGameオブジェクトを生成してこれもContext.ConnectionIdをキーにして辞書に登録します。これらは切断時に辞書から削除されます。これらの処理が滞りなく完了したときは”SuccessfulConnectionToClient”をクライアントサイドに送信して処理が正常に行なわれたことを通知します。
また初回実行時はタイマーの初期化をおこないます。PuyoGameオブジェクトの状態を取得してこれを1000 / 30ミリ秒ごとにクライアントサイドに送信します。
サーバーへの負荷を考慮して誰も接続していない場合はタイマーを停止させます。この処理によってClientProxyMap.Countが1になったときはタイマーが停止している状態なのでStartさせます。
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 |
public class PuyoHub : Hub { public override async Task OnConnectedAsync() { await base.OnConnectedAsync(); ClientProxyMap.Add(Context.ConnectionId, Clients.Caller); if (IsFirst) { IsFirst = false; TimerForUpdate.Interval = 1000 / 30; TimerForUpdate.Elapsed += TimerForUpdate_Elapsed; // イベントハンドラは後述 } PuyoGame game = new PuyoGame(Context.ConnectionId); game.SendNext += Game_SendNext; // イベントハンドラは後述 game.GameOvered += Game_GameOvered; game.StageCleared += Game_StageCleared; game.PlayerRensa += Game_PlayerRensa; game.CpuRensa += Game_CpuRensa; game.Downed += Game_Downed; game.Rotated += Game_Rotated; game.OjamaFalled += Game_OjamaFalled; game.Offseted += Game_Offseted; Games.Add(Context.ConnectionId, game); if (ClientProxyMap.Count == 1) TimerForUpdate.Start(); await Clients.Caller.SendAsync("SuccessfulConnectionToClient", "接続成功", Context.ConnectionId, Field.ROW_MAX, Field.COL_MAX); } } |
切断時の処理
切断されたときは辞書からContext.ConnectionIdをキーにするものを取り除きます。そしてこの処理によってClientProxyMap.Countが0になったときは誰もゲームをしていないということなのでタイマーを停止させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class PuyoHub : 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)) { PuyoGame game = Games[Context.ConnectionId]; game.StopTimerForPuyoDown(); // PuyoGameクラス内にも別のタイマーがあるので停止させる Games.Remove(Context.ConnectionId); } if (ClientProxyMap.Count == 0) TimerForUpdate.Stop(); try { System.GC.Collect(); } catch { Console.WriteLine("GC.Collect失敗"); } } } |
ゲームスタート時の処理
ユーザーがゲームを開始したときはGameStartメソッドが呼び出されるので、このときに対応するPuyoGameオブジェクトを探してPuyoGame.GameStartメソッドを実行します。またゲーム開始の処理が正常におこなわれたことを伝えるために”EventGameStartToClient”をクライアントサイドに送信します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class PuyoHub : Hub { public void GameStart(string id, string playerName) { if (!ClientProxyMap.ContainsKey(id) || !Games.ContainsKey(id)) return; Task.Run(async () => { await Games[id].GameStart(playerName); await ClientProxyMap[id].SendAsync("EventGameStartToClient"); }); } } |
キー操作時の処理
ユーザーがキー操作をしたときはそれに対応してPuyoGameクラスから適切なメソッドを呼び出します。
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 PuyoHub : Hub { public void SendKey(string key, string name) { // 長大なデータが送りつけられるかもしれないので対策 if (key.Length > 64 || name.Length > 64) return; if (!Games.ContainsKey(Context.ConnectionId)) return; PuyoGame game = Games[Context.ConnectionId]; game.PlayerName = name; if (key == "L") // 「Left」キーを押下された場合は落下中の組ぷよを左に移動(ただし可能な場合だけ) game.MoveLeft(); if (key == "R") // 「Right」キーを押下された場合は落下中の組ぷよを右に移動(ただし可能な場合だけ) game.MoveRight(); if (key == "D") // 「Down」キーを押下された場合は組ぷよを急速落下させる game.MoveDown(); if (key == "X") // 「X」キーを押下された場合は落下中の組ぷよを時計回りに回転(ただし可能な場合だけ) game.RotateRight(); if (key == "Z") // 「Z」キーを押下された場合は落下中の組ぷよを反時計回りに回転(ただし可能な場合だけ) game.RotateLeft(); } } |
イベント時に対応させる
PuyoGameクラスでイベント発生時にはクライアントサイドにこれを伝えます。GetClientメソッドは引数からこれに対応するIClientProxyを取得するためのものです。
1 2 3 4 5 6 7 8 9 10 |
public class PuyoHub : Hub { IClientProxy? GetClient(PuyoGame game) { if (Games.ContainsKey(game.ConnectionId) && ClientProxyMap.ContainsKey(game.ConnectionId)) return ClientProxyMap[game.ConnectionId]; else return null; } } |
回転が成功したときはクライアントサイドに”RotatedToClient”を、組ぷよを急速落下させたときは”DownedToClient”を送信します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class PuyoHub : Hub { private void Game_Rotated(PuyoGame game) { Task.Run(async () => { IClientProxy? client = GetClient(game); if (client != null) await client.SendAsync("RotatedToClient"); }); } private void Game_Downed(PuyoGame game) { Task.Run(async () => { IClientProxy? client = GetClient(game); if (client != null) await client.SendAsync("DownedToClient"); }); } } |
次の組ぷよが出現したときはクライアントサイドで次回と次々回に落ちてくる組ぷよの表示を変えないといけないので、クライアントサイドに”SendNextToClient”を送信します。
1 2 3 4 5 6 7 8 9 10 11 12 |
public class PuyoHub : Hub { private void Game_SendNext(PuyoGame game, bool isPlayer, string mainSub, string nextMainSub, string nextNextMainSub) { Task.Run(async () => { IClientProxy? client = GetClient(game); if (client != null) await client.SendAsync("SendNextToClient", isPlayer, mainSub, nextMainSub, nextNextMainSub); }); } } |
連鎖発生時は効果音を鳴らしたい(プレイヤーとCPUでは音も変えたい)ので”PlayerRensaToClient”と”CpuRensaToClient”を送信します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class PuyoHub : Hub { private void Game_PlayerRensa(PuyoGame game) { Task.Run(async () => { IClientProxy? client = GetClient(game); if (client != null) await client.SendAsync("PlayerRensaToClient"); }); } private void Game_CpuRensa(PuyoGame game) { Task.Run(async () => { IClientProxy? client = GetClient(game); if (client != null) await client.SendAsync("CpuRensaToClient"); }); } } |
おじゃまぷよが落ちてくるときも効果音を鳴らしたい(プレイヤー側とCPU側、大量落下時などで音を変えたい)ので、クライアントサイドに”OjamaFalledToClient”を送信します。また相殺発生時には”OffsetToClient”を送信します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class PuyoHub : Hub { private void Game_OjamaFalled(PuyoGame game, bool isPlayer, int ojamaCount) { Task.Run(async () => { IClientProxy? client = GetClient(game); if (client != null) await client.SendAsync("OjamaFalledToClient", isPlayer, ojamaCount); }); } private void Game_Offseted(PuyoGame game) { Task.Run(async () => { IClientProxy? client = GetClient(game); if (client != null) await client.SendAsync("OffsetToClient"); }); } } |
ステージクリア時とゲームオーバー時もクライアントサイドに”StageClearedToClient”または”GameOveredToClient”を送信します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class PuyoHub : Hub { private void Game_StageCleared(PuyoGame game, bool isPlayer) { Task.Run(async () => { IClientProxy? client = GetClient(game); if (client != null) await client.SendAsync("StageClearedToClient"); }); } private void Game_GameOvered(PuyoGame game, bool isPlayer) { Task.Run(async () => { IClientProxy? client = GetClient(game); if (client != null) await client.SendAsync("GameOveredToClient"); }); } } |
更新処理
Timer.Elapsedイベントが発生したら辞書内に存在するすべてのPuyoGameからその状態を取得して、これをそれらに対応するクライアントに送信します。送信するデータはプレイヤーとCPU側双方のフィールド上に固定されたぷよの位置と種類、落下中の組ぷよやおじゃまぷよ、スコアです。
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 |
public class PuyoHub : Hub { private void TimerForUpdate_Elapsed(object? sender, ElapsedEventArgs e) { foreach (PuyoGame game in Games.Values) { if (ClientProxyMap.ContainsKey(game.ConnectionId)) { Task.Run(async () => { IClientProxy client = ClientProxyMap[game.ConnectionId]; await client.SendAsync("BeginUpdatedToClient"); await client.SendAsync( "FixedPuyoUpdatedToClient", true, game.FixedPuyoRowsTexts[0], game.FixedPuyoColsTexts[0], game.FixedPuyoTypesTexts[0], game.FixedPuyoRensasTexts[0] ); await client.SendAsync( "FixedPuyoUpdatedToClient", false, game.FixedPuyoRowsTexts[1], game.FixedPuyoColsTexts[1], game.FixedPuyoTypesTexts[1], game.FixedPuyoRensasTexts[1] ); await client.SendAsync( "FallingPuyoUpdatedToClient", true, game.FallingPuyoRowsTexts[0], game.FallingPuyoColsTexts[0], game.FallingPuyoTypesTexts[0] ); await client.SendAsync( "FallingPuyoUpdatedToClient", false, game.FallingPuyoRowsTexts[1], game.FallingPuyoColsTexts[1], game.FallingPuyoTypesTexts[1] ); await client.SendAsync("ScoreUpdatedToClient", true, game.Scores[0], game.Fields[0].GetYokokuCount()); await client.SendAsync("ScoreUpdatedToClient", false, game.Scores[1], game.Fields[1].GetYokokuCount()); await client.SendAsync("EndUpdatedToClient"); }); } } } } |