前回 ASP.NET Core版 戦車対戦ゲームをつくる(2)の続きです。
Contents
TankHubクラスを定義する
1 2 3 4 5 6 7 8 9 10 |
using Microsoft.AspNetCore.SignalR; using TankGame; using System.Timers; namespace SignalRChat.Hubs { public class TankHub : Hub { } } |
以降は名前空間を省略して書きます。
1 2 3 |
public class TankHub : Hub { } |
まずフィールド変数を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class TankHub : Hub { static bool IsFirstConnection = true; static System.Timers.Timer Timer = new System.Timers.Timer(); static Dictionary<string, IClientProxy> ClientProxyMap = new Dictionary<string, IClientProxy>(); static Dictionary<string, HubCallerContext> HubCallerContexts = new Dictionary<string, HubCallerContext>(); static List<Player> NPCs = new List<Player>(); static Dictionary<string, Player> Players = new Dictionary<string, Player>(); const int PLAYER_MAX = 7; } |
接続されたときの処理
接続されたときの処理を示します。
プレイヤー数の上限は7なので最初に7つのPlayerオブジェクトをNPCとして生成します。そして壁が破壊されたとき、砲弾を発射したとき、砲弾が命中したとき、ゲームオーバーになったときのイベントハンドラを追加します。
接続したときすでにプレイしているユーザーが7人存在する場合はゲームに参加することはできません。この場合は別のページにリダイレクトさせて今はゲームに参加できないことを伝えます。
プレイしているユーザーが6人以下の場合はNPCのうちひとつをNPCのリストから取り除き、プレイヤーとして追加します。そのあとクライアントサイドで描画処理ができるように存在する壁の座標をクライアントサイドに送信します。
すべてのユーザーが離脱して新たに一人目のユーザーが接続した場合は破壊された壁をもとに戻してゲームを新たに開始します。
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 |
public class TankHub : Hub { public override async Task OnConnectedAsync() { if (IsFirstConnection) { // 初回のみ実行する処理 IsFirstConnection = false; Timer.Interval = 1000 / 24; Timer.Elapsed += Timer_Elapsed; Game.Init(); // 最初はNPCとして生成する for (int i = 0; i < PLAYER_MAX; i++) { Player npc = new Player(""); npc.BrokenWall += Player_BrokenWall; // イベントハンドラ(後述) npc.ShotEvent += Player_ShotEvent; npc.BombEvent += Player_BombEvent; npc.GameOverEvent += Player_GameOverEvent; NPCs.Add(npc); } } await Clients.Caller.SendAsync("ReceiveConnected", "接続成功", Context.ConnectionId); await base.OnConnectedAsync(); ClientProxyMap.Add(Context.ConnectionId, Clients.Caller); if (!IsFirstConnection && ClientProxyMap.Count == 1) { // すべてのユーザーが離脱して新たに一人目のユーザーが接続した場合 // すべてをリセットしてゲーム開始 Game.Init(); foreach (Player npc in NPCs) npc.Reset(""); Timer.Start(); } if (PLAYER_MAX < NPCs.Count + Players.Count + 1 && NPCs.Count == 0) { // 定員オーバー // ゲームに参加できない await ClientProxyMap[Context.ConnectionId].SendAsync("ReceiveDenyNewPlayer"); return; } HubCallerContexts.Add(Context.ConnectionId, Context); if (NPCs.Count > 0) { // 新しいユーザーがゲームに参加するときはNPCのうちひとつをNPCsリストから取り除き // Players辞書に追加する Player player = NPCs[0]; NPCs.RemoveAt(0); Players.Add(Context.ConnectionId, player); player.Reset(Context.ConnectionId); player.GameStart(); await Clients.Caller.SendAsync("ReceiveGameStart"); // 破壊可能な壁のうち存在する壁のみクライアントサイドに送信する string xs1 = String.Join(",", Game.ExistingWalls.Select(wall => wall.X.ToString()).ToArray()); string zs1 = String.Join(",", Game.ExistingWalls.Select(wall => wall.Z.ToString()).ToArray()); await Clients.Caller.SendAsync("ReceiveExistingWalls", xs1, zs1); // 破壊できない壁をクライアントサイドに送信する string xs2 = String.Join(",", Game.IndestructibleWalls.Select(wall => wall.X.ToString()).ToArray()); string zs2 = String.Join(",", Game.IndestructibleWalls.Select(wall => wall.Z.ToString()).ToArray()); await Clients.Caller.SendAsync("ReceiveIndestructibleWalls", xs2, zs2); } } } |
切断されたときの処理
切断されたときの処理を示します。
切断されたときはPlayers辞書から切断されたプレイヤーに対応する要素を取り除き、NPCのリストに追加します。またクライアントサイドにデータを送信する必要はなくなるのでClientProxyMapとHubCallerContextsからも要素を取り除きます。
切断された結果、接続されているユーザーが0になった場合はタイマーを停止させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class TankHub : Hub { public override async Task OnDisconnectedAsync(Exception? exception) { await base.OnDisconnectedAsync(exception); if (Players.ContainsKey(Context.ConnectionId)) { Player player = Players[Context.ConnectionId]; NPCs.Add(player); player.Reset(""); } ClientProxyMap.Remove(Context.ConnectionId); Players.Remove(Context.ConnectionId); HubCallerContexts.Remove(Context.ConnectionId); if(ClientProxyMap.Count == 0) Timer.Stop(); } } |
ゲームオーバーになったときは強制的に切断します。以下はconnectionIDを指定してAspNetCore.SignalRによる通信を切断するメソッドです。
1 2 3 4 5 6 7 8 9 10 |
public class TankHub : Hub { public void Disconnect(string connectionID) { if (HubCallerContexts.ContainsKey(connectionID)) { HubCallerContexts[connectionID].Abort(); } } } |
タイマーイベント発生時の処理
タイマーイベントが発生したときの処理を示します。プレイヤーとNPCの更新処理をおこなったあと当たり判定の処理をおこないます。そのあとプレイヤーとNPCの状態をクライアントサイドに送信します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class TankHub : Hub { static private void Timer_Elapsed(object? sender, ElapsedEventArgs e) { Task.Run(async () => { List<Player> players = Players.Select(_ => _.Value).ToList(); // プレイヤーとNPCの更新処理 foreach (Player player in players) player.Update(); foreach (Player player in NPCs) player.Update(); // 当たり判定 await HitCheck(players, NPCs); // 後述 // プレイヤーとNPCの状態をクライアントサイドに送信 foreach (Player player in players) await SendUpdateToClient(player.ConnectionID); // 後述 }); } } |
当たり判定の処理
当たり判定の処理を示します。
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 |
public class TankHub : Hub { static async Task HitCheck(List<Player> players, List<Player> npcs) { List<Player> allPlayer = new List<Player>(); allPlayer.AddRange(players); allPlayer.AddRange(npcs); // 砲弾同士の当たり判定 for (int i = 0; i < allPlayer.Count; i++) { Player player = allPlayer[i]; Bullet? bullet = player.Bullet; if (bullet == null) continue; List<Player> rivalPlayers = allPlayer.Where(_ => _ != player && !_.Dead).ToList(); for (int k = 0; k < rivalPlayers.Count; k++) { Bullet? rivalBullet = rivalPlayers[k].Bullet; if (rivalBullet == null) continue; if (Math.Pow(bullet.X - rivalBullet.X, 2) + Math.Pow(bullet.Z - rivalBullet.Z, 2) < Math.Pow(Game.CHARACTER_SIZE, 2)) { // 砲弾同士が衝突している場合は両方とも消滅させる rivalPlayers[k].Bullet = null; player.Bullet = null; break; } } } // 戦車と砲弾の当たり判定 List<Position> defeatedPositions = new List<Position>(); List<Player> defeatEnemyPlayers = new List<Player>(); List<Player> defeatedByEnemyPlayers = new List<Player>(); for (int i=0; i < allPlayer.Count; i++) { Player player = allPlayer[i]; Bullet? bullet = player.Bullet; if(bullet == null) continue; List<Player> rivalPlayers = allPlayer.Where(_ => _ != player && !_.Dead).ToList(); Player? DefeatedPlayer = rivalPlayers.FirstOrDefault(rival => ( !rival.Dead && bullet.X < rival.X + Game.CHARACTER_SIZE && bullet.X > rival.X - Game.CHARACTER_SIZE && bullet.Z < rival.Z + Game.CHARACTER_SIZE && bullet.Z > rival.Z - Game.CHARACTER_SIZE) ); // 命中したと判定されたときは結果を保存 if (DefeatedPlayer != null) { DefeatedPlayer.Dead = true; player.Bullet = null; defeatEnemyPlayers.Add(player); defeatedByEnemyPlayers.Add(DefeatedPlayer); defeatedPositions.Add(new Position(DefeatedPlayer.X, DefeatedPlayer.Z)); } } // 撃破したプレイヤーと撃破されたプレイヤーをクライアントサイドに送信する foreach (Player player in players) { if (player.ConnectionID == "") continue; for (int i = 0; i < defeatEnemyPlayers.Count; i++) { defeatEnemyPlayers[i].Score += 100; await ClientProxyMap[player.ConnectionID].SendAsync( "ReceiveDefeated", defeatEnemyPlayers[i].ConnectionID, defeatEnemyPlayers[i].Name, defeatedByEnemyPlayers[i].ConnectionID, defeatedByEnemyPlayers[i].Name, defeatedByEnemyPlayers[i].X, defeatedByEnemyPlayers[i].Z); } } } } |
クライアントサイドへのデータ送信
クライアントサイドへのデータを送信する処理を示します。
ReceiveStartUpdateを送信し、そのあと戦車の位置、砲弾の位置、非表示にすべき壁、スコア、残機数を送信します。これはクライアントサイドでうけとり一旦配列内に格納します。必要なデータをすべて送信したら最後にReceiveEndUpdateを送信してクライアントサイドで描画処理をおこなわせます。
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 |
public class TankHub : Hub { static async Task SendUpdateToClient(string id) { if (!ClientProxyMap.ContainsKey(id)) return; // クライアントサイドへのデータ送信開始 await ClientProxyMap[id].SendAsync("ReceiveStartUpdate"); foreach (KeyValuePair<string, Player> pair in Players) { Player player = pair.Value; double x = player.X; double z = player.Z; double ry = player.RotationY; // 戦車の位置をクライアントサイドに送信する await ClientProxyMap[id].SendAsync( "ReceiveUpdateTank", pair.Key, x, z, ry, player.Name, player.Dead ); // 戦車から発射された砲弾の位置をクライアントサイドに送信する if (player.Bullet != null) { double bx = player.Bullet.X; double bz = player.Bullet.Z; await ClientProxyMap[id].SendAsync( "ReceiveUpdateBullet", bx, bz ); } } if (Players.ContainsKey(id)) { string[] xs = Players[id].WallsToHide.Select(wall => wall.X.ToString()).ToArray(); string[] zs = Players[id].WallsToHide.Select(wall => wall.Z.ToString()).ToArray(); await ClientProxyMap[id].SendAsync("ReceiveHideWalls", id, String.Join(",", xs), String.Join(",", zs)); await ClientProxyMap[id].SendAsync("ReceiveScore", id, Players[id].Score, Players[id].Rest); } //NPCs foreach (Player player in NPCs) { double x = player.X; double z = player.Z; double ry = player.RotationY; // 戦車の位置をクライアントサイドに送信する await ClientProxyMap[id].SendAsync( "ReceiveUpdateTank", "", x, z, ry, player.Name, player.Dead ); // 戦車から発射された弾丸の位置をクライアントサイドに送信する if (player.Bullet != null) { double bx = player.Bullet.X; double bz = player.Bullet.Z; await ClientProxyMap[id].SendAsync( "ReceiveUpdateBullet", bx, bz ); } } // クライアントサイドへのデータ送信終了 await ClientProxyMap[id].SendAsync("ReceiveEndUpdate"); } } |
イベントハンドラ
砲弾を発射したときと命中したときのイベントハンドラを示します。
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 |
public class TankHub : Hub { // 砲弾を発射したとき private void Player_ShotEvent(Player player) { Task.Run(async () => { string id = player.ConnectionID; if (ClientProxyMap.ContainsKey(id)) await ClientProxyMap[id].SendAsync("ReceiveShot", id); }); } // 命中したとき private void Player_BombEvent(Player player) { Task.Run(async () => { string id = player.ConnectionID; if(ClientProxyMap.ContainsKey(id)) await ClientProxyMap[id].SendAsync("ReceiveBomb", id); }); } // 破壊可能な壁に命中したとき(該当する壁を非表示にしなければならないので座標も送信する) private void Player_BombEvent(Player player) { Task.Run(async () => { string id = player.ConnectionID; if(ClientProxyMap.ContainsKey(id)) await ClientProxyMap[id].SendAsync("ReceiveBomb", id); }); } } |
ゲームオーバー時の処理
ゲームオーバーになったときの処理を示します。
クライアントサイドにゲームオーバーになったことを送信し、通信を切断します。それと同時にスコアランキングへの登録の処理もおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class TankHub : Hub { private void Player_GameOverEvent(Player player) { Task.Run(async () => { string id = player.ConnectionID; if (ClientProxyMap.ContainsKey(id)) { await ClientProxyMap[id].SendAsync("ReceiveGameOver", id); SaveHiscore(player); Disconnect(id); } }); } } |
スコアランキングへの登録
スコアランキングに登録する処理を示します。
まずスコアを管理するためのクラスを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
namespace Zero.Pages.Tank { public class Hiscore { public Hiscore(string name, long score, string time) { Name = name; Score = score; Time = time; } public string Name = ""; public long Score = 0; public string Time = ""; } } |
次にスコアランキングに登録する処理を示します。ゲームオーバー時のスコアが上位30以内にはいっているかどうかを調べて入っている場合はプレイヤー名とスコア、そのときの時刻を登録します。
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 |
public class TankHub : Hub { void SaveHiscore(Player player) { string path = "../hiscore-tank.txt"; List<Zero.Pages.Tank.Hiscore> hiscores = new List<Zero.Pages.Tank.Hiscore>(); if (System.IO.File.Exists(path)) { System.IO.StreamReader sr = new StreamReader(path); string text = sr.ReadToEnd(); string[] vs1 = text.Split('\n'); foreach (string str in vs1) { try { string[] vs2 = str.Split(','); Zero.Pages.Tank.Hiscore hiscore = new Zero.Pages.Tank.Hiscore(vs2[0], long.Parse(vs2[1]), vs2[2]); hiscores.Add(hiscore); } catch { } } sr.Close(); } DateTime now = DateTime.Now; string time = String.Format("{0}-{1:00}-{2:00} {3:00}:{4:00}:{5:00}", now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second); hiscores.Add(new Zero.Pages.Tank.Hiscore(player.Name, player.Score, time)); hiscores = hiscores.OrderByDescending(x => x.Score).ToList(); if(hiscores.Count > 30) hiscores = hiscores.Take(30).ToList(); System.Text.StringBuilder sb = new System.Text.StringBuilder(); foreach (Zero.Pages.Tank.Hiscore hiscore in hiscores) { sb.Append(String.Format("{0},{1},{2}\n", hiscore.Name, hiscore.Score, hiscore.Time)); } System.IO.StreamWriter sw = new StreamWriter(path); sw.Write(sb.ToString()); sw.Close(); } } |
キー操作への対応
キー操作をおこなったときの処理を示します。
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 |
public class TankHub : Hub { public void DownKey(string key, string name) { // 長大なデータが送りつけられるかもしれないので対策 if (key.Length > 16) return; if (!Players.ContainsKey(Context.ConnectionId)) return; Player player = Players[Context.ConnectionId]; // プレイヤー名をPlayer.Nameにセット。プレイヤー名は最長で16文字とする // プレイヤー名に ,(カンマ)が含まれている場合は別の文字に置換する player.Name = name.Length > 16 ? name.Substring(0, 16) : name; player.Name = player.Name.Replace(",", "_"); if (key == "ArrowUp") player.IsUpKeyDown = true; if (key == "ArrowDown") player.IsDownKeyDown = true; if (key == "ArrowLeft") player.IsLeftKeyDown = true; if (key == "ArrowRight") player.IsRightKeyDown = true; // 発射はスペースキー if (key == " ") player.Shot(); } public void UpKey(string key) { // 長大なデータが送りつけられるかもしれないので対策 if (key.Length > 16) return; if (!Players.ContainsKey(Context.ConnectionId)) return; Player player = Players[Context.ConnectionId]; if (key == "ArrowUp") player.IsUpKeyDown = false; if (key == "ArrowDown") player.IsDownKeyDown = false; if (key == "ArrowLeft") player.IsLeftKeyDown = false; if (key == "ArrowRight") player.IsRightKeyDown = false; } } |