Newer
Older
PixelPaintWar / docs / 05_TECH / TECH_03_描画最適化.txt
========================================================================
描画最適化 (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が同一ティックで重い計算を行うことを回避する