========================================================================
描画最適化 (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が同一ティックで重い計算を行うことを回避する