ASP.NET Core版 対戦型ラリーエックスをつくる(2)の続きです。
AspNetCore.SignalR.Hubクラスを継承してRallyXHubクラスを定義します。
| 1 2 3 4 5 6 7 8 9 10 | using System.Collections.Generic; using Microsoft.AspNetCore.SignalR; using System.Timers; namespace RallyX {     public class RallyXHub : Hub     {     } } | 
以降は
| 1 2 3 | public class RallyXHub : Hub { } | 
と書きます。
Contents
接続時の処理
クライアントがサーバーサイドに接続したときの処理を示します。
最初の1回だけタイマーの初期化をおこなっています。接続に成功したらクライアントサイドにContext.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 | public class RallyXHub : Hub {     static bool IsFirstConnection = true;     static Dictionary<string, IClientProxy> ClientProxyMap = new Dictionary<string, IClientProxy>();     public override async Task OnConnectedAsync()     {         // 最初に実行されたときだけタイマーの初期化をおこなう         if (IsFirstConnection)         {             IsFirstConnection = false;             Timer.Interval = 1000 / 36;             Timer.Elapsed += Timer_Elapsed; // Timer_Elapsedは後述         }         await base.OnConnectedAsync();         // 接続に成功したらクライアントサイドに通知する         await Clients.Caller.SendAsync("SuccessfulConnectionToClient", "接続成功", Context.ConnectionId);         ClientProxyMap.Add(Context.ConnectionId, Clients.Caller);         if (ClientProxyMap.Count == 1)         {             InitField();             Timer.Start();         }         // 壁をクライアントサイドに送信する         string wallXs = String.Join(",", RallyXGame.Walls.Select(wall => wall.X.ToString()).ToArray());         string wallYs = String.Join(",", RallyXGame.Walls.Select(wall => wall.Y.ToString()).ToArray());         int xMax = RallyXGame.Walls.Max(wall => wall.X) + RallyXGame.CHARACTER_SIZE;         int yMax = RallyXGame.Walls.Max(wall => wall.Y) + RallyXGame.CHARACTER_SIZE;         await Clients.Caller.SendAsync("SendWallsToClient", wallXs, wallYs, xMax.ToString(), yMax.ToString());     } } | 
接続したときにそのクライアントが最初のユーザーであった場合は、フィールドを初期化します。ここでは壁の追加とNPCの初期化の処理をしています。
| 1 2 3 4 5 6 7 8 | public class RallyXHub : Hub {     void InitField()     {         RallyXGame.Init();         RallyXGame.InitNpcs();     } } | 
NPCの初期化
NPCを初期化する処理はRallyXGameクラスで定義しています。NPCsに格納されているPlayerオブジェクトがあったらクリアして赤青緑のPlayerを3つずつ合計9つ生成しています。Playerクラスのコンストラクタにプレイヤー識別番号を渡すことで自動的に初期位置にセットされます。
| 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 | public class RallyXGame {     static Dictionary<string, Player> Players = new Dictionary<string, Player>();     static List<Player> NPCs = new List<Player>();     public static List<Player> BluePlayers = new List<Player>();     public static List<Player> RedPlayers = new List<Player>();     public static List<Player> GreenPlayers = new List<Player>();     public static void InitNpcs()     {         NPCs.Clear();         int[] arr = { 101, 102, 103, 201, 202, 203, 301, 302, 303, };         for (int i = 0; i < arr.Length; i++)         {             Player npc = new Player("", arr[i]);             RallyXGame.NPCs.Add(npc);         }         // 色別にもわける         BluePlayers = GetAllPlayers().Where(player => player.PlayerNumber / 100 == 1).ToList();         RedPlayers = GetAllPlayers().Where(player => player.PlayerNumber / 100 == 2).ToList();         GreenPlayers = GetAllPlayers().Where(player => player.PlayerNumber / 100 == 3).ToList();     } } | 
GetAllPlayersメソッドは以下のようになっています。これはプレイヤーとNPCの両方のPlayerオブジェクトのリストを返します。
| 1 2 3 4 5 6 7 8 9 10 11 | public class RallyXGame {     public static List<Player> GetAllPlayers()     {         List<Player> allPlayers = new List<Player>(NPCs);         foreach (Player player in Players.Values)             allPlayers.Add(player);         return allPlayers.OrderBy(player => player.PlayerNumber % 3).ThenBy(player => player.PlayerNumber / 3).ToList();     } } | 
切断されたときの処理
切断されたときの処理を示します。ClientProxyMapのなかから対応するものを取り除きます。そして接続ユーザーが0になった場合はタイマーを止めます。
切断されたユーザーはプレイ中のユーザーかもしれません。そこでContext.ConnectionIdからプレイヤーとして登録されているかを調べます。登録されている場合はそこからも削除します。そのあと接続しているユーザー全員に試合放棄があったことを通知します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class RallyXHub : Hub {     public override async Task OnDisconnectedAsync(Exception? exception)     {         await base.OnDisconnectedAsync(exception);         if (ClientProxyMap.ContainsKey(Context.ConnectionId))             ClientProxyMap.Remove(Context.ConnectionId);         if (ClientProxyMap.Count == 0)             Timer.Stop();         string playerName = RallyXGame.GetPlayerNameFromKey(Context.ConnectionId);         RallyXGame.RemovePlayerFromKey(Context.ConnectionId);         await Clients.All.SendAsync("NotificationToClient", playerName + "が試合放棄しました");     } } | 
GetPlayerNameFromKeyメソッドはContext.ConnectionIdを引数にプレイヤー名を取得するメソッドです。RallyXGameクラス内で以下のように定義されています。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | public class RallyXGame {     public static string GetPlayerNameFromKey(string key)     {         if (Players.ContainsKey(key))         {             string playerName = Players[key].Name;             return playerName.Replace(",", "_").Replace("<", "<").Replace(">", ">");         }         else             return "";     } } | 
RemovePlayerFromKeyメソッドはRallyXGameクラス内で以下のように定義されています。
引数に対応するPlayerオブジェクトがPlayers内に存在する場合はこれを削除します。そして新しいNPCを生成してNPCsに追加しています。そして全プレイヤーを色別に分類しています。
| 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 RallyXGame {     public static Player? RemovePlayerFromKey(string key)     {         if (Players.ContainsKey(key))         {             Player player = Players[key];             int playerNumber = player.PlayerNumber;             Players.Remove(key);             Player npc = new Player("", playerNumber);             NPCs.Add(npc);             BluePlayers = GetAllPlayers().Where(player => player.PlayerNumber / 100 == 1).ToList();             RedPlayers = GetAllPlayers().Where(player => player.PlayerNumber / 100 == 2).ToList();             GreenPlayers = GetAllPlayers().Where(player => player.PlayerNumber / 100 == 3).ToList();             return player;         }         else             return null;     } } | 
ゲームに参加するときの処理
ユーザーがプレイに参加するときの処理を示します。
新しくユーザーがゲームに参加した場合、そのユーザーにはたしかにゲームに参加できたことを、それ以外のユーザーには新しいユーザーがゲームに参加したことを通知します。また新たにゲームに参加したユーザーにはフィールド上に存在する煙幕の位置もあわせて通知します。
| 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 | public class RallyXHub : Hub {     public async Task GameStart(string id, string playerName)     {         if (RallyXGame.GetPlayerFromKey(id) != null)             return;         // 自分がはじめてのプレイヤーの場合のみフィールドをリセット         if (RallyXGame.GetPlayersCount() == 0)             InitField();         // プレイヤーオブジェクトを生成して追加         Player? player = RallyXGame.AddPlayerFromKey(id, playerName);         if (player != null)         {             playerName = playerName.Replace(",", "_");             player.GameOverEvent += Player_GameOverEvent;             player.Name = playerName;             // 新しく参加したユーザーのプレイヤー名を通知             playerName = playerName.Replace(",", "_").Replace("<", "<").Replace(">", ">");             await Clients.Others.SendAsync("NotificationToClient", "[" + player.Name + "]が参戦しました");             await Clients.Caller.SendAsync("NotificationToClient", "[" + player.Name + "]として参戦しました");             // 新しく参加したユーザーにフィールド上の煙幕の位置を通知する             await Clients.Caller.SendAsync(                 "SendSmokesToClient",                 RallyXGame.GetStringSmokesX(), RallyXGame.GetStringSmokesY(), RallyXGame.GetStringSmokesNumber()             );         }     } } | 
上記で呼び出されているRallyXGameクラスのメソッドを示します。
| 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 | public class RallyXGame {     // keyで検索してPlayerオブジェクトを返す     public static Player? GetPlayerFromKey(string key)     {         if (Players.ContainsKey(key))             return Players[key];         else             return null;     }     // 現在プレイ中のユーザー数を返す     public static int GetPlayersCount()     {         return Players.Count;     }     // 新しいPlayerを追加する     public static Player? AddPlayerFromKey(string key, string playerName)     {         int resetNumber = -1;         // NPCのひとつをPlayerオブジェクトに入れ替える         if (NPCs.Count > 0)         {             playerName = playerName.Replace(",", "_");             NPCs = NPCs.OrderBy(player => player.PlayerNumber % 100).ThenBy(player => player.PlayerNumber / 3).ToList();             resetNumber = RallyXGame.NPCs[0].PlayerNumber;             NPCs.RemoveAt(0);             Player player = new Player(key, resetNumber);             Players.Add(key, player);             BluePlayers = GetAllPlayers().Where(player => player.PlayerNumber / 100 == 1).ToList();             RedPlayers = GetAllPlayers().Where(player => player.PlayerNumber / 100 == 2).ToList();             GreenPlayers = GetAllPlayers().Where(player => player.PlayerNumber / 100 == 3).ToList();             return player;         }         else             return null;     } } | 
フィールド上に存在する煙幕をクライアントに送信する
以下はフィールド上に存在する煙幕の座標と状態をカンマ区切りの文字列で取得するためのメソッドです。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class RallyXGame {     public static string GetStringSmokesX()     {         return String.Join(",", Smokes.Select(smoke => smoke.X.ToString()).ToArray());     }     public static string GetStringSmokesY()     {         return String.Join(",", Smokes.Select(smoke => smoke.Y.ToString()).ToArray());     }     public static string GetStringSmokesNumber()     {         return String.Join(",", Smokes.Select(smoke => smoke.PlayerNumber.ToString()).ToArray());     } } | 
更新処理
更新処理をおこなう前にクライアントサイドで更新処理をするために必要なデータを取得するための処理を示します。
以下は新しく発生した煙幕のX座標、Y座標、生成したプレイヤーを文字列として取得するためのメソッドです。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class RallyXGame {     public static string GetStringNewSmokesX()     {         return String.Join(",", _newSmokes.Select(smoke => smoke.X).ToArray());     }     public static string GetStringNewSmokesY()     {         return String.Join(",", _newSmokes.Select(smoke => smoke.Y).ToArray());     }     public static string GetStringNewSmokesNumber()     {         return String.Join(",", _newSmokes.Select(smoke => smoke.PlayerNumber / 100).ToArray());     } } | 
以下は消滅する煙幕のX座標、Y座標をカンマ区切りの文字列として取得するためのメソッドです。
| 1 2 3 4 5 6 7 8 9 10 11 12 | public class RallyXGame {     public static string GetStringSmokesDisappearX()     {         return String.Join(",", SmokesDisappear.Select(smoke => smoke.X).ToArray());     }     public static string GetStringSmokesDisappearY()     {         return String.Join(",", SmokesDisappear.Select(smoke => smoke.Y).ToArray());     } } | 
以下は未通過のフラッグのX座標、Y座標をカンマ区切りの文字列として取得するためのメソッドです。
| 1 2 3 4 5 6 7 8 9 10 11 12 | public class RallyXGame {     public static string GetStringFlagsX()     {         return String.Join(",", Flags.Select(f => f.X).ToArray());     }     public static string GetStringFlagsY()     {         return String.Join(",", Flags.Select(f => f.Y).ToArray());     } } | 
以下は火花のX座標、Y座標、消滅までの時間をカンマ区切りの文字列として取得するためのメソッドです。
| 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 RallyXGame {     static List<Spark> _sparks = new List<Spark>();     public static List<Spark> Sparks     {         get{ return _sparks.Where(fire => fire.Life > 0).ToList(); }     }     public static string GetStringSparksX()     {         return String.Join(",", RallyXGame.Sparks.Select(spark => spark.X).ToArray());     }     public static string GetStringSparksY()     {         return String.Join(",", RallyXGame.Sparks.Select(spark => spark.Y).ToArray());     }     public static string GetStringSparksLife()     {         return  String.Join(",", RallyXGame.Sparks.Select(spark => spark.Life.ToString()).ToArray());     } } | 
タイマーでElapsedイベントが発生したら更新処理をおこないます。
RallyXGame.Updateメソッドを呼び出してデータの更新処理をおこない、当たり判定をしています。そのあとSendUpdateToClientメソッドを呼び出してクライアントサイドでの更新処理に必要なデータを送っています。送るデータは同じなので最初にまとめて取得する処理をおこなっています。
煙幕はゲームの進行と時間の経過によって新たに発生したり消滅したりします。これらを毎回すべてのクライアントにすべて送信していては処理に時間がかかるので差分のみを送信します。そのため新しく生成された煙幕を送信したらRallyXGame.EndUpdateメソッドを実行することで、新しく生成された煙幕と既存の煙幕をひとつにまとめています。
| 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 | public class RallyXHub : Hub {     static private void Timer_Elapsed(object? sender, ElapsedEventArgs e)     {         Task.Run(async () =>         {             RallyXGame.Update();             await HitCheck(RallyXGame.BluePlayers, RallyXGame.RedPlayers, RallyXGame.GreenPlayers);             // SendUpdateToClientメソッドに渡すデータをつくる             List<string> keys = new List<string>();             List<int> numbers = new List<int>();             List<int> xs = new List<int>();             List<int> ys = new List<int>();             List<string> directs = new List<string>();             List<int> fuels = new List<int>();             List<string> scores = new List<string>();             List<string> rests = new List<string>();             List<string> names = new List<string>();             List<bool> isDeads = new List<bool>();             List<int> invincibleTimes = new List<int>();             List<int> timeToSpins = new List<int>();             List<Player> players = RallyXGame.GetAllPlayers();             foreach (Player player in players)             {                 string score = "";                 string rest = "";                 if (player.ConnectionID != "")                 {                     score = player.Score.ToString();                     rest = player.Rest.ToString();                 }                 keys.Add(player.ConnectionID);                 numbers.Add(player.PlayerNumber);                 xs.Add(player.X);                 ys.Add(player.Y);                 directs.Add(GetDirectText(player.MovingDirect));                 fuels.Add(player.Fuel);                 scores.Add(score);                 rests.Add(rest);                 names.Add(player.Name);                 isDeads.Add(player.IsDead);                 invincibleTimes.Add(player.InvincibleTime);                 timeToSpins.Add(player.TimeToSpin);             }             foreach (string id in ClientProxyMap.Keys)             {                 if(!RallyXGame.IsTimeBetweenStages)                     await SendUpdateToClient(                         id, keys, numbers,                         xs, ys, directs, fuels, scores, rests, names, isDeads, invincibleTimes, timeToSpins,                         RallyXGame.GetStringNewSmokesX(), RallyXGame.GetStringNewSmokesY(), RallyXGame.GetStringNewSmokesNumber(),                         RallyXGame.GetStringSmokesDisappearX(), RallyXGame.GetStringSmokesDisappearY(),                         RallyXGame.GetStringFlagsX(), RallyXGame.GetStringFlagsY(),                         RallyXGame.GetStringSparksX(), RallyXGame.GetStringSparksY(), RallyXGame.GetStringSparksLife()                     );                 else                     await ShowStageClearString(id);             }             RallyXGame.EndUpdate();         });     }     // Directを文字列に変換する     public static string GetDirectText(Direct direct)     {         if (direct == Direct.Up)             return "n";         if (direct == Direct.Right)             return "e";         if (direct == Direct.Down)             return "s";         if (direct == Direct.Left)             return "w";         else             return "n";     } } | 
RallyXGame.EndUpdateメソッドは新しく発生した煙幕を既存の煙幕と統合しています。IsTimeBetweenStagesプロパティは現在の状態がステージクリア後のステージとステージのあいだにあるかどうかを返します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class RallyXGame {     public static void EndUpdate()     {         Smokes.AddRange(_newSmokes);         _newSmokes.Clear();     }     public static bool IsTimeBetweenStages     {         get         {             // いまは_updateCountが何かわからないので意味不明かもしれないが・・・             return _updateCount < 0;         }     } } | 
更新用のデータをクライアントサイドに送信する
クライアントサイドに更新用のデータを送信する処理を示します。ここでは各プレイヤーの名前、ID、座標、移動方向、燃料と上記の処理で取得した煙幕の状態、フラッグ、火花を文字列にしたものをクライアントサイドに送信しています。
| 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 | public class RallyXHub : Hub {     static async Task SendUpdateToClient(         string id, List<string> keys, List<int> numbers,         List<int> xs, List<int> ys, List<string> directs, List<int> fuels, List<string> scores, List<string> rests,         List<string> names, List<bool> isDeads, List<int> invincibleTimes, List<int> timeToSpins,         string newSmokesX, string newSmokesY, string newSmokesNumber,         string smokesDisappearX, string smokesDisappearY,         string flagsX, string flagsY,         string sparksX, string sparksY, string sparksLife     )     {         if (!ClientProxyMap.ContainsKey(id))             return;         // クライアントサイドへのデータ送信開始         await ClientProxyMap[id].SendAsync("IsTimeBetweenStagesToClient", false, "");         await ClientProxyMap[id].SendAsync("StartUpdateToClient");         int imageIndex = -1;         int count = keys.Count;         for (int i=0; i < count; i++)         {             await ClientProxyMap[id].SendAsync(                 "UpdatePlayer1ToClient",                 keys[i], numbers[i], xs[i], ys[i], directs[i], fuels[i],                 isDeads[i], invincibleTimes[i], timeToSpins[i]             );             await ClientProxyMap[id].SendAsync(                 "UpdatePlayer2ToClient",                 keys[i], names[i], scores[i], rests[i]             );             if (keys[i] == id)                 imageIndex = numbers[i] / 100 - 1;         }         await ClientProxyMap[id].SendAsync(             "UpdateSmokesToClient",             newSmokesX, newSmokesY, newSmokesNumber, smokesDisappearX, smokesDisappearY         );         await ClientProxyMap[id].SendAsync("UpdateSparksToClient", sparksX, sparksY, sparksLife);         await ClientProxyMap[id].SendAsync("UpdateFlagsToClient", flagsX, flagsY);         string gameStatus;         bool canGameStart = false;         if (RallyXGame.GetPlayerFromKey(id) != null)             gameStatus = "現在 参戦中!";         else if (RallyXGame.GetNPCsCount() == 0)             gameStatus = "現在満員です。空きが出るまでお待ちください。";         else         {             gameStatus = "参加可能です!";             canGameStart = true;         }         await ClientProxyMap[id].SendAsync("UpdateGameStatusToClient", gameStatus, canGameStart, imageIndex);         // クライアントサイドへのデータ送信終了         await ClientProxyMap[id].SendAsync("EndUpdateToClient");     } } | 
RallyXGame.GetNPCsCountメソッドはNPCの数を返します。
| 1 2 3 4 5 6 7 | public class RallyXGame {     public static int ()     {         return NPCs.Count;     } } | 
ステージクリア時の文字列を表示させる
ステージクリア後の次のステージが開始されるまでに表示する文字列を送信する処理を示します。ここでは各プレイヤーのボーナスポイントと成績上位者を表示するための文字列を送信しています。
| 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 | public class RallyXHub : Hub {     static async Task ShowStageClearString(string id)     {         string str = "";         Player? player = RallyXGame.GetPlayerFromKey(id);         if (player != null)         {             int bonus = player.Fuel / 100 * 100;             if (bonus <= 0)                 bonus = 100;             str = String.Format("Bonus Point {0},", bonus);         }         else             str = " ,";         string lastFlagPlayerName = "";         if (RallyXGame.LastFlagPlayer != null)             lastFlagPlayerName = RallyXGame.LastFlagPlayer.Name;         str += String.Format("最後の旗を取ったのは {0} です,", lastFlagPlayerName);         str += RallyXGame.StageClearString;         await ClientProxyMap[id].SendAsync("IsTimeBetweenStagesToClient", true, str);         await ClientProxyMap[id].SendAsync("StartUpdateToClient");         await ClientProxyMap[id].SendAsync("EndUpdateToClient");     } } | 
LastFlagPlayerには最後にフラッグを取ったプレイヤーが格納されます。ステージクリアの際に最後にフラッグを取ったプレイヤー名を表示させたいのでここに一時的に格納しています。
| 1 2 3 4 | public class RallyXGame {     public static Player? LastFlagPlayer = null; } | 
データの更新
_updateCount == 0であればステージが開始されるときなのでステージの初期化の処理をおこないます。各PlayerオブジェクトをリセットするとともにStageKillCountプロパティとStageFlagCountプロパティを0に戻します。
_updateCount > 0のときは各オブジェクトの更新処理をおこないます。プレイヤーならUpdatePlayerメソッド、NPCならUpdateNPCメソッドを呼び出したあと、煙幕と火花の状態も更新します。そして煙幕のなかで消滅したものをSmokesDisappearに一時的に保存します。そのあとステージクリア判定をおこないます。
| 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 | public class RallyXGame {     static int _updateCount = 0;     static List<Smoke> SmokesDisappear = new List<Smoke>();     public static void Update()     {         _updateCount++;         // _updateCount == 0ならステージが開始されるときなのでステージの初期化をする         if (_updateCount == 0)         {             List<Player> allPlayers = GetAllPlayers();             foreach (Player player in allPlayers)             {                 player.Reset();                 player.StageKillCount = 0;                 player.StageFlagCount = 0;             }             return;         }         // _updateCount < 0ならステージとステージの間の時間帯なのでなにもしない         if (_updateCount < 0)             return;         // _updateCount > 0のときは更新処理をおこなう         List<Player> players = Players.Select(_ => _.Value).ToList();         foreach (Player player in players)             player.UpdatePlayer();         if (NPCs.Count > 0)         {             foreach (Player player in NPCs)             {                 if (!player.IsDead)                     player.UpdateNPC();             }         }         foreach (Smoke smoke in Smokes)             smoke.Update();         // 煙幕のなかで消滅したものを取得する         SmokesDisappear = Smokes.Where(smoke => smoke.Life <= 0).ToList();         _smokes = Smokes.Where(smoke => smoke.Life > 0).ToList();         foreach (Spark spark in Sparks)             spark.Update();         // ステージクリアになっているかもしれないのでチェックする         CheckStageClear();     } } | 
当たり判定
X座標が同じでY軸の差がCHARACTER_SIZE – 8以下の場合、Y座標が同じでX軸の差がCHARACTER_SIZE – 8以下の場合は両者は衝突していると判断します。ただし片方の死亡フラグが立っている場合、または当たり判定によって撃破される側が無敵状態のときはは当たり判定はしません。
衝突している場合は撃破された側のDeadメソッドを呼び出し、火花を発生させます。そしてクライアントサイドにPlayerDeadToClientを送信します。そのあと後述するKillメソッドを呼び出して撃破したプレイヤーに対して加点処理をおこないます。
| 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 | public class RallyXHub : Hub {     static async Task HitCheck(List<Player> blues, List<Player> reds, List<Player> greens)     {         await HitCheck(blues, reds);         await HitCheck(reds, greens);         await HitCheck(greens, blues);     }     static async Task HitCheck(List<Player> players, List<Player> enemies)     {         int distance = RallyXGame.CHARACTER_SIZE - 8;         foreach (Player player in players)         {             if (player.IsDead || player.InvincibleTime > 0)                 continue;             List<Player> killers = enemies.Where(p => !p.IsDead && p.X == player.X && p.Y - distance <= player.Y && player.Y <= p.Y + distance).ToList();             if (killers.Count > 0)             {                 player.Dead();                 RallyXGame.SetSparks(player.X, player.Y);                 if (player.ConnectionID != "")                 {                     if (ClientProxyMap.ContainsKey(player.ConnectionID))                         await ClientProxyMap[player.ConnectionID].SendAsync("PlayerDeadToClient");                 }                 await Kill(killers, player);                 continue;             }             killers = enemies.Where(p => !p.IsDead && p.Y == player.Y && p.X - distance <= player.X && player.X <= p.X + distance).ToList();             if (killers.Count > 0)             {                 player.Dead();                 RallyXGame.SetSparks(player.X, player.Y);                 if (player.ConnectionID != "")                 {                     if (ClientProxyMap.ContainsKey(player.ConnectionID))                         await ClientProxyMap[player.ConnectionID].SendAsync("PlayerDeadToClient");                 }                 await Kill(killers, player);                 continue;             }             if (!player.IsDead)             {                 Flag? flag = RallyXGame.Flags.FirstOrDefault(f => f.X == player.X && f.Y == player.Y);                 if (flag != null)                 {                     flag.IsPassed = true;                     player.StageFlagCount++;                     player.FlagCount++;                     player.Score += 100 * player.FlagCount;                     player.Fuel += 100;                     if (RallyXGame.Flags.Count == 0)                     {                         player.Score += 100 * player.FlagCount;                         RallyXGame.LastFlagPlayer = player;                     }                     if (player.ConnectionID != "")                     {                         if (ClientProxyMap.ContainsKey(player.ConnectionID))                             await ClientProxyMap[player.ConnectionID].SendAsync("GetFlagToClient");                     }                     foreach (IClientProxy client in ClientProxyMap.Values)                     {                         await client.SendAsync(                             "NotificationToClient2", player.Name + " が旗を回収しました");                     }                 }             }         }     } } | 
RallyXGame.SetSparksメソッドは撃破されたプレイヤーの周辺に火花を表示させるための処理をおこないます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class RallyXGame {     public static void SetSparks(int x, int y)     {         for (int i = 0; i < 24; i++)         {             int r = _random.Next(100);             double rad = Math.PI * 2 * r / 100;             double v = 8 + _random.Next(8);             _sparks.Add(new Spark(x, y, v * Math.Cos(rad), v * Math.Sin(rad)));         }     } } | 
加点処理
撃破したプレイヤーに加点処理をおこなう処理を示します。撃破したプレイヤーには800点が加算されますが、そうでなくても同じ色であれば100点加算されます。
| 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 | public class RallyXHub : Hub {     static async Task Kill(List<Player> killers, Player killed)     {         foreach (Player killer in killers)         {             killer.Score += 800; // 直接倒したプレイヤーに800点加算             killer.StageKillCount++;             killer.KillCount++;             // 同じ色のそれ以外のプレイヤーには100点加算             List<Player> wins1 = new List<Player>();             if (killer.PlayerNumber / 100 == 1)                 wins1 = RallyXGame.BluePlayers;             if (killer.PlayerNumber / 100 == 2)                 wins1 = RallyXGame.RedPlayers;             if (killer.PlayerNumber / 100 == 3)                 wins1 = RallyXGame.GreenPlayers;             wins1.Where(p => p.ConnectionID != "" && p != killer).Select(p => p.Score += 100).ToList();             if (killer.ConnectionID != "")             {                 if (ClientProxyMap.ContainsKey(killer.ConnectionID))                     await ClientProxyMap[killer.ConnectionID].SendAsync("EnemyDeadToClient");             }             foreach (IClientProxy client in ClientProxyMap.Values)             {                 await client.SendAsync(                     "NotificationToClient2", killer.Name + " が " + killed.Name + "を撃破しました");             }         }     } } | 
ステージクリア判定
ステージクリアになっているかどうかをチェックする処理を示します。
Flags.Count == 0であればステージクリアです。この場合は_updateCountに負数を代入します。更新処理が1回おこなわれるたびに_updateCountはインクリメントされますが、これが負数の場合はステージクリアから次のステージまでの休止期間であることになります。
ステージクリアであると判定されたら新しいステージをつくります。やることはフラッグの初期化です。
このときに成績優秀者を表示させるための文字列を生成します。フラッグ取得数と撃破数が多いプレイヤーは誰なのかを文字列にして取得します。
| 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 | public class RallyXGame {     public static string? StageClearString     {         private set;         get;     }     public static void CheckStageClear()     {         if (_updateCount < 0)             return;         if (Flags.Count == 0)         {             if (Global.IsDebug)                 Console.WriteLine("CheckStageClear");             _updateCount = -36 * 5; // 5秒間休む             InitFlags();             // 成績優秀者を表示させるための文字列を生成する             List<Player> players = GetAllPlayers();             players = players.OrderByDescending(player => player.StageFlagCount + player.StageKillCount).ToList();             for (int i = 0; i < 3; i++)                 StageClearString += String.Format("No.{0} {1}  Kill-{2} Flag-{3},",                     i + 1, players[i].Name.Replace(",", "_"), players[i].StageKillCount, players[i].StageFlagCount);         }         else             StageClearString = "";     } } | 
ゲームオーバー時の処理
ゲームオーバーになったらすべてのユーザーにこれを通知します。それと同時にスコアをランキングに登録する処理をおこないます。
| 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 RallyXHub : Hub {     private void Player_GameOverEvent(Player player)     {         RallyXGame.RemovePlayerFromKey(player.ConnectionID);         if (ClientProxyMap.ContainsKey(player.ConnectionID))         {             Task.Run(async () =>             {                 await ClientProxyMap[player.ConnectionID].SendAsync("GameOverToClient");             });             SaveHiscore(player);         }         string playerName = player.Name;         playerName = playerName.Replace(",", "_").Replace("<", "<").Replace(">", ">");         foreach (string key in ClientProxyMap.Keys)         {             Task.Run(async () => {                 await ClientProxyMap[key].SendAsync("NotificationToClient", playerName + "がゲームオーバーになりました");             });         }     } } | 
スコアをランキングに登録する処理を示します。これまでのゲームとほとんど同じことをしています。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | namespace Zero.Pages.RallyX {     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 = "";     } } | 
| 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 RallyXHub : Hub {     void SaveHiscore(Player player)     {         string path = "../hiscore-rallyx.txt";         List<Zero.Pages.RallyX.Hiscore> hiscores = new List<Zero.Pages.RallyX.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.RallyX.Hiscore hiscore = new Zero.Pages.RallyX.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.RallyX.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.RallyX.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 53 54 55 56 57 58 | public class RallyXHub : Hub {     public void DownKey(string key, string name)     {         // 長大なデータが送りつけられるかもしれないので対策         if (key.Length > 16)             return;         Player? player = RallyXGame.GetPlayerFromKey(Context.ConnectionId);         if (player == null)             return;         player.Name = name.Length > 16 ? name.Substring(0, 16) : name;         player.Name = player.Name.Replace(",", "_");         if (key == "ArrowUp")             player.IsUpKeyDown = true;         else if (key == "ArrowDown")             player.IsDownKeyDown = true;         else if (key == "ArrowLeft")             player.IsLeftKeyDown = true;         else if (key == "ArrowRight")             player.IsRightKeyDown = true;         else if (key == " ")             Smoke(player);     }     public void UpKey(string key)     {         // 長大なデータが送りつけられるかもしれないので対策         if (key.Length > 16)             return;         Player? player = RallyXGame.GetPlayerFromKey(Context.ConnectionId);         if (player == null)             return;         if (key == "ArrowUp")             player.IsUpKeyDown = false;         if (key == "ArrowDown")             player.IsDownKeyDown = false;         if (key == "ArrowLeft")             player.IsLeftKeyDown = false;         if (key == "ArrowRight")             player.IsRightKeyDown = false;     }     void Smoke(Player player)     {         RallyXGame.SetSmoke(player.CurrentColumn, player.CurrentRow, player.PlayerNumber);         Task.Run(async () =>{             if (ClientProxyMap.ContainsKey(player.ConnectionID))                 await ClientProxyMap[player.ConnectionID].SendAsync("SmokeToClient");         });     } } | 
最後にRallyX.RallyXHubをProgram.csに追加します。
Program.cs
| 1 2 3 | // app.Run();の直前あたりに以下を追加 app.MapHub<RallyX.RallyXHub>("/RallyXHub"); | 
