======================================================================== 描画最適化 (Rendering Optimization) ======================================================================== 1. 概要 (Overview) ------------------------------------------------------------------------ 1-1. 目的 PixiJSによるリアルタイム描画において,大人数・大マップでも60fpsを維持するための クライアント側の描画最適化手法をまとめる. 1-2. 主要な最適化手法 ・ビューポートカリング: 画面外のエンティティの描画をスキップする ・画面外プレイヤーの間引き更新: 不可視プレイヤーのtick頻度を削減する ・プレイヤー補間(LERP): リモートプレイヤーの位置を滑らかに補間する ・フレームデルタクランプ: 異常なデルタタイムを制限する ・テクスチャキャッシュ: 同一アセットの重複ロードを防止する ・マップの差分描画: 変化セルのみを再描画する ・Bot JITウォームアップ: ゲーム開始前にV8 JITコンパイルを促進する 2. ビューポートカリング (Viewport Culling) ------------------------------------------------------------------------ 2-1. 仕組み カメラの表示範囲外にあるエンティティのvisibleプロパティをfalseに設定し, PixiJSの描画パイプラインから除外する. 2-2. カリング対象 ・プレイヤー(PlayerView) ・ボム(BombView) ・ハリケーン(HurricaneOverlayController) 2-3. 判定方法 ・円-矩形交差判定(isCircleIntersectingViewport)を使用する ・エンティティの半径円とビューポート矩形の交差を判定する 2-4. カリングマージン ・ビューポートをGRID_CELL_SIZE分だけ拡張する(expandWorldViewport) ・マージンにより画面端でのエンティティのポップイン(突然の出現)を防止する 2-5. 遅延描画 ・ハリケーンは可視状態に変化した瞬間のみrenderDisplayFromState()を呼び出す ・不可視→可視の遷移時のみ再描画し,可視継続中は通常のtick更新のみ行う 3. 画面外プレイヤーの間引き更新 (Offscreen Update Throttling) ------------------------------------------------------------------------ 3-1. 仕組み 画面外のリモートプレイヤーのtick処理を6フレームに1回に間引く. 3-2. 実装 ・フレームカウンタ: frameCount % 6 === 0 のフレームのみ画面外プレイヤーをtickする ・デルタタイム補正: 間引き時は deltaSeconds × 6 を渡し,位置精度を維持する ・画面内プレイヤー: 毎フレーム通常のdeltaSecondsでtickする 3-3. 効果 ・画面外プレイヤーの更新コストを約1/6に削減する ・デルタタイム補正により,画面内に戻った際の位置ずれを防止する 4. プレイヤー補間 (Player Interpolation) ------------------------------------------------------------------------ 4-1. リモートプレイヤーのLERP サーバーから受信した目標座標に向けて,フレームごとに滑らかに補間移動する. ■ 計算式 newPos = currentPos + (targetPos - currentPos) × LERP_SMOOTHNESS × deltaTime ・LERP_SMOOTHNESS: 設定値で補間速度を制御する ・deltaTimeを乗算し,フレームレートに依存しない一定速度の補間を実現する 4-2. スナップ閾値 ・現在位置と目標位置の距離がPLAYER_LERP_SNAP_THRESHOLDを下回る場合,即座に目標位置に移動する ・微小な揺れ(ジッター)を防止する 4-3. 効果 ・20Hz(50ms間隔)の位置更新でも60fpsで滑らかに見える ・サーバーからの位置が急に変わった場合もスムーズに追従する 5. フレームデルタクランプ (Frame Delta Clamping) ------------------------------------------------------------------------ 5-1. 問題 フレーム落ちやタブ非表示からの復帰時に,deltaTimeが異常に大きくなり 物理計算の暴走(プレイヤーが瞬間移動する等)が発生する. 5-2. 解決策 ・resolveFrameDelta()関数でdeltaTimeの上下限を制限する ・下限: 0(負の値やNaNを防止する) ・上限: FRAME_DELTA_MAX_MS(設定された最大値) 5-3. 適用範囲 ・プレイヤーの移動計算 ・LERP補間 ・ボム・ハリケーンの状態更新 ・全ての時間依存処理がこのクランプ済みデルタを使用する 6. テクスチャキャッシュ (Texture Caching) ------------------------------------------------------------------------ 6-1. モジュールレベルキャッシュ SVGアセットのテクスチャをモジュールスコープの変数にキャッシュし, 複数のインスタンスから参照しても1回だけロードする. ■ キャッシュパターン ・cachedTexture: ロード完了後のTexture参照を保持する ・texturePromise: ロード中のPromise参照を保持する ・初回呼び出し: Assets.load()でPromiseを生成し,texturePromiseに保存する ・2回目以降: 既存のPromiseをawaitする(追加のフェッチは発生しない) ・ロード完了後: cachedTextureから即座に返す 6-2. キャッシュ対象 ・爆発エフェクト(bakuhatueffe.svg): RespawnEffectTextureCache ・ハリケーン(hurricane.svg): HurricaneTextureCache ・ボム(Bomb.svg): BombViewの静的テクスチャ 6-3. アセットプリロード ・preloadGameStartAssets()をゲーム開始前に呼び出す ・重いSVGアセットを事前にキャッシュし,初回出現時のフレーム落ちを防止する ・void(fire-and-forget)で呼び出し,ゲーム開始をブロックしない 7. マップの差分描画 (Differential Map Rendering) ------------------------------------------------------------------------ 7-1. セル単位の更新 ・GameMapViewはセルごとにGraphicsオブジェクトをコンストラクタで事前確保する ・renderCell(index, color)で変化したセルのみを再描画する ・全セルの再描画は行わない 7-2. 描画手順 1. 対象セルのGraphics.clear()で前回の描画を消去する 2. 新しい色でGraphics.fill()を適用する 3. 未塗装(-1)のセルは既定の背景色を使用する 7-3. リビジョン管理 ・GameMapModelがapplyUpdates()のたびにリビジョン番号をインクリメントする ・ミニマップ等のUI要素はリビジョン変化時のみ再描画する 8. 状態変化キャッシュ (State Change Caching) ------------------------------------------------------------------------ 8-1. ボムの描画スキップ ・BombViewが前回描画した状態(state, color, radiusGrid)を保持する ・状態が変化していない場合はrender()をスキップする ・armed状態のボムは毎フレーム位置更新のみ,外観は変化しない 8-2. ハリケーンの描画スキップ ・半径の変化が0.0001以下の場合はスプライトサイズの更新をスキップする ・画面外→画面内の遷移時のみrenderDisplayFromState()を実行する 8-3. ボムの重複チェック ・BombRepositoryがupsertBomb()時にisSameRenderPayload()で全フィールドを比較する ・同一ペイロードの場合はオブジェクトの破棄・再生成をスキップする 9. ゲームループのステップ分離 (Game Loop Step Architecture) ------------------------------------------------------------------------ 9-1. ステップ構成 GameLoopが毎フレーム以下の4ステップを順に実行する. 1. InputStep: ジョイスティック入力をPlayerControllerに適用する 2. SimulationStep: 自プレイヤーの移動 + リモートプレイヤーの補間 + カリング判定 3. BombStep: ボムの状態更新 + 爆発判定 + カリング適用 4. CameraStep: ワールドコンテナの位置を自プレイヤーに追従させる 9-2. 設計上の利点 ・各ステップが独立しており,個別に最適化できる ・共有のLoopFrameContext(deltaSeconds等)で一貫した時間管理を行う ・LoopFrameEffectsで移動状態をステップ間で受け渡す 10. リソース管理 (Resource Management) ------------------------------------------------------------------------ 10-1. DisposableRegistry ・ゲームシーンのリソース(イベントリスナー,PixiJSオブジェクト等)を登録する ・disposeAll()で登録の逆順に破棄し,依存関係のクラッシュを防止する ・シーン遷移時にメモリリークを防止する 10-2. プレイヤーの遅延テクスチャロード ・初期状態ではTexture.WHITE(1×1ピクセル)で描画を開始する ・非同期でプレイヤー画像をロードし,完了後にスプライトのテクスチャを差し替える ・ゲーム画面の初期表示をブロックしない 11. Bot JITウォームアップ (Bot JIT Warmup) ------------------------------------------------------------------------ 11-1. 問題 ゲーム開始直後の最初のティックでBot全員のdecide()が初めて呼ばれると, V8エンジンのJITコンパイルが集中し,ティック処理時間がスパイクする. 11-2. 解決策 ・ゲーム開始前の待機時間(GAME_START_DELAY_MS: 5000ms)を利用する ・warmUp()メソッドで全BotのbotTurnOrchestrator.decide()を1回ずつ空実行する ・V8がホットパスを事前にコンパイルし,初回ティックのスパイクを軽減する 11-3. ターゲット初期化の分散 ・各Botのインデックスに応じてchooseNextTarget()の呼び出し回数を分散する ・chainCount = (botIndex % 4) + 1 で1〜4回の初期化を割り当てる ・全Botが同一ティックで重い計算を行うことを回避する