戦闘画面がチラつく
Description
戦闘時に画面全体が激しくチラつくことがよくある
リロードすると直ることが多い。
原因を調査し、修正する
現時点でとりあえず直したほうが良さそうなところまとめ
- 背景テクスチャはmemo化したほうがいい
- alta-entity.tsのsetTimeoutは戦闘終了時にクリアしたほうがいい
- AnimatedSprite.tsxの new Texture() の累積
- DamageDisplayの再利用(これでランファによる表示バグは出なくなった)
- 呼吸アニメ(これをやめるとprojectileの再レンダリングがめちゃ減る)
- battleAssetsにはAltaのテクスチャが含まれていません。AltaMasterのモーションパスを確認して、プリロードに追加する必要があります。
Comments (25)
Claude Codeで調査
主な原因
- 毎レンダリング時にTextureが再作成される(最重要)
BattleBackground.tsx:54 で:
<pixiSprite
texture={Texture.from(backgroundImagePath)} // ← 毎回新しいTextureが作成される
...
/>
Texture.from() がレンダリング毎に呼ばれ、GPUとの同期タイミングでチラつきが発生します。
- setIntervalによる高頻度state更新
メドロノヴァのフェードイン/アウト処理で毎16ms(60fps)に setWhiteOverlayAlpha を更新しており、背景を含むコンポーネント全体が再レンダリングされます。
- 「時々発生」「リロードで解消」の理由
- キャッシュ状態がランダムに異なるため毎回ではない
- リロードでキャッシュがクリアされ、新規作成されるため解消
多少よくなった気がするが、
修正しても、まだチラつく。他にも広範囲で再レンダリングされてるものがあるかも
広範囲レンダリングの問題箇所
背景以外で問題になりそうな箇所が見つかりました:
高優先度
- Battle.tsx - 画面揺れ処理(shakeOffset)
// メドロノヴァ発射中、16msのsetIntervalで画面全体が揺れる
const shakeInterval = setInterval(() => {
setShakeOffset({ x: (Math.random() - 0.5) * 4, y: (Math.random() - 0.5) * 4 });
}, 16); // ← PixiTickと非同期
- 背景と同じ問題(setIntervalとPixiTickの非同期化)
- 全コンポーネント(背景含む)が揺れ処理で再レンダリング
- BurstSkillOverlay.tsx
- useTickで毎フレーム複数のGraphics(背景、放射線20本、台形等)を描画
- バーストスキル発動中にチラつきが悪化する可能性
中優先度
- RearguardBarrierDestruction.tsx
- 200個のパーティクルを毎フレーム状態更新
- AltaCastingGauge.tsx
- Date.now()を使った点滅エフェクト
enemy_lanhuaのバーストスキル時のチラつきを解消するため、以下の最適化を行いました:
原因
戦闘画面で多数のコンポーネントが毎レンダリングごとにnew TextStyle()を作成しており、特にバーストスキル発動時にスキル名表示やダメージ表示が頻繁に更新されると、大量のオブジェクト生成が発生してチラつきの原因となっていました。
修正したファイル
| ファイル | 修正内容 |
|---|---|
| SkillNameDisplay.tsx | |
| Enemy.tsx | |
| DamageDisplay.tsx | |
| BurstSkillOverlay.ts | |
| Character.tsx | |
| Alta.tsx | |
| SummonEntity.tsx |
以前の修正(背景チラつき)
- BattleBackground.tsx - BackgroundSpriteをmemo化、Textureをusememo化
- Battle.tsx - 画面揺れ処理をsetIntervalからuseTick + useRefに変更
問題点をまとめると:
- SkillNameDisplay.tsx - バーストスキル発動時のスケールアニメーション中、毎フレームnew TextStyle()を作成
- Enemy.tsx - 敵表示時に毎フレームnew TextStyle()を作成
- DamageDisplay.tsx - ダメージ表示時に毎フレームnew TextStyle()を作成
- AnimatedSprite.tsx - keyframe変更のたびにnew Texture()とnew Rectangle()を作成
完了した最適化まとめ
戦闘演出のレンダリング最適化として、以下のファイルでメモ化を実施しました:
| ファイル | 最適化内容 |
|---|---|
| SkillNameDisplay.tsx | |
| Enemy.tsx | |
| DamageDisplay.tsx | |
| BurstSkillOverlay.ts | |
| Character.tsx | |
| Alta.tsx | |
| SummonEntity.tsx | |
| HPBar.tsx | |
| BackgroundOverlay.ts |
主な修正ポイント:
- new TextStyle()やnew Motion()を毎レンダリングで生成していた箇所をuseMemoでメモ化
- PixiJS Graphicsのdraw関数をuseCallbackでメモ化
- Reactのhooksルール違反(早期リターン後にhooksを呼ぶ)を修正
これにより、enemy_lanhuaのバーストスキル発動時のフリッカーが改善されているはずです。アプリケーションで動作確認をしてみてください。
かなり改善された様子。発生頻度は減ったが、まだチラつきが発生する
また背景が真っ暗になる時がたまにある。
⏺ 背景画像はサーバーから返されるアセットリストに含まれています(line 27-32)。しかし、Texture.from()の使い方に問題があるかもしれません。
⏺ Read(src/lib/assets.ts)
⎿ Read 157 lines
⏺ 問題を発見しました!
原因: BattleBackground.tsxでTexture.from()を使用していますが、これはPixiJSのAssetsキャッシュを参照しません。プリロードされたアセットはAssets.get()で取得する必要があります。
Texture.from(path) → URLから直接読み込み(キャッシュを無視)
Assets.get(alias) → プリロード済みキャッシュから取得
最初は大丈夫だが、戦闘を繰り返すたびにおかしくなっていく気がする。setTimeout系残ってるかも?
修正まとめ
戦闘終了後のクリーンアップ問題
| ファイル | 問題 | 修正内容 |
|---|---|---|
| alta-entity.t | ||
| Battle.tsx |
altaの姿が描画されないことがある
一度何かしらの描画関連の不具合に遭遇すると、リロードするまで毎回戦闘で描画がおかしい
lanhuaのバーストスキル起点でおかしくなることが多い気がする
最初の数回のバトルは問題ないが、3回目くらいでチラつき始め、それ以降は毎回のバトルで描画がおかしくなる(アルタの姿が表示されない、背景がくろ
い、画面がチラつく、などいずれか、あるいは複合的に発生する)
⏺ 問題発見! AnimatedSprite.tsxの213行目で、フレームが変わるたびにnew Texture()で新しいテクスチャを作成しています。これらは破棄されず、WebGLリソースがどんどん蓄積していきます。
現時点でとりあえず直したほうが良さそうなところまとめ
・背景テクスチャはmemo化したほうがいい
・alta-entity.tsのsetTimeoutは戦闘終了時にクリアしたほうがいい
・AnimatedSprite.tsxの new Texture() の累積
autoモードでルークのバーストスキルが放たれる、または敵にランファがいて、ランファのバーストスキルが発生すると描画がおかしくなるっぽい。その2つを縛ると連戦しても問題なかった
手動でもバーストスキルを何発か打ってるとダメそう。デルルクだけになんども打たせてみたが、1回目の戦いで数発うって大丈夫でも2回目の戦いで数発打つとおかしくなる
複数の敵に大量に連続したダメージが発生した場合にバグりやすい説
lanhuaのnormalAttackの内容をフレイムロンドに変えてテストしたところ、バグが再現した。なので、バーストスキル自体というよりも、スキルの内容が描画処理に不具合を与えている模様
ボトルネック候補 claud clodeより
Battle.tsx のレンダリングボトルネック候補
- useBattleSystem の返り値が毎フレーム新しいオブジェクト参照 (Line 78-94)
- characters, projectiles, enemies などが毎フレーム新しい配列参照を返している可能性
- これにより BattleContent 全体が毎フレーム再レンダリングされる
- enemies.find() が毎レンダリングで実行 (Line 157-161)
enemies.find((enemy) => enemy.instanceId === targetLockEnemyId)
- TargetLockIndicator のpropsで毎回検索処理が走る
- useMemo でメモ化されていない
- shakeOffset のsetInterval による高頻度更新 (Line 105-118)
- メドロノヴァ発射中は16msごとに setShakeOffset が呼ばれる
- これが BattleContent 全体の再レンダリングをトリガー
- mapの中でインラインオブジェクトを渡している (Line 131-136)
position={character.position}
- character.position が毎回新しいオブジェクト参照なら再レンダリングの原因に
- handleCharacterClick, handleEnemyClick がメモ化されているか不明 (Line 87-89)
- useBattleSystem 内で useCallback されていなければ毎回新しい関数参照
- 条件付きレンダリングの多用 (Line 128, 129, 164, 180, 198-199)
{alta && <Alta alta={alta} />}
{isAiming && <BurstSkillOverlay ... />}
- 条件が変わるたびにマウント/アンマウントが発生
- screenSize と toPixel の参照 (Line 98-99)
- これらの値が変わると BattleContent 全体が再レンダリング
- 開発モードのデバッグ表示でのみ使用されているが常に取得している
- 複数のJotai atomの使用 (Line 96-97)
const [isAiming] = useIsAimingAtom();
const [isMedronovaFiring] = useIsMedronovaFiringAtom();
- atomの更新が BattleContent 全体の再レンダリングをトリガー
- damageDisplays と skillNameDisplays の配列 (Line 172-177)
- 頻繁に追加/削除されるリストで、配列が変わるたびに親も再レンダリング
- sortableChildren={true} (Line 121)
- PixiJSが毎フレームzIndex順にソートを実行するコスト
治ったかも。
修正内容
- PixiStage.tsx - アンマウント時のクリーンアップ改善
- 問題: Reactのref callbackは、useEffectのクリーンアップより先にnullになるため、アンマウント時にappRef.currentが既にnullになっていた
- 解決: pixiAppInstanceRefという別のrefを作成し、PixiJS Applicationのインスタンスを直接保持するようにした
- Battle.tsx - バトル終了時の手動クリーンアップ
- 問題: @pixi/reactのdestroy()はオプションなしで呼ばれるため、WebGLリソースが完全にクリーンアップされなかった
- 解決: onBattleClearとonGameOverでナビゲーション前に手動でdestroy(true, { children: true })を呼ぶようにした
- エラーハンドリング
- 既に破棄済みかどうかをstageとrendererの存在で確認
- try-catchで破棄時のエラーをキャッチ
根本原因
複数回のバトルを繰り返すと、PixiJS Applicationが適切に破棄されず、WebGLコンテキストやリソースが蓄積していました。ブラウザはWebGLコンテキスト数に制限があるため、古いコンテキストが無効化され、テクスチャが表示されなくなっていました。
一応は解決してますが、PixiStage.tsxのdestroy処理を行うと、以下のエラーがコンソールに出ます(進行に問題はなし)
strictModeをやめても出る。
@pixi_react.js?v=ff74f184:4354 TypeError: this._cancelResize is not a function
at _Application2.destroy (chunk-XC2OT7WO.js?v=ff74f184:180:10)
at chunk-2VBGONQW.js?v=ff74f184:6201:22
at Array.forEach (<anonymous>)
at _Application2.destroy (chunk-2VBGONQW.js?v=ff74f184:6200:13)
at @pixi_react.js?v=ff74f184:13223:35
at callCallback (@pixi_react.js?v=ff74f184:2169:18)
at commitCallbacks (@pixi_react.js?v=ff74f184:2181:13)
at runWithFiberInDEV (@pixi_react.js?v=ff74f184:756:18)
at commitLayoutEffectOnFiber (@pixi_react.js?v=ff74f184:7291:17)
at commitLayoutEffects (@pixi_react.js?v=ff74f184:7939:9) this._cancelResize is not a function
手動destroy後、Applicationの自動destroyが走っていて、そこでエラーが出てた。
そのため、手動destroy後に destroy = ()=>{} とし、無効化。
そもそもApplicationの自動destroyじゃダメなのかというと、それだけだと解消しなかったため手動で行なっている(mount,unmountが2回行われるためかも)
PRマージ済み