ASP.NET Core版 デジタルインベーダーをつくる(1)の続きです。
Contents
DigitalInvaderHubクラスの定義
1 2 3 4 5 6 7 8 |
using Microsoft.AspNetCore.SignalR; namespace DigitalInvader { public class DigitalInvaderHub : Hub { } } |
以降は名前空間を省略して以下のように書きます。
1 2 3 |
public class DigitalInvaderHub : Hub { } |
接続時の処理
OnConnectedAsyncメソッドが初めて実行されたときはタイマーの初期化もおこないます。配列DigitalInvaderGame.Intervalsから値を取得してIntervalプロパティが異なるタイマーを配列の要素数個だけつくります。
以降は共通の処理です。Context.ConnectionIdをキーにしてClients.CallerとDigitalInvaderGameクラスのインスタンスを辞書に登録します。またDigitalInvaderGameクラスにおけるイベントに対応できるようにイベントハンドラを追加します。
ClientProxyMap.Countが0のときはサーバーへの負荷を考えてタイマーを停止させることにしています。もしClientProxyMap.Countが1になった場合は停止しているタイマーをスタートさせなければならないのでその処理を再開させます。
最後にクライアントサイドに接続に成功したことを通知するために”SuccessfulConnectionToClient”を送信します。
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 DigitalInvaderHub : Hub { static bool IsFirstConnection = true; static List<System.Timers.Timer> Timers = new List<System.Timers.Timer>(); static Dictionary<string, IClientProxy> ClientProxyMap = new Dictionary<string, IClientProxy>(); static Dictionary<string, DigitalInvaderGame> Games = new Dictionary<string, DigitalInvaderGame>(); public override async Task OnConnectedAsync() { if (IsFirstConnection) { IsFirstConnection = false; // 初回実行時だけ処理をする foreach (int interval in DigitalInvaderGame.Intervals) { System.Timers.Timer timer = new System.Timers.Timer(); timer.Interval = interval; timer.Elapsed += Timer_Elapsed; Timers.Add(timer); } } await base.OnConnectedAsync(); ClientProxyMap.Add(Context.ConnectionId, Clients.Caller); DigitalInvaderGame game = new DigitalInvaderGame(Context.ConnectionId); game.SendEnemiesEvent += Game_SendEnemiesEvent; // イベントハンドラを追加(定義は後述) game.ChangeTargetEvent += Game_ChangeTargetEvent; ; game.HitEvent += Game_HitEvent; game.HitUfoEvent += Game_HitUfoEvent; game.StageClearEvent += Game_StageClearEvent; game.ChangeScoreEvent += Game_ChangeScoreEvent; game.ChangeStageEvent += Game_ChangeStageEvent; game.ChangeLifeEvent += Game_ChangeLifeEvent; game.MissEvent += Game_MissEvent; game.GameOverEvent += Game_GameOverEvent; Games.Add(Context.ConnectionId, game); if (ClientProxyMap.Count == 1) { foreach (System.Timers.Timer timer in Timers) timer.Start(); } await Clients.Caller.SendAsync("SuccessfulConnectionToClient", "接続成功", Context.ConnectionId); } } |
切断時の処理
通信が切断されたときはClientProxyMapとGamesからキーを削除します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class DigitalInvaderHub : 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)) Games.Remove(Context.ConnectionId); if (ClientProxyMap.Count == 0) { foreach (System.Timers.Timer timer in Timers) timer.Stop(); } try { System.GC.Collect(); } catch { Console.WriteLine("GC.Collect失敗"); } } } |
各イベントハンドラについて
DigitalInvaderGameクラス内イベント発生時の処理を示します。いずれもクライアントサイドに通知しているだけです。
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 |
public class DigitalInvaderHub : Hub { // インベーダーが前進した。または撃墜されて状態が変わった。 private void Game_SendEnemiesEvent(DigitalInvaderGame game, string text) { if (ClientProxyMap.ContainsKey(game.ConnectionId)) { Task.Run(async () => { await ClientProxyMap[game.ConnectionId].SendAsync("SendEnemiesEventToClient", text); }); } } // ユーザーの処理によってターゲットが変更された private void Game_ChangeTargetEvent(DigitalInvaderGame game, string target) { if (ClientProxyMap.ContainsKey(game.ConnectionId)) { Task.Run(async () => { await ClientProxyMap[game.ConnectionId].SendAsync("ChangeTargetEventToClient", target); }); } } // UFOを撃墜した private void Game_HitUfoEvent(DigitalInvaderGame game) { if (ClientProxyMap.ContainsKey(game.ConnectionId)) { Task.Run(async () => { await ClientProxyMap[game.ConnectionId].SendAsync("HitUfoEventToClient"); }); } } // 数字のインベーダーを撃墜した private void Game_HitEvent(DigitalInvaderGame game) { if (ClientProxyMap.ContainsKey(game.ConnectionId)) { Task.Run(async () => { await ClientProxyMap[game.ConnectionId].SendAsync("HitEventToClient"); }); } } // ステージをクリアした private void Game_StageClearEvent(DigitalInvaderGame game) { if (ClientProxyMap.ContainsKey(game.ConnectionId)) { Task.Run(async () => { await ClientProxyMap[game.ConnectionId].SendAsync("StageClearEventToClient"); }); } } // スコアが変更された private void Game_ChangeScoreEvent(DigitalInvaderGame game, int score) { if (ClientProxyMap.ContainsKey(game.ConnectionId)) { Task.Run(async () => { await ClientProxyMap[game.ConnectionId].SendAsync("ChangeScoreEventToClient", score); }); } } // ステージが変更された private void Game_ChangeStageEvent(DigitalInvaderGame game, int stage) { if (ClientProxyMap.ContainsKey(game.ConnectionId)) { Task.Run(async () => { await ClientProxyMap[game.ConnectionId].SendAsync("ChangeStageEventToClient", stage); }); } } // Lifeが変更された private void Game_ChangeLifeEvent(DigitalInvaderGame game, int life) { if (ClientProxyMap.ContainsKey(game.ConnectionId)) { Task.Run(async () => { await ClientProxyMap[game.ConnectionId].SendAsync("ChangeLifeEventToClient", life); }); } } // プレイヤーがミスをした private void Game_MissEvent(DigitalInvaderGame game) { if (ClientProxyMap.ContainsKey(game.ConnectionId)) { Task.Run(async () => { await ClientProxyMap[game.ConnectionId].SendAsync("MissEventToClient"); }); } } // ゲームオーバー時の処理は後述 } |
ゲームスタート時の処理
ゲームをスタートするときに呼び出されるGameStartメソッドを示します。
辞書のなかにキー(引数id)に対応するDigitalInvaderGameを取得して、DigitalInvaderGame.GameStartメソッドを呼び出します。それと同時にgame.Nameプロパティにプレイヤー名をセットします。このとき名前にカンマが入っているとスコアランキングへの登録の処理で支障がでるので別の文字(”_”)に置き換えています。
処理が正常におこなわれたらクライアントサイドに”EventGameStartToClient”を送信します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class DigitalInvaderHub : Hub { public void GameStart(string id, string playerName) { if (!ClientProxyMap.ContainsKey(id) || !Games.ContainsKey(id)) return; DigitalInvaderGame game = Games[id]; string name = playerName.Length > 16 ? playerName.Substring(0, 16) : playerName; game.Name = name.Replace(",", "_"); Games[id].GameStart(); Task.Run(async () => { await ClientProxyMap[id].SendAsync("EventGameStartToClient"); }); } } |
インベーダーを前進させる処理
Timer.Elapsedイベントが発生したらインベーダーを前進させる処理を行なうのですが、ステージによって対応するタイマーが違います。そこでDigitalInvaderGame.Stageステージの値とDigitalInvaderGame.Intervalsから適切なintervalを取得します。そのあとsenderからTimerを取得してIntervalプロパティが同じ場合だけDigitalInvaderGame.Updateメソッドを呼び出して更新処理をおこないます。
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 DigitalInvaderHub : Hub { static private void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) { Task.Run(() => { foreach (DigitalInvaderGame game in Games.Values) { int interval = DigitalInvaderGame.Intervals.First(); if (game.Stage == 0) // game.Stage == 0のときはゲームが始まっていないので何もしない continue; else if (game.Stage - 1 >= DigitalInvaderGame.Intervals.Length) interval = DigitalInvaderGame.Intervals.Last(); else interval = DigitalInvaderGame.Intervals[game.Stage - 1]; System.Timers.Timer? timer = (System.Timers.Timer?)sender; if (timer != null && timer.Interval == interval) game.Update(); } }); } } |
ゲームオーバー時の処理
ゲームオーバーになったらイベントハンドラGame_GameOverEventが呼び出されます。
ここではDigitalInvaderGameオブジェクトからNameプロパティとScoreプロパティを抜き取ってスコアランキングに登録する処理をおこなっています。そのあとクライアントサイドに”GameOverEventToClient”を送信しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class DigitalInvaderHub : Hub { private void Game_GameOverEvent(DigitalInvaderGame game) { HiscoreManager.Save("../hiscore-digitalInvader.txt", game.Name, game.Score); if (ClientProxyMap.ContainsKey(game.ConnectionId)) { Task.Run(async () => { await ClientProxyMap[game.ConnectionId].SendAsync("GameOverEventToClient"); }); } } } |
キーが押下されたときの処理
ユーザーがキーを押下するたびにDownKeyメソッドが呼び出されます。このとき引数として渡された文字列が長すぎる場合は処理をおこないません(プレイヤー名は16文字までに制限しているのでスパム的なことをしない限り、ここで弾かれることはない)。
プレイヤー名のなかにカンマがある場合はスコアランキングへの登録で支障がでるので別の文字に置き換えています。そのあとDigitalInvaderGame.Nameプロパティにセットしています。そのあと第一引数を調べて”Z”であればターゲットの変更、”X”であればインベーダーを攻撃する処理をおこなわせています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class DigitalInvaderHub : Hub { public void DownKey(string key, string name) { // 長大なデータが送りつけられるかもしれないので対策 if (key.Length > 16 || name.Length > 16) return; if (!Games.ContainsKey(Context.ConnectionId)) return; DigitalInvaderGame game = Games[Context.ConnectionId]; game.Name = name.Replace(",", "_"); key = key.ToUpper(); if (key == "Z") game.ChangeTarget(); if (key == "X") game.Shot(); } } |