クソゲーに魂を!プロジェクト(4)の続きです。今回はサーバーサイドとクライアントサイドでデータのやりとりをするためのGameHubクラスを定義します。

準備

Microsoft.AspNetCore.SignalR.Hubクラスを継承してGameHubクラスを定義します。

インデントが深くなるので名前空間部分は省略します。

VisualStudioのプロジェクト作成でASP.NET Core Webアプリを選択を選択するとコードが自動生成されるのですが、Program.csに以下のコードを追加します。追加するのは2行だけでそれ以外は既存のコードです。

Program.cs

接続時の処理

ハブ メソッドの各呼び出しは、新しいハブ インスタンスで実行されるため、変数はすべて静的変数として定義しています。

はじめてユーザーが接続してきたときにタイマーの初期化をおこないます。Elapsedイベントが1秒間に60回発生するようにしてイベントハンドラを追加します。またゲーム(バトル)開始と終了、敵を倒したとき、ゲームオーバー時に呼び出されるイベントハンドラも追加します。

その後、ASP.NET SignalRで使われる一意の接続IDとIClientProxyをConnectionIdClientPairsに登録し(これが現在接続している唯一のユーザーである場合はGameオブジェクトの初期化もおこなう)、生成されたPlayerオブジェクトの取得、バトルフィールド上の餌や弾丸、他のプレイヤーの位置情報などを文字列にして取得したあとクライアントサイドに送信します。

イベントハンドラの追加

新しいバトルが開始されたときに呼び出されるイベントハンドラを示します。

バトルが終了されたときに呼び出されるイベントハンドラを示します。いったん更新処理を停止してバトルの結果(優勝者と優勝者が得たボーナスポイント)をクライアントサイドに送信します。そして3秒後に新しいバトルを開始し更新処理を再開しています。

敵を倒したときに呼び出されるイベントハンドラを示します。

ゲームオーバーになったときに呼び出されるイベントハンドラを示します。

データの送信

クライアントサイドに送信するプレイヤーの位置情報などの文字列を取得する処理を示します。送信するのは、PlayerのID、頭の座標、進行方向、キャンバスに表示するPlayerの名前、スコア、体長、そのプレイヤーが倒した敵の数、そのプレイヤーはユーザーかNPCか?現在旋回中か?スネークの身体を構成する各円の座標です。

クライアントサイドに送信する弾丸の位置情報の文字列を取得する処理を示します。送信するのは、弾丸のID、座標、進行方向、弾丸のタイプ、生存期間です。

クライアントサイドに送信する餌の位置情報の文字列を取得する処理を示します。送信するのは、餌のID、座標、進行方向です。

Ping値を計測できるようにする

Ping値を計測できるようにしておきます。やっていることはクライアントサイドからPingメソッドが呼び出されたら同じクライアントに”SendToClientPing”を送信しているだけです。

切断時の処理

ユーザーがページから離脱したときにおこなわれる処理を示します。

ConnectionIdClientPairsに登録されているASP.NET SignalRで使われる一意の接続IDとIClientProxyを削除するのですが、ユーザーの意思で切断されたのではなく一時的な通信障害で切れてしまったときは再接続してゲームを継続できるようにしなければなりません。なので3秒間待機しています。3秒間待っても再接続されないときはユーザーの意思で切断されたと判断して辞書からIDを削除する処理を実行しています。

またユーザーがページから離脱することでユーザーの接続数が0になったときはサーバーへの負荷を軽減するために更新処理を停止しています。

なんらかの原因で通信が切断されたが再接続に成功したときの処理を示します。再接続に成功したときにASP.NET SignalRで使われる一意の接続IDが変更されてしまうのでGame.ChangeConnectionIDメソッド(既出)でID変更の処理をしています。

ゲーム開始時の処理

ゲーム開始時の処理を示します。

誰もゲームに参加しているユーザーが誰もいないときはGameオブジェクトを初期化します。そのあとPlayerオブジェクトを取得して前回のプレイの値が残っているので初期化します。ただPlayer.DeadCount(同じステージでゲームオーバーになった回数)はリセットしません。これは意図的にゲームオーバーになって初期状態という有利な状態でゲームに再参加することを防ぐためのものです。

取得されたPlayerオブジェクトに初期座標と長さ、PlayerNameを設定し、Game.AllPlayersに追加します。これらの処理が滞りなくおこなわれたらクライアントサイドに”SendToClientGameStartSuccessful”を送信します。

ユーザーの操作に対応させる

ユーザーが方向転換や弾丸を発射しようとしたときにおこなわれる処理を示します。TurnLeftメソッドとTurnRightメソッドはキーを押下したときの処理、TurnByMouseメソッドはマウスで操作しようとしたときにおこなわれる処理です。

弾丸発射時の処理を示します。自機に対応するPlayerオブジェクトを取得してShotメソッドを呼び出します。弾丸発射の処理が成功したら効果音を鳴らすためにクライアントサイドに”SendToClientShoted”を送信します。

更新処理

更新時におこなわれる処理を示します。これまでは更新処理がおこなわれるたびに各オブジェクトの位置情報をすべて送信していましたが、これでは送信量が多くなりカクツキの原因になるので送信するデータを減らします。

プレイヤーは方向転換しなければまっすぐ進みます。また旋回しているときも旋回速度は一定です。いうまでもなく直進時は旋回速度は0(一定)です。

なので旋回開始時と旋回終了時のみクライアント時にそれを通知すればよいことになります。このことは餌や弾丸でも同じです。ただこのような差分だけを送り続けると誤差が発生するのでときどき全データを送ることにします。これで送信するデータ量を大きく減らすことができそうです。

生成されたばかりのオブジェクトを送信するための文字列の生成

送信するデータは生成されたばかりのオブジェクト、更新によって状態が変化したオブジェクトをそれぞれ文字列に変換することで生成します。

生成されたばかりのPlayerが存在する場合はそれをクライアントサイドに送信しなければなりません。Game.GetCreatedPlayersStringメソッドはそのための文字列を生成します。

生成されたばかりの弾丸がある場合はそれをクライアントサイドに送信しなければなりません。Game.GetCreatedBulletsStringメソッドはそのための文字列を生成します。

生成されたばかりの餌がある場合はそれをクライアントサイドに送信しなければなりません。Game.GetFoodCreateStringメソッドはそのための文字列を生成します。

状態が変化したオブジェクトを送信するための文字列の生成

旋回を開始したり停止したPlayerがいる場合はそれをクライアントサイドに送信しなければなりません。Game.GetPlayersRotateChangedStringメソッドはそのための文字列を生成します。旋回を開始したとか停止したのはCircleオブジェクトの回転処理がおこなわれた回数が保存されている部分を比較してみればわかります。

体長が変化したPlayerがいる場合はそれをクライアントサイドに送信しなければなりません。Game.GetPlayersLengthChangedStringメソッドはそのための文字列を生成します。

スコアが変化したPlayerがいる場合はそれをクライアントサイドに送信しなければなりません。Game.GetPlayersScoreChangedStringメソッドはそのための文字列を生成します。

倒した敵の数が変化したPlayerがいる場合はそれをクライアントサイドに送信しなければなりません。Game.GetPlayersKillCountChangedStringメソッドはそのための文字列を生成します。

当たり判定が無効になっているPlayerがいる場合はそれをクライアントサイドに送信しなければなりません。Game.NoHitCheckPlayerIDsToStringメソッドはそのための文字列を生成します。

進行方向が変化した餌がある場合はそれをクライアントサイドに送信しなければなりません。Game.GetFoodVelocityChengedStringメソッドはそのための文字列を生成します。

Game.GetStringForUpdateFieldStatusメソッドはバトルフィールドの状態(生存しているPlayerの数と残り時間)を文字列に変換します。

全データの送信用の文字列の生成

サーバーサイドとクライアントサイドのデータにズレがないように時々全データを送信して確認処理をおこないます。そのための文字列を生成する処理を示します。