========================================================================
通信最適化 (Network Optimization)
========================================================================
1. 概要 (Overview)
------------------------------------------------------------------------
1-1. 目的
最大100人が同時接続するリアルタイム対戦ゲームにおいて,サーバー・クライアント間の通信帯域と
メッセージ数を最小化しつつ,低遅延な状態同期を実現する.
1-2. 主要な最適化手法
・AOI(関心領域)フィルタリング: プレイヤーの周辺データのみ送信する
・ティックバッチ送信: 50ms間隔で差分をまとめて1回送信する
・座標量子化: 浮動小数点座標を丸めて帯域を削減する
・差分同期: 前回送信値から変化した分のみ送信する
・マップセルのグループ化: セル更新をチームIDでまとめて送信する
2. AOI(関心領域)フィルタリング (Area of Interest)
------------------------------------------------------------------------
2-1. 仕組み
フィールド全体をAOIセル(3×3グリッド単位)に分割し,各プレイヤーの周辺ウィンドウ
(横5×縦3 AOIセル = 15×9グリッド)に含まれるデータのみを送信する.
2-2. AOIグリッド設定
・AOIセルサイズ: 3グリッド単位
・AOIウィンドウ: 横5セル × 縦3セル
2-3. フィルタリング対象
・プレイヤー位置更新(UPDATE_PLAYERS)
・ボム設置通知(BOMB_PLACED)
・ハリケーン更新(UPDATE_HURRICANES)
2-4. AOIセルキャッシュ
・各ソケットごとに前回のAOIセル座標を保持する
・プレイヤーのAOIセルが変化した場合のみウィンドウ再計算を行う
・同一セル内の移動ではフィルタリング処理をスキップする
2-5. プレイヤー可視性管理
各ソケットごとに以下のSetを保持し,AOI境界の出入りを検知する.
・visiblePlayerIds: 現在可視のプレイヤーIDセット
・visibleBombIds: 現在可視のボムIDセット
・visibleHurricaneIds: 現在可視のハリケーンIDセット
■ 出入り検知
・前回不可視 → 今回可視: NEW_PLAYERイベントを発行する
・前回可視 → 今回不可視: REMOVE_PLAYERイベントを発行する
・継続可視のプレイヤーのみUPDATE_PLAYERSに含める
3. 20Hzティック&バッチ送信 (Tick-Based Batching)
------------------------------------------------------------------------
3-1. ティックレート
・サーバーゲームループ: 20Hz(50ms間隔)
・スケジュール方式: setTimeoutで次回ティック時刻を正確に計算し,setIntervalのドリフトを回避する
3-2. バッチ処理
1ティックで以下の処理をまとめて実行し,結果を1回で送信する.
1. ハリケーンの位置更新と衝突判定
2. Bot AIの行動決定と位置更新
3. マップの塗り処理と競合判定
4. ボムの爆発判定(Bot向け)
5. 差分データの組み立て
3-3. 送信メッセージ(1ティックあたり最大)
・UPDATE_PLAYERS: 位置が変化したプレイヤーのみ
・UPDATE_MAP_CELLS: 塗り状態が変化したセルのみ
・UPDATE_HURRICANES: 状態が変化したハリケーンのみ
・変化がない種別のメッセージは送信しない
4. 座標量子化 (Position Quantization)
------------------------------------------------------------------------
4-1. プレイヤー座標
・量子化スケール: 100(小数第2位で丸める)
・計算式: Math.round(value × 100) / 100
・例: 3.14159 → 3.14
4-2. ハリケーン座標
・位置の量子化スケール: 10(小数第1位で丸める)
・回転の量子化スケール: 4(0.25刻み)
・プレイヤーより粗い精度で十分なため,帯域をさらに削減する
4-3. 3層の量子化パイプライン(プレイヤー位置)
1. クライアント送信時: 量子化後の座標が前回と同じなら送信をスキップする
2. サーバー受信時: 受信座標を再量子化して精度を統一する
3. サーバー送信時: 量子化後の座標で差分比較し,変化分のみ送信する
4-4. 異常値ガード
・NaN/Infinityの場合は0を返す
・量子化スケールが無効(0以下)の場合は元の値をそのまま返す
5. 差分同期 (Delta Synchronization)
------------------------------------------------------------------------
5-1. 汎用デルタ抽出ユーティリティ
・collectSyncDeltaEntries(): IDベースのスナップショット比較を行う汎用関数
・前回送信したスナップショットをMapで保持し,変化したエントリのみを返す
・プレイヤー位置同期とハリケーン同期の両方で共通利用する
5-2. プレイヤー位置の差分
・ソケットごとにlastSentPositionByPlayerIdを保持する
・量子化後の座標が前回と同じプレイヤーはUPDATE_PLAYERSから除外する
・切断プレイヤーのキャッシュは自動クリーンアップする
5-3. UPDATE_PLAYERSのteamId省略
・初回接続時のCURRENT_PLAYERSでteamIdを含む完全なPlayerDataを送信する
・以降のUPDATE_PLAYERSではid, x, yのみ送信し,teamIdを省略する
・毎ティックのペイロードサイズを削減する
6. マップセルのグループ化 (Grouped Cell Updates)
------------------------------------------------------------------------
6-1. データ形式
セル更新を個別のオブジェクト配列ではなく,チームIDをキーとしたグループ形式で送信する.
■ 変換前(個別形式)
[{ index: 5, teamId: 2 }, { index: 7, teamId: 2 }, { index: 10, teamId: 1 }]
■ 変換後(グループ形式: GroupedCellUpdates)
{ "2": [5, 7], "1": [10] }
6-2. 効果
・teamIdキーの重複を排除し,ペイロードサイズを削減する
・セル数が多いほど圧縮効率が高くなる
6-3. 差分のみ送信
・MapStoreのpendingUpdatesキューに変化セルのみを蓄積する
・既に同じチームで塗られているセルはキューに追加しない
・getAndClearUpdates()でキューの参照をゼロコピーで差し替える
7. クライアント側の送信最適化 (Client-Side Send Optimization)
------------------------------------------------------------------------
7-1. 位置送信の間引き
・PlayerMoveSenderが量子化後の座標を前回送信値と比較する
・変化がない場合は送信をスキップする
・停止時にはforce: trueオプションで最終座標を確実に送信する
7-2. 送信間隔
・PLAYER_POSITION_UPDATE_MS: 50ms間隔でゲームループから呼び出される
・ティックレートと同期し,サーバーが処理可能な頻度に制限する
8. パフォーマンス計測 (Performance Monitoring)
------------------------------------------------------------------------
8-1. 計測項目(1秒ウィンドウ単位)
・tickCount: ウィンドウ内のティック数
・avgTickMs: ティックあたりの平均処理時間
・maxTickMs: ピーク処理時間
・cpuUsagePct: ゲームロジックのCPU使用率(= totalTickMs / windowMs × 100)
・avgPayloadBytesPerTick: ティックあたりの平均ペイロードサイズ
・outboundBytesPerSec: 推定送信帯域(= avgPayload × playerCount × tickCount)
8-2. 活用
・1秒ごとにログ出力し,ボトルネックの検知に利用する
・ティック処理が50msを超えた場合はキャッチアップ機構が作動する