blog

座標の意味を変えたら5箇所が順に壊れた

  • Claude Code
  • リファクタリング
  • ゲーム開発

「射撃時に弾が地面からでてる」という報告が来た。

サイトの下端で動かしているミニゲームを、上から見た動きから完全なサイドビューに作り変えた直後だった。横から見た2Dアクションにするので、相棒は地面に立ち、y は重力と跳躍だけで決まる。そのとき hero.yfoe.y の意味を、胴の中ほどから 足元(接地点) へ変えていた。描画の都合でそうしたほうが素直だったからだ。

弾が地面から出るのは、発射位置に hero.y を使っていたからで、これは分かりやすい。直せば終わりに見えた。

同じ原因で4箇所だった

念のため、hero.y を使っている場所を一通り見てみることにした。報告された症状は1つだが、意味が変わった値を参照している場所が他にもあるはずだ、と思ったからだ。

出てきたのは4箇所あった。

弾の発射位置が足元だったので、弾が地面から出る。これが報告された症状だ。狙いの目標も敵の足元になっていて、敵の足を狙って撃っていた。当たり判定の中心も足元にあり、胴に当てても当たらず、地面をかすめると当たる。弾から敵への判定と、敵弾から相棒への判定の両方でそうなっていた。spitter という敵の吐く弾も、発射位置が足元なので地面から出ていた。

銃口の高さと胴の中心を名前つきの定数にして、描画側も戦闘側も同じ定数を見るようにした。スライディング中は姿勢が低いので銃口も胴の中心も下げる。通常45px と拡大61px の両方で「発射位置が足元より上」「狙いが胴」「判定の中心がスプライトの上下端の間」をテストで固定した。

4箇所まとめて直したので、これで片付いたと思った。

5箇所目

同じ日の1時間後に「落ちてるアイテムが地面に引っ付きながら収集される」という報告が来た。直したコミットが 11:56、次の報告を受けて直したコミットが 13:03 なので、片付いたと思っていられたのは67分間だった。

ドロップしたアイテムが相棒に吸い寄せられるとき、吸着先に hero.y を使っていた。つまり足元だ。アイテムは地面を這いながら近づいてくる。

同じ原因の5箇所目だった。

ここで、直し方を変えた。これまでは報告された症状を追いかけて、その1箇所を直していた。症状が来るたびに1件ずつ直す。でもこのやり方だと、まだ誰も気づいていない6箇所目が残っている可能性を潰せない。症状は原因の一部しか見せてくれない。

なので hero.y と敵の y を直接使っている箇所を、症状と関係なく全部洗い出した。

全部を中心に直すのは間違いだった

洗い出してみて分かったのは、機械的に全部を胴の中心へ置き換えると壊れる、ということだった。

中心へ直すべきものと、足元のままが正しいものが混ざっている。

胴の中心へ直したのは、ドロップの吸着、爆発の中心、演出の発生位置。どれも「見た目として体のどこで起きるか」を扱っている。

足元のまま残したのは、接地判定、地面線との比較、描画原点、敵の落下とせり上がり、そして近接武器の間合いや最寄りの敵の選定、交戦枠の判定だ。

最後の3つが判断の要るところだった。間合いや敵の選定は当たり判定に見えるが、実際には 地上の平面上でどれだけ離れているか を測っている。横から見た画面で「近い敵」を選ぶとき、基準は足の位置のほうが直感に合う。試しに胴の中心へ変えてみたら、撃破時間と生存時間の受け入れ範囲を両方とも外れた。

「意味が変わった値だから全部直す」ではなく、参照している1箇所ごとに、それが物理的な接地の話なのか、体のどこかの話なのか、地上での距離の話なのかを判断する必要があった。

アクセサで用途を分ける

再発防止としてやったのは、生の y を直接読ませないことだ。

heroFootY()    // 接地点
heroCenterY()  // 胴の中心
foeCenterY()   // 敵の胴の中心

戦闘と吸着はこの中心アクセサを使う。生の y を読んでいいのは、物理、接地、描画原点、地上の間合いに限定した。

これで完全に防げるわけではない。実際、演出の一部にはまだ生の y が残っている。倒れたときの位置を hero.deathY = hero.y で保存していて、これは足元だ。倒れて崩れ落ちる動きの起点としては足元のほうが自然なので、意図してそうしている面もある。ただ「演出は全部中心アクセサ」と言い切れる状態ではない。

新しいコードが生の y を読んでしまう余地も残っている。ただ、レビューで hero.y という文字列を見たときに「ここは物理か描画か地上距離か?」と問い直すきっかけにはなる。名前が違えば、違うものだと気づける確率は上がる。

前段でも同じ穴に落ちていた

あとから振り返ると、サイドビュー化そのものの回でも同じ種類のバグを2件踏んでいた。

キャラの脚が地面線の下で切り落とされていた。スプライトのローカル原点が足元ではない(相棒は下端 +18、敵は +16)のに、y へ地面線をそのまま入れていたからだ。もう1つは、跳躍の頂点が頭上の余白を超えて、跳ぶと頭が画面上端を突き抜けて消えていた。高さの比率だけで跳躍力を決めていて、相棒の背丈を考えていなかった。

つまり y の意味を変えた瞬間から、すでに壊れ始めていた。自分は報告が来るたびに個別に対処していただけで、途中まで「同じ1つの原因が広がっている」という見え方をしていなかった。

規則にした

この一件は、プロジェクトの CLAUDE.md に規則として書き足した。

値の意味を変えたら、その値の全参照を洗う。症状ベースで潰すと同じ原因を何度も踏む。意味を変える変更をしたら、その場で全参照を列挙して1つずつ判断する。

AI に実装を任せていると、この失敗はやりやすいと思う。報告された症状を渡せば、その症状はきれいに直る。直った時点でこちらも満足してしまう。原因が同じ他の4箇所は、誰も報告していないので誰も直さない。

意味を変える変更をした自覚があるなら、報告を待たずに全参照を列挙するところまでを1つの作業として扱うべきだった。5回踏んで、ようやくそう思えるようになった。

足元と胴の区別がついた相棒は画面の下端で動いている。手を動かして遊ぶほうはKludge Worksにある。

STAGE10/50SCORE0BEST0COMBO×1貯蓄0KILLS0
TAP = FOCUS FIRE
強化ツリー倒した数で買う。1つの強化は1回だけ。枝は左から順に開く。
kill -9 agentKILLS 0
kill -9 agent 停止中