クソゲーに魂を!プロジェクト(4)の続きです。今回はサーバーサイドとクライアントサイドでデータのやりとりをするためのGameHubクラスを定義します。
Contents
準備
Microsoft.AspNetCore.SignalR.Hubクラスを継承してGameHubクラスを定義します。
1 2 3 4 5 6 7 8 |
using Microsoft.AspNetCore.SignalR; namespace FireSnake { public class GameHub : Hub { } } |
インデントが深くなるので名前空間部分は省略します。
1 2 3 |
public class GameHub : Hub { } |
VisualStudioのプロジェクト作成でASP.NET Core Webアプリを選択を選択するとコードが自動生成されるのですが、Program.csに以下のコードを追加します。追加するのは2行だけでそれ以外は既存のコードです。
Program.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); builder.Services.AddSignalR(); // これを追加 var app = builder.Build(); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); } app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); app.MapHub<SnakeGame.GameHub>("/fire-snake-hub"); // これを追加 app.Run(); |
接続時の処理
ハブ メソッドの各呼び出しは、新しいハブ インスタンスで実行されるため、変数はすべて静的変数として定義しています。
1 2 3 4 5 6 7 8 |
public class GameHub : Hub { static Game Game = new Game(); // 前述のGameクラスのインスタンス static bool IsFirstConnection = true; // OnConnectedAsync()がはじめて呼び出された static System.Timers.Timer Timer = new System.Timers.Timer(); // 更新処理用のタイマー static Dictionary<string, IClientProxy> ConnectionIdClientPairs = new Dictionary<string, IClientProxy>(); // ASP.NET SignalRで使われる一意の接続IDからIClientProxyを求めるための辞書 static object syncGame = new object(); // 非同期処理でforeachを実行したときに例外が発生しないようにするための同期オブジェクト } |
はじめてユーザーが接続してきたときにタイマーの初期化をおこないます。Elapsedイベントが1秒間に60回発生するようにしてイベントハンドラを追加します。またゲーム(バトル)開始と終了、敵を倒したとき、ゲームオーバー時に呼び出されるイベントハンドラも追加します。
その後、ASP.NET SignalRで使われる一意の接続IDとIClientProxyをConnectionIdClientPairsに登録し(これが現在接続している唯一のユーザーである場合はGameオブジェクトの初期化もおこなう)、生成された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 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public class GameHub : Hub { public override async Task OnConnectedAsync() { if (IsFirstConnection) { IsFirstConnection = false; Timer.Interval = 1000 / Constant.UPDATE_PAR_SECOND; // 1000 / 60 Timer.Elapsed += Timer_Elapsed; Game.MatchBegun += Game_MatchBegun; Game.GameFinished += Game_GameFinished; Game.KillEvent += OnKillPlayer; Game.GameOveredEvent += OnPlayerGameOvered; } await base.OnConnectedAsync(); Player? player = null; long playerID = -1; string playersString = "", foodsString = "", bulletsString = ""; lock (syncGame) { ConnectionIdClientPairs.Add(Context.ConnectionId, Clients.Caller); if (ConnectionIdClientPairs.Count == 1) { Game.Init(); Timer.Start(); } player = Game.AddPlayer(Context.ConnectionId); if (player != null) playerID = player.PlayerID; playersString = Game.GetPlayersString(); bulletsString = Game.GetBulletString(); foodsString = Game.GetFoodString(); } await Clients.Caller.SendAsync("SendToClientConnectionSuccessful", Context.ConnectionId, playerID, Constant.INIT_FIELD_RADUUS, playersString, foodsString, bulletsString); } } |
イベントハンドラの追加
新しいバトルが開始されたときに呼び出されるイベントハンドラを示します。
1 2 3 4 5 6 7 8 9 10 11 |
public class GameHub : Hub { private void Game_MatchBegun(object? sender, EventArgs e) { lock (syncGame) { foreach (var pair in ConnectionIdClientPairs) pair.Value.SendAsync("MatchBegun"); } } } |
バトルが終了されたときに呼び出されるイベントハンドラを示します。いったん更新処理を停止してバトルの結果(優勝者と優勝者が得たボーナスポイント)をクライアントサイドに送信します。そして3秒後に新しいバトルを開始し更新処理を再開しています。
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 GameHub : Hub { private void Game_GameFinished(object sender, Game.WinnerArgs e) { Task.Run(async () => { Timer.Stop(); await Task.Delay(100); lock (syncGame) { string winnerName = "該当者なし"; if (e.Winner != null) { winnerName = e.Winner.PlayerName; if (e.Winner.ConnectionId != "" && ConnectionIdClientPairs.ContainsKey(e.Winner.ConnectionId)) ConnectionIdClientPairs[e.Winner.ConnectionId].SendAsync("SendToClientWin"); } foreach (var pair in ConnectionIdClientPairs) { Player? player = Game.GetPlayer(pair.Key); if (player != null) pair.Value.SendAsync("SendToClientMatchIsFinished", winnerName, e.Bonus); } } await Task.Delay(3000); lock (syncGame) Game.NewStage(); Timer.Start(); }); } } |
敵を倒したときに呼び出されるイベントハンドラを示します。
1 2 3 4 5 6 7 8 9 10 11 |
public class GameHub : Hub { private void OnKillPlayer(object sender, string id) { lock (syncGame) { if (ConnectionIdClientPairs.ContainsKey(id)) ConnectionIdClientPairs[id].SendAsync("SendToClientKillPlayer"); } } } |
ゲームオーバーになったときに呼び出されるイベントハンドラを示します。
1 2 3 4 5 6 7 8 9 10 11 |
public class GameHub : Hub { private void OnPlayerGameOvered(object sender, string id) { lock (syncGame) { if (ConnectionIdClientPairs.ContainsKey(id)) ConnectionIdClientPairs[id].SendAsync("SendToClientGameOvered"); } } } |
データの送信
クライアントサイドに送信するプレイヤーの位置情報などの文字列を取得する処理を示します。送信するのは、PlayerのID、頭の座標、進行方向、キャンバスに表示する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 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 |
public class Game { public string GetPlayersString() { List<string> list = new List<string>(); foreach (Player player in AllPlayers) { if (player.IsDead) continue; List<string> vs = new List<string>(); long pID = player.PlayerID; // PlayerID double x = player.HeadX; // 頭の座標 double y = player.HeadY; double angle = player.Angle; // 進行方向 string name = player.PlayerShortName; // キャンバスに表示するPlayerの名前 int score = player.TotalScore; // スコア double length = player.Length; // 体長 int killCount = player.KillCount; // 倒した敵の数 string isPlayer = player.ConnectionId != "" ? "true" : "false"; // ユーザーかNPCか? string rotating = ""; if (player.Circles.Length > 1) // 現在旋回中か? { int r0 = player.Circles[0].RotateCount; int r1 = player.Circles[1].RotateCount; if (r0 > r1) rotating = "right"; if (r0 < r1) rotating = "left"; } vs.Add(pID.ToString()); vs.Add(x.ToString()); vs.Add(y.ToString()); vs.Add(angle.ToString()); vs.Add(name); vs.Add(isPlayer); vs.Add(length.ToString()); vs.Add(rotating); int len = player.Circles.Length; for (int i = 0; i < len; i++) { vs.Add(player.Circles[i].X.ToString()); // 身体を構成する各円の座標 vs.Add(player.Circles[i].Y.ToString()); } list.Add(string.Join("\t", vs)); } return string.Join("\n", list); } } |
クライアントサイドに送信する弾丸の位置情報の文字列を取得する処理を示します。送信するのは、弾丸のID、座標、進行方向、弾丸のタイプ、生存期間です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class Game { public string GetBulletString() { List<string> list = new List<string>(); foreach (Bullet bullet in Bullets) { if (bullet.IsDead) continue; list.Add($"{bullet.ID},{bullet.X},{bullet.Y},{bullet.VX},{bullet.VY},{bullet.Type},{bullet.Life}"); } return string.Join(",", list); } } |
クライアントサイドに送信する餌の位置情報の文字列を取得する処理を示します。送信するのは、餌のID、座標、進行方向です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class Game { public string GetFoodString() { List<string> list = new List<string>(); foreach (Food food in Foods) { if (food.IsDead) continue; list.Add($"{food.ID},{food.X},{food.Y},{food.VX},{food.VY}"); } return string.Join(",", list); } } |
Ping値を計測できるようにする
Ping値を計測できるようにしておきます。やっていることはクライアントサイドからPingメソッドが呼び出されたら同じクライアントに”SendToClientPing”を送信しているだけです。
1 2 3 4 5 6 7 |
public class GameHub : Hub { public async void Ping(long time) { await Clients.Caller.SendAsync("SendToClientPing", time); } } |
切断時の処理
ユーザーがページから離脱したときにおこなわれる処理を示します。
ConnectionIdClientPairsに登録されているASP.NET SignalRで使われる一意の接続IDとIClientProxyを削除するのですが、ユーザーの意思で切断されたのではなく一時的な通信障害で切れてしまったときは再接続してゲームを継続できるようにしなければなりません。なので3秒間待機しています。3秒間待っても再接続されないときはユーザーの意思で切断されたと判断して辞書からIDを削除する処理を実行しています。
またユーザーがページから離脱することでユーザーの接続数が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 |
public class GameHub : Hub { public override async Task OnDisconnectedAsync(Exception? exception) { await base.OnDisconnectedAsync(exception); await Task.Delay(3000); lock (syncGame) { ConnectionIdClientPairs.Remove(Context.ConnectionId); Player? removedPlayer = Game.RemovePlayer(Context.ConnectionId); // 一時的な切断で再接続が成功したときはPlayerのIDが変更されるので // removedPlayer == nullとなる。このときは何もしない if (removedPlayer != null) { removedPlayer.IsDead = true; if (UsersCount == 0) ConnectionIdClientPairs.Clear(); if (ConnectionIdClientPairs.Count == 0) { Timer.Stop(); Game.UnInit(); } } } } } |
なんらかの原因で通信が切断されたが再接続に成功したときの処理を示します。再接続に成功したときにASP.NET SignalRで使われる一意の接続IDが変更されてしまうのでGame.ChangeConnectionIDメソッド(既出)でID変更の処理をしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class GameHub : Hub { public void OnReConnected(string curConnectionID, string oldConnectionID) { lock (syncGame) { Game.RemovePlayer(curConnectionID); Player? player = Game.GetPlayer(oldConnectionID); if (player != null) Game.ChangeConnectionID(player, oldConnectionID, curConnectionID); } } } |
ゲーム開始時の処理
ゲーム開始時の処理を示します。
誰もゲームに参加しているユーザーが誰もいないときはGameオブジェクトを初期化します。そのあとPlayerオブジェクトを取得して前回のプレイの値が残っているので初期化します。ただPlayer.DeadCount(同じステージでゲームオーバーになった回数)はリセットしません。これは意図的にゲームオーバーになって初期状態という有利な状態でゲームに再参加することを防ぐためのものです。
取得されたPlayerオブジェクトに初期座標と長さ、PlayerNameを設定し、Game.AllPlayersに追加します。これらの処理が滞りなくおこなわれたらクライアントサイドに”SendToClientGameStartSuccessful”を送信します。
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 GameHub : Hub { public async void GameStart(string playerName) { string connectionId = Context.ConnectionId; Player? player = null; lock (syncGame) { if (Game.Players.Count(_ => !_.IsDead) <= 0) Game.Init(); player = Game.GetPlayer(connectionId); if (player != null) { // 前回のプレイの値が残っているので初期化する player.Score = 0; player.TotalScore = 0; player.VictoryCount = 0; player.JoinInCount = 0; player.KillCount = 0; // player.DeadCount はリセットしてはならない // 初期座標と長さ、PlayerNameを設定する var ret = Game.GetPlayerInitPositionAngle(); player.Init(ret.X, ret.Y, ret.Angle); player.SetPlayerName(playerName); if (player.DeadCount > 0) player.Length = Constant.PLAYER_MIN_LENGTH; else { double len = Constant.PLAYER_INIT_LENGTH; Player[] players = Game.AllPlayers.Where(_ => _ != player && !_.IsDead).ToArray(); if (players.Length > 0) // 途中参加したときの体長は全Playerの平均値 len = players.Average(_ => _.Length); player.Length = len; } Game.AllPlayers.Add(player); } } if (player != null) await Clients.Caller.SendAsync("SendToClientGameStartSuccessful"); } } |
ユーザーの操作に対応させる
ユーザーが方向転換や弾丸を発射しようとしたときにおこなわれる処理を示します。TurnLeftメソッドとTurnRightメソッドはキーを押下したときの処理、TurnByMouseメソッドはマウスで操作しようとしたときにおこなわれる処理です。
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 GameHub : Hub { public void TurnLeft(bool b) { lock (syncGame) Game.GetPlayer(Context.ConnectionId)?.TurnLeft(b); } public void TurnRight(bool b) { lock (syncGame) Game.GetPlayer(Context.ConnectionId)?.TurnRight(b); } public void TurnByMouse(double d) { if (Global.IsRelease) { lock (syncGame) Game.GetPlayer(Context.ConnectionId)?.TurnByMouse(d); } } } |
弾丸発射時の処理を示します。自機に対応するPlayerオブジェクトを取得してShotメソッドを呼び出します。弾丸発射の処理が成功したら効果音を鳴らすためにクライアントサイドに”SendToClientShoted”を送信します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class GameHub : Hub { public async void Shot() { bool ret = false; lock (syncGame) { Player? player = Game.GetPlayer(Context.ConnectionId); if (player != null) ret = player.Shot(Game.Bullets); } if(ret) await Clients.Caller.SendAsync("SendToClientShoted"); } } |
更新処理
更新時におこなわれる処理を示します。これまでは更新処理がおこなわれるたびに各オブジェクトの位置情報をすべて送信していましたが、これでは送信量が多くなりカクツキの原因になるので送信するデータを減らします。
プレイヤーは方向転換しなければまっすぐ進みます。また旋回しているときも旋回速度は一定です。いうまでもなく直進時は旋回速度は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 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 GameHub : Hub { static long _countTimerElapsed = 0; static private void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) { try { IClientProxy[] clients; string updateText = ""; string fieldStatusText = ""; lock (syncGame) { if (ConnectionIdClientPairs.Count == 0) return; _countTimerElapsed++; string[] players = new string[8]; string[] bullets = new string[3]; string[] foods = new string[4]; // 生成されたばかりのPlayer、弾丸、餌の座標等を取得する players[0] = Game.GetCreatedPlayersString(); bullets[0] = Game.GetCreatedBulletsString(); foods[0] = Game.GetFoodCreateString(); // 更新処理をおこなう (List<long> DeadPlayers, List<long> DeadFoods, List<long> DeadBullets) deads = Game.Update(); // 更新処理後に状態が変化した部分を調べて文字列としてまとめる players[1] = Game.GetPlayersRotateChangedString(); players[2] = Game.GetPlayersLengthChangedString(); players[3] = Game.GetPlayersScoreChangedString(); players[4] = Game.GetPlayersKillCountChangedString(); players[5] = string.Join("\t", deads.DeadPlayers); players[6] = Game.NoHitCheckPlayerIDsToString(); foods[1] = Game.GetFoodVelocityChengedString(); foods[2] = string.Join("\t", deads.DeadFoods); bullets[1] = string.Join("\t", deads.DeadBullets); // 1秒に1回残り時間を減算してフィールドの半径を小さくする // フィールドの状態をクライアントサイドに送信するため送信する文字列も生成する if (_countTimerElapsed % 60 == 0) { Game.DecrementRemainingTime(); fieldStatusText = Game.GetStringForUpdateFieldStatus(); } // 3秒に1回だけ全データを送る if (_countTimerElapsed % 180 == 0) { players[7] = Game.GetPlayersStringForFix(); bullets[2] = Game.GetBulletsStringForFix(); foods[3] = Game.GetFoodsStringForFix(); } // この文字列を接続している全ユーザーに送る updateText = $"{Game.FieldRadius}<\n>{string.Join("\n", players)}<\n>{string.Join("\n", foods)}<\n>{string.Join("\n", bullets)}"; clients = ConnectionIdClientPairs.Values.ToArray(); } foreach (IClientProxy client in clients) { Task.Run(async () => { await client.SendAsync("SendToClientUpdate", updateText); if (fieldStatusText != "") await client.SendAsync("SendToClientUpdateFieldStatus", fieldStatusText); // 描画される文字列が変更されると見づらくなるので1秒に4回程度に抑える if (_countTimerElapsed % 16 == 0) { await client.SendAsync("SendToClientUpdatePlayersStatus"); await client.SendAsync("SendToClientUpdateRadar"); } }); } } catch (Exception ex) { } } } |
生成されたばかりのオブジェクトを送信するための文字列の生成
送信するデータは生成されたばかりのオブジェクト、更新によって状態が変化したオブジェクトをそれぞれ文字列に変換することで生成します。
生成されたばかりのPlayerが存在する場合はそれをクライアントサイドに送信しなければなりません。Game.GetCreatedPlayersStringメソッドはそのための文字列を生成します。
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 Game { public string GetCreatedPlayersString() { List<string> list = new List<string>(); foreach (Player player in AllPlayers) { if (player.IsDead) continue; if (player.Circles.Length == 1) { double angle = player.Angle; double x = player.HeadX; double y = player.HeadY; string name = player.PlayerShortName; string isPlayer = player.ConnectionId != "" ? "true" : "false"; list.Add($"{player.PlayerID}\t{angle}\t{x}\t{y}\t{name}\t{isPlayer}"); } } return string.Join("\t", list); } } |
生成されたばかりの弾丸がある場合はそれをクライアントサイドに送信しなければなりません。Game.GetCreatedBulletsStringメソッドはそのための文字列を生成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Game { public string GetCreatedBulletsString() { List<string> creates = new List<string>(); foreach (Bullet bullet in Bullets) { if (bullet.IsDead) continue; if (bullet.Life == Constant.BULLET_LIFE) creates.Add($"{bullet.ID}\t{bullet.X}\t{bullet.Y}\t{bullet.VX}\t{bullet.VY}\t{bullet.Type}\t{bullet.Life}"); } return string.Join("\t", creates); } } |
生成されたばかりの餌がある場合はそれをクライアントサイドに送信しなければなりません。Game.GetFoodCreateStringメソッドはそのための文字列を生成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Game { public string GetFoodCreateString() { List<string> creates = new List<string>(); foreach (Food food in Foods) { if (food.IsDead) continue; if (food.UpdateCount == 0 || food.UpdateCount == 1) creates.Add($"{food.ID}\t{food.X}\t{food.Y}\t{food.VX}\t{food.VY}"); } return string.Join("\t", creates); } } |
状態が変化したオブジェクトを送信するための文字列の生成
旋回を開始したり停止したPlayerがいる場合はそれをクライアントサイドに送信しなければなりません。Game.GetPlayersRotateChangedStringメソッドはそのための文字列を生成します。旋回を開始したとか停止したのはCircleオブジェクトの回転処理がおこなわれた回数が保存されている部分を比較してみればわかります。
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 |
public class Game { public string GetPlayersRotateChangedString() { List<string> list = new List<string>(); foreach (Player player in AllPlayers) { if (player.IsDead) continue; if (player.Circles.Length == 2) { int r0 = player.Circles[0].RotateCount; int r1 = player.Circles[1].RotateCount; string ev = ""; if (r0 > r1) // 右旋回を開始した ev = "r"; if (r0 < r1) // 左旋回を開始した ev = "l"; if (ev != "") list.Add($"{player.PlayerID}\t{ev}"); } else if (player.Circles.Length >= 3) { int r0 = player.Circles[0].RotateCount; int r1 = player.Circles[1].RotateCount; int r2 = player.Circles[2].RotateCount; string ev = ""; if ((r1 == r2 || r1 < r2) && r0 > r1) // 右旋回を開始した ev = "r"; if ((r1 == r2 || r1 > r2) && r0 < r1) // 左旋回を開始した ev = "l"; if (r1 != r2 && r0 == r1) // 旋回を停止した ev = "s"; if (ev != "") list.Add($"{player.PlayerID}\t{ev}"); } } return string.Join("\t", list); } } |
体長が変化したPlayerがいる場合はそれをクライアントサイドに送信しなければなりません。Game.GetPlayersLengthChangedStringメソッドはそのための文字列を生成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Game { public string GetPlayersLengthChangedString() { List<string> list = new List<string>(); foreach (Player player in AllPlayers) { if (player.IsDead) continue; if (player.Circles.Length >= 2) { int len0 = player.Circles[0].PlayerLength; int len1 = player.Circles[1].PlayerLength; if (len0 != len1) // 体長が前回更新時とは違っている list.Add($"{player.PlayerID}\t{len0}"); } } return string.Join("\t", list); } } |
スコアが変化したPlayerがいる場合はそれをクライアントサイドに送信しなければなりません。Game.GetPlayersScoreChangedStringメソッドはそのための文字列を生成します。
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 Game { public string GetPlayersScoreChangedString() { List<string> list = new List<string>(); foreach (Player player in AllPlayers) { if (player.IsDead) continue; if (player.ConnectionId != "" && player.Circles.Length >= 2) { if (player.Circles.Length >= 2) { int score0 = player.Circles[0].Score; int score1 = player.Circles[1].Score; if (score0 != score1) // スコアが前回更新時とは違っている list.Add($"{player.PlayerID}\t{score0}"); } } } return string.Join("\t", list); } } |
倒した敵の数が変化したPlayerがいる場合はそれをクライアントサイドに送信しなければなりません。Game.GetPlayersKillCountChangedStringメソッドはそのための文字列を生成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Game { public string GetPlayersKillCountChangedString() { List<string> list = new List<string>(); foreach (Player player in AllPlayers) { if (player.IsDead) continue; if (player.Circles.Length >= 2) { int kill0 = player.Circles[0].KillCount; int kill1 = player.Circles[1].KillCount; if (kill0 != kill1) list.Add($"{player.PlayerID}\t{kill0}"); } } return string.Join("\t", list); } } |
当たり判定が無効になっているPlayerがいる場合はそれをクライアントサイドに送信しなければなりません。Game.NoHitCheckPlayerIDsToStringメソッドはそのための文字列を生成します。
1 2 3 4 5 6 7 8 |
public class Game { public string NoHitCheckPlayerIDsToString() { long[] arr = Players.Where(_ => _.NoHitCheckValue > 0).Select(_ => _.PlayerID).ToArray(); return string.Join("\t", arr); } } |
進行方向が変化した餌がある場合はそれをクライアントサイドに送信しなければなりません。Game.GetFoodVelocityChengedStringメソッドはそのための文字列を生成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class Game { public string GetFoodVelocityChengedString() { List<string> list = new List<string>(); foreach (Food food in Foods) { if (food.IsDead || (food.OldVX == 0 && food.OldVY == 0) || (food.OldVX == food.VX && food.OldVY == food.VY)) continue; list.Add($"{food.ID}\t{food.X}\t{food.Y}\t{food.VX}\t{food.VY}"); } return string.Join("\t", list); } } |
Game.GetStringForUpdateFieldStatusメソッドはバトルフィールドの状態(生存しているPlayerの数と残り時間)を文字列に変換します。
1 2 3 4 5 6 7 8 9 |
public class Game { public string GetStringForUpdateFieldStatus() { int playersCount = Players.Count(_ => !_.IsDead); int npcsCount = NPCs.Count(_ => !_.IsDead); return $"{playersCount},{npcsCount},{RemainingTime}"; } } |
全データの送信用の文字列の生成
サーバーサイドとクライアントサイドのデータにズレがないように時々全データを送信して確認処理をおこないます。そのための文字列を生成する処理を示します。
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 |
public class Game { public string GetPlayersStringForFix() { List<string> list = new List<string>(); foreach (Player player in AllPlayers) { if (player.IsDead) continue; list.Add($"{player.PlayerID}\t{player.HeadX}\t{player.HeadY}\t{player.Angle}"); } return string.Join("\t", list); } public string GetBulletsStringForFix() { List<string> list = new List<string>(); foreach (Bullet bullet in Bullets) { if (bullet.IsDead) continue; list.Add($"{bullet.ID}\t{bullet.X}\t{bullet.Y}\t{bullet.VX}\t{bullet.VY}\t{bullet.Life}"); } return string.Join("\t", list); } public string GetFoodsStringForFix() { List<string> list = new List<string>(); foreach (Food food in Foods) { if (food.IsDead) continue; list.Add($"{food.ID}\t{food.X}\t{food.Y}\t{food.VX}\t{food.VY}"); } return string.Join("\t", list); } } |