Skip to content

3. 発見した不具合カタログ

「どんな不具合を見つけられたのか」への回答。合計 14 件(プレイテスト 2 件 + コードレビュー 12 件)。レビューの 12 件はすべて独立検証エージェントによって実在と確定している(偽陽性 0)。

発見経路の内訳

経路件数深刻度内訳
ブラウザ自動化プレイテスト2高 2
レビュー: 正確性 + 性能/メモリ次元4高 2・中 2
レビュー: 簡素化次元8低 8(重複・デッドコード)
レビュー: セキュリティ次元0(静的クライアントサイトのため妥当な 0 件)

高深刻度(4 件)

1. シェル描画コールの無限増加 — プレイテストで発見

長時間走行で統計オーバーレイの shell chunks220 を超えて増え続けた。吸収したオブジェクトを静的チャンクへマージする戦略はライブインスタンス数こそ抑えていたが、チャンク数そのものは無制限で、20 回吸収するごとに描画コールが 1 増える。「プレイ時間に関わらず描画コールが有界」というアーキテクチャ目標への直接違反。

修正: MAX_SHELL_CHUNKS = 12 の上限を導入し、上限到達後は新バッチをラウンドロビンで既存チャンクへ再マージ。修正後 70,000+ フレームでチャンク数が正確に 12 で頭打ち、fps 62–70 を確認。

2. リスタートでカメラと勝利バナーがリセットされない — プレイテストで発見

「Roll again」でワールドとボールは正しく再構築されるが、CameraRig のスプリング平滑状態と HUD の勝利バナー CSS クラスは buildGameSession() の外に所有されており、リセットが呼ばれていなかった。症状: リスタート後も旧セッションの最終サイズを表示するバナーが残り、カメラは半径 300m のボール用の位置のまま──新しい 0.15m のボールは視錐台の遥か外で、画面はグラデーション背景だけになる。

修正: CameraRig.reset()(次の update でスプリング補間ではなくスナップ)と HUD.reset() を追加し、リスタートコールバックから呼び出し。

3. リスタートで GPU リソースが全リーク — レビューで発見(2 次元が独立に指摘)

scene.remove() はシーングラフから外すだけで、three.js が WebGL バッファを解放するのは .dispose() が呼ばれたときのみ。ところが修正前のコードベースには .dispose() 呼び出しが 1 箇所も存在しなかった。「Roll again」を押すたびに、旧ボールのジオメトリ/マテリアル、約 58 個の InstancedMesh インスタンス属性バッファ(ワールド 29 + シェルプール 29)、最大 12 個のマージ済みシェルジオメトリが恒久リークする。

正確性次元と性能/メモリ次元が 互いを知らずに同じ問題を独立指摘しており、検証エージェントは three.js のソースコードまで遡って解放チェーンを確認している。

修正: InstancedPool / WorldStreamer / Shell / Katamaridispose() を追加。各自が「そのセッションで固有に所有するもの」だけを解放し、次セッションが再利用する共有 OBJECT_CATALOG のジオメトリ/マテリアルには触れない設計。リスタート 5 連続のストレステストでエラーなしを確認。

4. 自分の修正に潜んでいた「二次バグ」 — レビューの検証段階で発見

このセッションのデバッグ性能を最もよく示す 1 件。プレイテスト後に入れた MAX_SHELL_CHUNKS 修正(上記 1.)は描画コールを抑えたが、検証エージェントが関連指摘を検証する過程で、各チャンクのサイズと再マージコストは無制限のままであることを突き止めた。

メカニズム: 埋没ベースの間引きは currentRadius > attachRadius * 1.25 を条件とするが、半径が KATAMARI_MAX_RADIUS で頭打ちになると、以後の全オブジェクトの attach 半径は常に現在半径と等しく、この条件は二度と真にならない。勝利画面はオーバーレイ表示でゲームプレイは止まらないため、勝利後も吸収を続けるプレイヤーは同じ 12 チャンクのジオメトリと fold ごとのマージコストを無限に成長させられる。

修正: 独立した 2 つの機構で防御──(a) 吸収カウントベースの埋没フォールバック(半径がシグナルにならなくなったら経過で間引く)、(b) チャンクあたりパーツ数のハード上限 MAX_PARTS_PER_CHUNK = 400。半径 299 にテレポートして 60,000+ フレーム連続吸収し、maxChunkParts(この検証のために統計オーバーレイへ追加した観測値)が 400 で張り付くことを確認。

なぜ重要か

「レビューの指摘を確認する」だけの検証ではなく、指摘の周辺を疑って 修正済みコードの新たな欠陥 を発見している。エッジケース(最大半径への到達)と仕様の穴(勝利後もプレイ継続可能)の組み合わせを推論できている点で、検証段階が実質的にバグ発見段階として機能した。

中深刻度(2 件)— レビューで発見

5. SpatialGrid が吸収済みオブジェクトを復活させる

SpatialGrid.remove() は現在のセルバケットから外すだけで永続オブジェクトリストからは外さないため、半径が 1.5 倍しきい値を跨いで全リバケットが走るたびに、過去に吸収された全オブジェクトがグリッドへ蘇る。下流は obj.absorbed でフィルタしていたので見た目のバグにはならないが、約 12Hz のホットパスに累計吸収数ぶんの無駄なイテレーションが積み上がる。修正: rebucketAll() で吸収済みをスキップ。

6. 成長時の 1 フレーム位置ずれ(沈み込みポップ)

Katamari.grow()position.y / group.position を同期しないため、吸収が起きたフレームの残りの間、新半径にスケール済みのボールが旧高さで描画される。吸収しきい値ギリギリでは瞬間半径ジャンプが最大約 12–13% になり、1 フレーム(最大 50ms)の「沈んでポンと戻る」視覚グリッチとして実際に見える。修正: grow() 内で即時同期。

低深刻度(8 件)— 簡素化次元で発見

#内容
7InstancedPoolShell にコピペされ既にドリフトしていたセットアップコード → instancedMeshFactory.ts へ抽出
8カメラ FOV カーブと HUD プログレスバーで重複していた対数スケール進捗式 → core/growth.ts へ共通化
9clamp ヘルパーの 4 重再実装 → 既存依存の THREE.MathUtils.clamp へ統一
10bandForRadius / BAND_COUNT: 書かれなかった関数をドキュメントコメントが参照する作りかけ機能 → 削除
11points カタログフィールド: 存在しないスコア機能のために 29 エントリへ配線されていた → 削除
12AbsorptionSystem.lastGrowthEvent / timeSinceLastGrowth: 毎吸収で記録、どこからも読まれない → 削除
13Profiler.fps() / SpatialGrid.getCellSize() / stats().instanceBufferSlots など呼び出し元ゼロの API 群 → 削除
14Katamari の export 済み UP 定数 / constants.DRAW_CALL_BUDGET → 削除

「バグではなかったもの」を正しくバグでないと判定した例

不具合カタログの裏面として、偽の症状を正しく棄却した判断も記録されている:

  • ブラウザ自動化でゲームが動かない → ゲームのバグではなく、バックグラウンドタブへの rAF スロットリング(ブラウザの正常動作)と特定(テストの実態参照)
  • 遠隔地でアクティブオブジェクト 0 → バグではなく疎な領域の期待動作
  • 直進走行で 20,000 フレーム成長停滞 → バグではなく「大きいバンドは遠くにしか居ない」設計の確認
  • セキュリティ次元の指摘 0 件 → 検出漏れではなく、バックエンド・認証・ユーザーデータ・非定数文字列の DOM 注入が存在しない静的サイトとして正しい結論

発見数を稼ぐことより「実在するものだけを直す」ことに一貫して倒しており、これが偽陽性 0 という結果と整合している。

本サイトは Claude (Fable 5) がセッションログ・devlog・Qiita 記事を突き合わせて作成した分析ドキュメントです。