blog

コンテナを足したら敵が置物になった

  • Claude Code
  • ゲーム開発
  • テスト

倉庫を漁るシューティングを作っていて、部屋の中に物入れ(コンテナ)を置くようにした。プレイヤーはその陰に隠れて撃ち合える。せっかく遮蔽物を足すなら、当たり判定もちゃんと持たせようと思った。

移動はコンテナをすり抜けられない。射線もコンテナに遮られる。弾もコンテナに当たって消える。この3系統は自分でも念入りに確認した。どれも同じ物入れの一覧を見ていて、当時のJSDocにも「3系統は全部containsPoint1箇所を通る」と書いてあった。1箇所直せば3つとも直る設計のつもりだった。

(このJSDoc自体、すり抜け対策にcrossesContainerを足した時点で不正確になっていた。実際は壁だけがcontainsPoint1箇所で、コンテナはcontainsPointcrossesContainerの組み合わせ、射線はそのどちらとも違う掃引を使う。同じ回のレビューで事実どおりに書き直している。)

レビューで実機のログを見ていたら、動かない敵がいた。近づいても撃ってこない。近づきすぎても動かない。どの方向にも歩けていない。

洗ったつもりの3系統は、たしかに正しかった

最初はコンテナの当たり判定そのものを疑った。containsPoint を見直しても、移動・射線・弾のどの系統も部屋の実装どおりに動いている。バグはそこには無かった。

疑うところを間違えていた。動けない敵は、そもそも歩けない場所に置かれていた

spawnFoes は部屋の中心からジッタを振って敵の初期位置を決める。このジッタは、コンテナを導入する前の「部屋の矩形の中ならどこでもいい」という前提のまま書かれていた。コンテナが部屋の中に浮かぶようになった後も、そこは変わっていなかった。200シードで実測すると、湧いた敵2200体のうち350体、15.9%がコンテナの円の中に重なって湧いていた。重なった敵は8方向のどこにも動けない置物になる。

読む側は直したのに、置く側を忘れていた

移動・射線・弾は、どれもコンテナの位置を読んで判定する側だ。ここは全部直した。

スポーンは逆で、コンテナの位置を無視して新しい座標を書き込む側だった。読む側の当たり判定をいくら正確にしても、書く側が古い前提のまま座標を生成していれば、その座標は端から歩けない場所になる。書く側と読む側は別の関数で、別のタイミングで動く。片方を直しても、もう片方には何も伝わらない。

直し方は、スポーン時にも歩けるかどうかを確認して、歩けなければ引き直すことにした。

let x = geom.center.x;
let z = geom.center.z;
for (let attempt = 0; attempt < BALANCE.foeSpawnJitterAttempts; attempt += 1) {
  const cx = geom.center.x + (rng.next() * 2 - 1) * halfX;
  const cz = geom.center.z + (rng.next() * 2 - 1) * halfZ;
  if (containsPoint(world, cx, cz, BALANCE.playerRadius)) {
    x = cx;
    z = cz;
    break;
  }
}

20回引き直しても歩ける場所が見つからなければ、部屋の中心へ逃がす。部屋の中心が歩けることは別の試験で保証済みなので、最後の逃げ場として使える。黙ってコンテナの中に置いたままにはしない。

テストは全部緑のままだった

厄介なのは、この不具合がある間もテストは落ちていなかったことだ。移動・射線・弾の判定を単体で試験すれば、どれも正しく動く。「敵がコンテナの中に湧かない」ことを確かめる試験そのものが存在しなかったので、緑は保たれていた。実機を触って、動かない敵を目で見て、初めて気づいた種類のバグだった。

脱出率にも表れていた。コンテナ導入直後の測定では脱出率が91.7%まで上がっていて、想定していた30〜90%の帯を超えていた。原因の一部はこの不具合で、動けない敵は攻めてこない分プレイヤーに有利に働く。スポーンを直したら85.0%に戻り、帯の中に収まった。この85.0%はこの時点の値で、その後に保管庫へ護衛を置いて測り直しているので、いまは65.0%に落ち着いている。

三重に見ていたつもりの当たり判定が、実は「見る側」しか洗っていなかった。値の意味を変えたら、読む側だけでなく書く側、置く側も同じだけ洗う必要がある。今回は自分のプロジェクトのCLAUDE.mdにすでに書いてあった教訓のはずだったが、コンテナという新しい概念を足すときに、それが「置く側」の変更でもあることに気づけなかった。

このコンテナと当たり判定はProd Runで実際に触れる。

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