ASP.NET Coreで3DのSpaceWar!のような対戦型ゲームをつくる(2)の続きです。AspNetCore.SignalRにおける処理をおこないます。
1 2 3 4 5 6 7 8 |
using Microsoft.AspNetCore.SignalR; namespace SpaceWar { public class SpaceWarHub : Hub { } } |
以降は名前空間を省略して以下のように書きます。
1 2 3 |
public class SpaceWarHub : Hub { } |
接続時の処理
接続時におこなわれる処理を示します。
サーバーサイドに接続したら初回接続時のみタイマーの初期化をおこない、そのあとClientProxiesにClients.Callerを登録します。このとき接続ユーザー数が1の場合はSpaceWarGameオブジェクトの初期化をおこないます。
そのあとクライアントサイドに接続に成功したことを伝えます。他のプレイヤーの状態を表示させるための文字列を送信するために後述するSendPlayerInfosToClientメソッドを実行し、現在ゲームに参加することができるかをクライアントサイドで判断できるようにNPCの数を送信します(0が送信された場合は参加不可)。
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 SpaceWarHub : Hub { static bool IsFirstConnection = true; static System.Timers.Timer Timer = new System.Timers.Timer(); static Dictionary<string, IClientProxy> ClientProxies = new Dictionary<string, IClientProxy>(); static SpaceWarGame _game = new SpaceWarGame(); public override async Task OnConnectedAsync() { if (IsFirstConnection) { IsFirstConnection = false; Timer.Interval = 1000 / SpaceWarGame.UPDATES_PER_SECOND; Timer.Elapsed += Timer_Elapsed; } await base.OnConnectedAsync(); ClientProxies.Add(Context.ConnectionId, Clients.Caller); if (ClientProxies.Count == 1) { _game.Init(); Timer.Start(); } await Clients.Caller.SendAsync("SuccessfulConnectionToClient", "接続成功", Context.ConnectionId); await SendPlayerInfosToClient(); await Clients.Caller.SendAsync("SendNpcCountToClient", _game.GetNpcCount()); } } |
切断時の処理
切断された場合はClientProxiesに登録されているIClientProxyを削除します。またこのユーザーがゲームに参加している場合はSpaceWarGameクラスのなかにある辞書からPlayerを削除する処理もおこないます。そして削除の処理がおこなわれた場合(SpaceWarGame.RemovePlayerメソッドがtrueを返した場合)はゲームに参加しているプレイヤーの状態(人数)が変わったことになるので、これを全ユーザーに伝えます。
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 SpaceWarHub : Hub { public override async Task OnDisconnectedAsync(Exception? exception) { await base.OnDisconnectedAsync(exception); if (ClientProxies.ContainsKey(Context.ConnectionId)) ClientProxies.Remove(Context.ConnectionId); if (ClientProxies.Count == 0) Timer.Stop(); if (_game.RemovePlayer(Context.ConnectionId)) { await SendPlayerInfosToClient(); foreach (string id in ClientProxies.Keys) await ClientProxies[id].SendAsync("SendNpcCountToClient", _game.GetNpcCount()); } foreach (string id in ClientProxies.Keys) await ClientProxies[id].SendAsync("SendNpcCountToClient", _game.GetNpcCount()); try { System.GC.Collect(); } catch { Console.WriteLine("GC.Collect失敗"); } } } |
ゲームスタート時の処理
接続しているユーザーがゲームに参加するときの処理を示します。
この場合はSpaceWarGame.AddPlayerメソッドを実行し、戻り値がnullかどうかを調べます。nullでない場合は新しいプレイヤーとして追加されたことになるのでそのユーザーにはEventGameStartToClientを送信して確かにゲーム開始の処理が成功したことを知らせます。またこの場合もゲームに参加しているプレイヤーの状態(人数)が変わったことになるので、これを全ユーザーに伝えます。
またSpaceWarGame.AddPlayerメソッドが返したPlayerオブジェクトにはイベントハンドラを追加して弾丸を発射したとき、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 |
public class SpaceWarHub : Hub { public void GameStart(string id, string playerName) { if (!ClientProxies.ContainsKey(id)) return; _game.AddPlayer(Context.ConnectionId); Player? player = _game.GetPlayer(id); if (player != null) { player.Name = playerName; player.ShotEvent += Player_PlayerShotEvent; player.BeingAttackedEvent += Player_BeingAttackedEvent; player.GameOverEvent += Player_GameOverEvent; Task.Run(async () => { await SendPlayerInfosToClient(); if (ClientProxies.ContainsKey(id)) await ClientProxies[id].SendAsync("EventGameStartToClient"); foreach (string id in ClientProxies.Keys) await ClientProxies[id].SendAsync("SendNpcCountToClient", _game.GetNpcCount()); }); } } } |
イベントハンドラ
追加されるイベントハンドラについて示します。弾丸を発射したときはクライアントサイドにShotEventToClientを、NPCに狙われた場合はBeingAttackedEventToClientを送信します。
ゲームオーバーになったときはスコアランキングに登録したあとクライアントサイドにGameOverEventToClientを送信して効果音を鳴らす処理をおこなわせます。
この場合もゲームに参加しているプレイヤーの状態(人数)が変わったことになるので、これを全ユーザーに伝えます。
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 |
public class SpaceWarHub : Hub { void Player_PlayerShotEvent(object? sender, EventArgs args) { Player? player = (Player?)sender; if (player == null) return; if (!ClientProxies.ContainsKey(player.ConnectionId)) return; Task.Run(async () => { await ClientProxies[player.ConnectionId].SendAsync("ShotEventToClient"); }); } private void Player_BeingAttackedEvent(object? sender, EventArgs e) { Player? player = (Player?)sender; if (player == null) return; if (!ClientProxies.ContainsKey(player.ConnectionId)) return; Task.Run(async () => { await ClientProxies[player.ConnectionId].SendAsync("BeingAttackedEventToClient"); }); } private void Player_GameOverEvent(object? sender, EventArgs e) { Player? player = (Player?)sender; if (player == null) return; if (!ClientProxies.ContainsKey(player.ConnectionId)) return; SaveHiscore(player); Task.Run(async () => { if (ClientProxies.ContainsKey(player.ConnectionId)) { await SendPlayerInfosToClient(); await ClientProxies[player.ConnectionId].SendAsync("GameOverEventToClient", player.Score); } foreach (string id in ClientProxies.Keys) await ClientProxies[id].SendAsync("SendNpcCountToClient", _game.GetNpcCount()); }); } void SaveHiscore(Player player) { string path = "../hiscore-spacewar.txt"; HiscoreManager.Save(path, player.Name, player.Score); } } |
更新処理
タイマーイベントが発生したときは全プレイヤーの状態を更新してから当たり判定の処理をおこないます。もし爆発が発生しているのであれば火花の状態も更新します。
そして接続しているユーザー全員に対して各プレイヤーの状態を伝えるためにUpdatePlayerNamesToClientとUpdatePlayerNamesToClientを送信します。また弾丸と火花の状態を伝えるためにUpdateBulletsToClientとUpdateSparksToClientを送信します。最後にクライアントサイドにおける更新処理で必要なものはすべて送信したことを伝えるためにEndUpdateToClientを送信します。
またプレイヤーに対してはカメラの座標と残機、スコアも伝える必要があります。そのためにUpdateCameraPositionToClientとUpdateScoreToClientを送信します。
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 |
public class SpaceWarHub : Hub { static private void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) { List<Player> allPlayers = _game.GetAllPlayers(); foreach (Player player in allPlayers) player.Update(); Task.Run(async () => { bool isHit = await HitCheck(allPlayers); if(isHit) await SendPlayerInfosToClient(); }); List <Spark> sparks = _game.Sparks; foreach (Spark spark in sparks) spark.Update(); Task.Run(async () => { string playerIds = String.Join(",", allPlayers.Select(p => p.ConnectionId).ToArray()); string playerXs = String.Join(",", allPlayers.Select(p => p.X).ToArray()); string playerYs = String.Join(",", allPlayers.Select(p => p.Y).ToArray()); string playerZs = String.Join(",", allPlayers.Select(p => p.Z).ToArray()); string playerHeadingAngles = String.Join(",", allPlayers.Select(p => p.HeadingAngle).ToArray()); string playerAttitudeAngles = String.Join(",", allPlayers.Select(p => p.AttitudeAngle).ToArray()); string playerBankAngles = String.Join(",", allPlayers.Select(p => p.BankAngle).ToArray()); string playerIsDeads = String.Join(",", allPlayers.Select(p => { if (p.IsDead) return "true"; else return "false"; }).ToArray()); string playerInvincibleTimes = String.Join(",", allPlayers.Select(p => p.InvincibleTime).ToArray()); string playerNames = String.Join(",", allPlayers.Select(p => p.Name).ToArray()); List<Bullet> bullets = new List<Bullet>(); foreach (Player player in allPlayers) bullets.AddRange(player.GetBullets()); string bulletXs = String.Join(",", bullets.Select(b => b.X).ToArray()); string bulletYs = String.Join(",", bullets.Select(b => b.Y).ToArray()); string bulletZs = String.Join(",", bullets.Select(b => b.Z).ToArray()); sparks = _game.Sparks; string sparkXs = String.Join(",", sparks.Select(s => s.X).ToArray()); string sparkYs = String.Join(",", sparks.Select(s => s.Y).ToArray()); string sparkZs = String.Join(",", sparks.Select(s => s.Z).ToArray()); string sparkLifes = String.Join(",", sparks.Select(s => s.Life).ToArray()); foreach (string id in ClientProxies.Keys) { // プレイヤーの位置をクライアントサイドに送信する await ClientProxies[id].SendAsync( "UpdatePlayersToClient", id, playerIds, playerXs, playerYs, playerZs, playerHeadingAngles, playerAttitudeAngles, playerBankAngles, playerIsDeads, playerInvincibleTimes); await ClientProxies[id].SendAsync( "UpdatePlayerNamesToClient", playerNames); Player? player = _game.GetPlayer(id); if (player != null && !player.IsDead) { await ClientProxies[id].SendAsync( "UpdateCameraPositionToClient", player.CameraX, player.CameraY, player.CameraZ); await ClientProxies[id].SendAsync( "UpdateScoreToClient", player.Score, player.Rest, player.Life); } await ClientProxies[id].SendAsync( "UpdateBulletsToClient", bulletXs, bulletYs, bulletZs ); await ClientProxies[id].SendAsync( "UpdateSparksToClient", sparkXs, sparkYs, sparkZs, sparkLifes ); await ClientProxies[id].SendAsync("EndUpdateToClient"); } }); } } |
当たり判定と結果の送信
以下は当たり判定の処理です。
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 112 |
public class SpaceWarHub : Hub { static async Task<bool> HitCheck(List<Player> allPlayers) { bool isHit = false; foreach (Player player in allPlayers) { List<Bullet> bullets = player.GetBullets(); foreach (Player enemy in allPlayers) { if (enemy == player || enemy.IsDead || enemy.InvincibleTime > 0) continue; bool isPlayerDead = false; bool isEnemyDead = false; if (!player.IsDead && Math.Pow(player.X - enemy.X, 2) + Math.Pow(player.Y - enemy.Y, 2) + Math.Pow(player.Z - enemy.Z, 2) < 64 * 64) { player.BeHit(); if (player.IsDead) { isPlayerDead = true; if (ClientProxies.ContainsKey(player.ConnectionId)) await ClientProxies[player.ConnectionId].SendAsync("DeadToClient"); } else { if (ClientProxies.ContainsKey(player.ConnectionId)) await ClientProxies[player.ConnectionId].SendAsync("DamageToClient"); } enemy.BeHit(); if (enemy.IsDead) { isEnemyDead = true; if (ClientProxies.ContainsKey(enemy.ConnectionId)) await ClientProxies[enemy.ConnectionId].SendAsync("DeadToClient"); } else { if (ClientProxies.ContainsKey(player.ConnectionId)) await ClientProxies[enemy.ConnectionId].SendAsync("DamageToClient"); } isHit = true; string playerName = player.Name.Replace("<", "<").Replace(">", ">"); string enemyName = enemy.Name.Replace("<", "<").Replace(">", ">"); string str = ""; if (isEnemyDead && isPlayerDead) str = String.Format("{0}と{1}が衝突しました", playerName, enemyName); else if (isEnemyDead) str = String.Format("{0}が{1}を撃破しました", playerName, enemyName); else if (isPlayerDead) str = String.Format("{0}が{1}を撃破しました", enemyName, playerName); if (str != "") { foreach (string id in ClientProxies.Keys) await ClientProxies[id].SendAsync("SendHitCheckToClient", str); } continue; } Bullet? bullet = bullets.FirstOrDefault(b => !b.IsDead && Math.Pow(b.X - enemy.X, 2) + Math.Pow(b.Y - enemy.Y, 2) + Math.Pow(b.Z - enemy.Z, 2) < 64 * 64); if (bullet != null) { bullet.IsDead = true; enemy.BeHit(); if (enemy.IsDead) { string playerName = player.Name.Replace("<", "<").Replace(">", ">"); string enemyName = enemy.Name.Replace("<", "<").Replace(">", ">"); string str = String.Format("{0}が{1}を撃破しました", playerName, enemyName); foreach (string id in ClientProxies.Keys) await ClientProxies[id].SendAsync("SendHitCheckToClient", str); if (ClientProxies.ContainsKey(player.ConnectionId)) await ClientProxies[player.ConnectionId].SendAsync("HitEnemyToClient"); if (ClientProxies.ContainsKey(enemy.ConnectionId)) await ClientProxies[enemy.ConnectionId].SendAsync("DeadToClient"); player.Score += 500; } else { if (ClientProxies.ContainsKey(player.ConnectionId)) await ClientProxies[player.ConnectionId].SendAsync("DamageToClient"); if (ClientProxies.ContainsKey(enemy.ConnectionId)) await ClientProxies[enemy.ConnectionId].SendAsync("DamageToClient"); player.Score += 50; } isHit = true; _game.SetSparks(enemy.X, enemy.Y, enemy.Z); break; } } } if (isHit) return true; else return false; } } |
結果の送信
SendPlayerInfosToClientメソッドは各プレイヤー(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 |
public class SpaceWarHub : Hub { static async Task SendPlayerInfosToClient() { List<Player> allPlayers = _game.GetPlayersWithoutNPC(); // エスケープ処理をおこなっている string[] names = allPlayers.Select(p => p.Name.Replace("<", "<").Replace(">", ">")).ToArray(); int[] scores = allPlayers.Select(p => p.Score).ToArray(); int[] rests = allPlayers.Select(p => p.Rest).ToArray(); int[] lifes = allPlayers.Select(p => p.Life).ToArray(); string[] vs = new string[names.Length]; for (int i = 0; i < names.Length; i++) { vs[i] = String.Format("<tr><td>{0}</td><td>{1}</td><td>{2} ({3})</td></tr>", names[i], scores[i], rests[i], lifes[i]); } string playerInfos = "<table>"; playerInfos += "<tr><td>プレイヤー名</td><td>スコア</td><td>残機 (耐久度)</td></tr>"; playerInfos += String.Join("", vs); playerInfos += "</table>"; foreach (string id in ClientProxies.Keys) { await ClientProxies[id].SendAsync( "UpdatePlayerInfosToClient", playerInfos); } } } |
キー操作への対応
キーが押されたり離されたときはPlayer.IsXXXKeyDownフラグをセットしたりクリアします。このとき送られてきたプレイヤー名をPlayer.Nameプロパティにセットしています。このとき名前のなかにカンマがあるとスコアランキングに登録するときに不具合を起こすので別の文字に置換しています。
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 |
public class SpaceWarHub : Hub { public void DownKey(string command, string name) { // 長大なデータが送りつけられるかもしれないので対策 if (command.Length > 16) return; Player? player = _game.GetPlayer(Context.ConnectionId); if (player == null) return; player.Name = name.Length > 16 ? name.Substring(0, 16) : name; player.Name = player.Name.Replace(",", "_"); if (command == "Left") player.IsLeftKeyDown = true; else if (command == "Right") player.IsRightKeyDown = true; else if (command == "Up") player.IsUpKeyDown = true; else if (command == "Down") player.IsDownKeyDown = true; else if (command == "Shot") player.Shot(); } public void UpKey(string command) { // 長大なデータが送りつけられるかもしれないので対策 if (command.Length > 16) return; Player? player = _game.GetPlayer(Context.ConnectionId); if (player == null) return; if (command == "Left") player.IsLeftKeyDown = false; else if (command == "Right") player.IsRightKeyDown = false; else if (command == "Up") player.IsUpKeyDown = false; else if (command == "Down") player.IsDownKeyDown = false; else if (command == "Shot") player.Shot(); } } |