当たり判定を速くする前に測る(総当たりを捨てて遅くなった話)
弾と敵の当たり判定を、総当たりからマス目(空間分割)に変えたら遅くなりました。切り出して測ると60倍速いのに、ゲームの中では負ける。理由と、結果を変えずに速さだけ選べるようにした形を、実測つきで残します。
公開 2026年9月19日
この記事の要点4 points
- 切り出して測ると、マス目は総当たりの16倍速い(300発 × 敵60体 × 200フレームで 100ms → 6ms)。
- ところがゲームの中では負けた(30秒ぶんの模擬プレイで 1ms → 7ms)。 敵が消えるたびに作り直す手間が乗るため。
- 実際に出ているのは数体〜十数体。 その規模では総当たりで足りる。
- 🔴 両方を残し、結果が同じであることをテストで固定してから、数で切り替えるようにした。
目次5章
検証した環境最終確認 2026年9月19日
- Windows 11 / RTX 2080 Ti
- Unity 6.6(6000.6.0f1)
- Claude Code(Opus 5 / 思考レベル 高)
書いてあるのは、この環境で実際に試した結果です。ツールの仕様や料金は変わるので、重要な判断の前には公式の情報も確認してください。
弾が最大300発、敵が最大60体。総当たりだと毎フレーム18,000回の距離くらべになります。これは減らしたい。
素直な手は「画面をマス目に区切って、弾の近くのマスにいる敵だけを見る」です。書いて、測って、遅くなりました。
16倍
切り出すと速い
100ms → 6ms
7倍
ゲームの中では遅い
1ms → 7ms
1,200
切り替える境目
弾 × 敵
0件
結果の違い
5つの種で突き合わせ
まず、切り出して測った
マス目だけを取り出して、敵の数を変えながら測ります(弾300発・200フレームぶん)。
敵数 弾数 総当たり(ms) マス目(ms) 60 300 100 6 150 300 210 9 400 300 382 17 1000 300 420 28 3000 300 501 62
数字だけ見れば、マス目の勝ちです。敵60体でも16倍、3000体なら8倍。ここで満足して差し替えると、次の落とし穴に落ちます。
ゲームの中では負けた
同じ実装で、実際の1プレイ(30秒ぶん・同じ種・同じ入力)を回して測ると、逆転しました。
マス目(7ms)
- 敵が消えるたびにマス目を作り直す
- 作り直しは「全部のマスを空にする」ところから
- 敵が数体しかいなくても、その手間は変わらない
総当たり(1ms)
- 実際に出ているのは数体〜十数体
- 弾も20発前後。かけ算しても数百回
- 下ごしらえが要らない
理由ははっきりしています。切り出した測定では、敵が消えません。実際のゲームでは、弾が当たるたびに敵が消え、並びが変わり、マス目は作り直しになります。
速くするための下ごしらえが、いちばん多い場面で毎回捨てられていた。
直し方:捨てずに、数で選ぶ
マス目が悪いわけではありません。数が増えれば逆転することは、切り出した測定で分かっています。そこで両方を残し、その場の数で選ぶようにしました。
bool useGrid = Mode == HitMode.Grid ||
(Mode == HitMode.Auto && _shotCount * _enemyCount > GridThresholdPairs);
境目の 1,200 は実測から決めた数字です(この機械で、総当たり1回ぶんの手間とマス目の下ごしらえが釣り合うあたり)。
片方を消さない
速くするときに古いほうを消してしまうと、比べる相手がいなくなります。残しておけば、数が変わったとき・機械が変わったときに測り直せます。残すコストは、このゲームでは数十行でした。
結果が変わっていないことを、テストで固定する
速さのために実装を変えたときに怖いのは、答えが変わることです。当たり判定は「どの弾がどの敵に当たるか」なので、少し違うだけでスコアが変わります。
そこで、同じ種・同じ入力で1プレイぶん回し、結果を文字にして突き合わせています。
return $"点{battle.Score} 撃破{battle.Kills} 敵{battle.EnemyCount} 弾{battle.ShotCount} 残機{battle.Lives} 時間{battle.Time:0.000}";
[Test]
public void 総当たりとマス目で結果が同じ()
{
for (int seed = 1; seed <= 5; seed++)
{
Assert.That(Play(HitMode.Grid, seed, 40f), Is.EqualTo(Play(HitMode.BruteForce, seed, 40f)), $"種 {seed} で結果が違う");
}
}
マス目にすると、見つける順番が変わる
- 症状
- 同じ場面なのに、当たる敵が入れ替わって点がずれる
- 原因
- マス目は「近くのマスにいるもの」をまとめて返すので、総当たりの「添字の小さい順」とは違う順で見つかる
- 対処
- 返ってきた候補のうちいちばん小さい添字を採る。これで総当たりと同じ答えになる
下ごしらえの回数を数えていない
- 症状
- 1回あたりは速いのに、全体では遅い
- 原因
- 対象が消えるたびに作り直していると、1フレームに何度も下ごしらえが走る
- 対処
- 作り直しが必要になった回数も測る。減らせないなら、その規模では使わない
まとめ
- 切り出した測定は上限の目安。実際の場面では前提(消える・数が少ない)が違う
- この規模(敵が数体〜十数体)では、総当たりで足りた
- 速くした実装は捨てずに残し、数で切り替える
- 実装を変えたら、結果が同じであることをテストで固定する
FAQ
よくある質問
当たり判定はマス目(空間分割)にすべきですか?
数が多いときだけです。切り出して測るとマス目は大きく速いのですが、実際のゲームでは「対象が消えるたびに作り直す」手間が乗ります。数十個しか出ない場面では、総当たりのほうが速いことがあります。
速くしたつもりが遅くなったのは、どうすれば分かりますか?
両方の実装を残して、同じ場面を同じ回数だけ回して測ります。片方だけ書いて差し替えると、比べる相手がいなくなります。
実装を差し替えるとき、結果が変わっていないかはどう確かめますか?
同じ種・同じ入力で1プレイぶん回し、点・撃破数・残機・時間を突き合わせます。文字にして比較すると、1か所でも違えばテストが落ちます。
どれくらいの数から切り替えますか?
このゲームでは「弾 × 敵」が 1,200 を超えたらマス目にしました。数字は実測から決めています(機械が変われば変わります)。
この記事は役に立ちましたか?
NEXT
あわせて読む
毎フレーム作って壊すのをやめる(オブジェクトプールの効き目を実測)
弾と敵が同時に数百個出るゲームを作りました。表示のたびに GameObject を作って壊す書き方と、作ったものを使い回す書き方を、同じ場面で測って比べます。1フレーム 19.06ms が 0.15ms になり、確保するメモリは 0 バイトになりました。
自動生成したパズルが必ず解ける作り方(逆再生+ソルバーで難しさも揃える)
箱押しパズルの面を自動生成しました。クリア状態から巻き戻して作れば必ず解けますが、それだけでは難しさが揃いません。1回引くたびに解かせて狙いの手数に合わせる方法と、狙いが外れ続けた原因、探索が重すぎて A* に変えた経緯を、実測つきで残します。
Webゲームの進行を保存する(何を保存しないか・壊れても落ちない形)
ブラウザで遊ぶパズルに「どこまでクリアしたか」と自己ベストを保存しました。盤面は保存しません。保存先がブラウザの領域であること、壊れた保存データで落ちない書き方、記事を書いていて気づいた「版が無い」という穴と、その直し方を残します。