======================================================================== 通信最適化 (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を超えた場合はキャッチアップ機構が作動する