Slither.io (スリザリオ)のようなオンラインゲームを作りたい(3)の続きです。今回はゲーム全体の処理をおこなうGameクラスを定義します。

フィールド変数

Gameクラスのフィールド変数を示します。コンストラクタでおこなわれる処理はありません。

_playersDicはASP.NET SignalRで使われる一意の接続IDから対応するPlayerオブジェクトを高速に求めるための辞書です。_dicSentDataはクライアントサイドに同じCircleオブジェクトの情報を何度も送信しないようにするために送信済みのCircleオブジェクトのIDを格納しておくための辞書です。

初期化

ユーザーがひとりしかいない状態でゲームを開始するときはゲームオブジェクトを初期化します。そのときにおこなわれる処理を示します。

初期化でおこなわれる処理は以下のとおりです。

送信データのクリア
CirclesMapに格納されているCircleオブジェクトの回収
CirclesMapの初期化
Playerオブジェクトが格納されているリストと辞書のクリア
NPCと餌の再生成

NPCを初期化する処理を示します。

完全にフィールドに含まれているセルを取得したあとセルをキーに乱数を値にした辞書を生成します。そのあと値でソートして先頭からN個とればランダムにセルを選択することができます。選択したセルの中心にNPCを配置します。

餌を初期化する処理を示します。

プレイヤーの追加と削除

新しいプレイヤー(NPCではない)をゲームに追加する処理を示します。ここでは新しいPlayerオブジェクトを生成してリストと辞書に追加しています。またプレイヤーが出現した直後に敵にぶつかって死なないように出現させるセルは他のプレイヤーやNPCがいないセルを選んでいます。そのようなセルが存在しない場合は中央に出現させます。

ユーザーがページにアクセスしたときにフィールド上の餌や移動する他のプレイヤーを表示させるために新しいPlayerオブジェクトを追加する処理をしめします。

これはASP.NET SignalRで使われる一意の接続IDから対応するPlayerオブジェクトを取得する処理です。

ユーザーが離脱したときにPlayerオブジェクトを削除する処理を示します。

Playerオブジェクト内のCircleオブジェクトを回収してからリストと辞書からPlayerオブジェクトを削除します。そのあと残った総プレイヤー数によってはNPCを再生成して全体の数を調整します。

更新処理

更新時におこなわれる処理は以下のとおりです。

Playerの状態の更新
当たり判定(2回に1回)
セル内の餌の数を調べて0のときは追加する(1分に1回)
不要なPlayerオブジェクト(NPC)の除去
次回更新処理の最初に座標が変更されたCircleオブジェクトのリストをクリアする

当たり判定

当たり判定では以下の処理をおこないます。

フィールドの境界線に衝突していたら死亡処理
頭部が餌や他のプレイヤーに接触しているか調べる
他のプレイヤーに接触することなく餌に接触していたら餌を食べる
(この場合はPlayer.Scoreをインクリメントして餌に相当するCircleオブジェクトを回収する)
他のプレイヤーに接触していた場合は死亡処理
(この場合は接触した相手のPlayer.KillCountをインクリメントする。それがNPCでない場合はイベントを発生させる)
16更新に1回の割合でNPCに衝突回避行動をさせる

プレイヤーがフィールドの境界線に衝突しているかどうかを調べる処理を示します。頭部の中心とフィールドの中心間の距離に頭部の半径を加えたものがフィールドの半径を超えていたらフィールドの境界線に衝突したと判定します。

死亡処理

死亡時は以下の処理がおこなわれます。

死亡したプレイヤーのPlayer.Circlesをクリア
Player.Circlesに格納されていたCircleオブジェクトはすべて回収し、CirclesMapからも忘れず削除する
遺体?の周辺に餌を生成して配置する
死亡したのがNPCの場合は別の場所で復活させる(NPC+プレイヤーの総数によっては復活させずに除去)
ユーザーの場合はGameOveredEventイベントの送信

クライアントサイドに送信するデータの取得

ここからはクライアントサイドに送信するデータを取得する処理を示します。

ユーザーの状態

以下はユーザーが担当するプレイヤーの状態(現在位置、playerID、体長)をカンマ区切りの文字列に変換したものを取得するメソッドです。

フィールドの状態

以下はプレイヤーとNPC、餌の総数、存在するCircleオブジェクトの総数をカンマ区切りの文字列に変換したものを取得するメソッドです。

Circleオブジェクトの状態

CircleToStringメソッドは引数として渡されたCircleオブジェクトの座標などをタブ文字区切りの文字列に変換したものを返します。クライアントサイドに送る文字数を減らすために餌ならCircleオブジェクトのIDと座標(整数部分だけ)のみ、スネークの身体を構成するオブジェクトはこれに加えて半径とプレイヤーID、そのCircleオブジェクトは頭から何番目かと体長を示す整数のみを文字列に変換するものとし、頭部のみ倒したプレイヤーの数とプレイヤー名も加えた文字列を送ることにします。

GetStringForUpdateCirclesメソッドは上記のCircleToStringメソッドで変換された文字列を改行文字で連結させた文字列を取得するのですが、前回更新時と比較して追加すべきものと削除すべきものにわけて取得します。削除すべきものはすでにフィールド上からは消えているので必要な情報はCircleオブジェクトのIDだけです。

まずCirclesMap.GetNearCirclesメソッドで自分が存在する座標を中心にゲーム画面を表示させるcanvasの幅と高さ分を加えた周囲のCircleオブジェクトを取得します。次に取得された情報がすでにクライアントサイドに送信されたかを調べます。これは_dicSentDataを調べればわかります。もしplayerIDのキーが存在しない場合ははじめての送信なのでキーを生成します。そのあとCiecleオブジェクトを文字列に変換したものを送信して、そのIDを辞書に登録します。

2回目以降は前回送信したものが辞書に記録されているので送る前に送信済みかどうかを調べます。CirclesMap.GetNearCirclesメソッドで取得したオブジェクトが辞書に登録されていない場合は新しく生成されたオブジェクトなので送信しなければならないし、取得されたCircleオブジェクトのなかに辞書に登録されているIDをもつオブジェクトが存在しない場合はフィールド上から消滅したということなので削除された旨をクライアントサイドに送信しなければなりません。

また送信済みではあるけどCircle.IsPositionUpdatedフラグがtrueになっているオブジェクトは座標の整数部分が変更されたということなので、これも座標が変更されたという情報を付加して送信しなければなりません。

最後に追加されたCircleオブジェクトを改行文字で連結した文字列と削除されたCircleオブジェクトのIDをカンマで連結した文字列を返します。

ゲーム画面の下部には現在プレイ中のプレイヤーの情報(名前、スコア、位置)を表示させます。GetStringForUpdatePlayersStatusメソッドはそのために必要な文字列を取得するためのメソッドです。

スコアランキングへの登録

ゲームオーバーになったらスコアランキングに登録します。

スコアを管理するHiscoreクラスを定義します。プレイヤー名とスコア、倒した敵の数、時刻をひとつのデータにしています。

ランキングデータをファイルに書き込んだり読み出すScoreRankingクラスを定義します。ファイルに書き込んでいる最中にファイルからの読み出しの処理がおこなわれる可能性があるので同期オブジェクトで排他ロックをかけています。