ゲーム世界の空間分割
全オブジェクト総当たりの衝突判定や描画で処理が重い人へ。グリッド・quadtree・spatial hash で近傍だけに絞る原理を押さえれば、オブジェクトが増えても破綻しない空間検索を設計できます。
- 空間分割はオブジェクトを空間の領域ごとに索引化し、衝突や可視判定を近傍だけに限定する仕組み。総当たりの O(n^2) を、平均で近傍数に比例する規模まで下げるのが目的。
- 一様グリッドは実装が単純で密度が均一なら最速、quadtree/octree は空きが多く粗密が偏る世界に適応的、spatial hash は無限・広大な世界を有限テーブルに畳み込める。密度分布と世界の広さで選ぶ。
- 動的オブジェクトはセルをまたぐたびに削除して入れ直す再挿入が必要。移動が激しいほどこの更新コストが効くため、更新の軽さと検索の速さのトレードオフで構造を決める。
なぜ総当たりが破綻し、空間分割が要るのか
横にスクロール
ゲーム世界に n 個のオブジェクトがあり、互いの衝突を調べたいとします。素朴にやれば全ペアを比較するので比較回数は約 n*(n-1)/2、すなわち計算量は O(n^2) です。オブジェクトが 100 個なら約 5000 回で済みますが、10000 個になると約 5000 万回に膨れ、毎フレーム回すのは非現実的になります。可視性カリング(カメラに映らないものを描画から外す処理)も、全オブジェクトを毎フレーム視錐台と照合すれば同じく n に線形で効いてきます。
問題の本質は、遠く離れて絶対に相互作用しないペアまで律儀に比較している点です。画面の左端と右端のオブジェクトが衝突することはあり得ないのに、総当たりはそれを毎回確かめます。空間分割(spatial partitioning)は、オブジェクトを空間上の位置で索引化しておき、「あるオブジェクトの近くにいるのは誰か」を高速に引けるようにするデータ構造の総称です。これにより、衝突判定は「同じ・隣接する領域にいる相手」だけ、カリングは「視錐台と交差する領域」だけに絞り込め、平均的な計算量を近傍オブジェクト数に比例する規模まで落とせます。
衝突判定は二段構えで考えます。ブロードフェーズは空間分割を使って「衝突する可能性のあるペア」を安価に絞り込む前段。ナローフェーズは絞られた各ペアに対して、実際の形状(凸包・ポリゴンなど)で厳密な交差を計算する後段です。本稿の空間分割はブロードフェーズの担い手であり、ここで候補を減らせるほど、高価なナローフェーズの呼び出し回数が減ります。空間分割が返すのは「衝突するペア」ではなく「衝突しうる候補ペア」である点に注意してください。AABB(軸並行境界ボックス)の重なりで粗く判定し、通ったものだけ厳密判定へ回すのが定石です。
一様グリッド:単純ゆえに速い
最も単純な構造が一様グリッド(uniform grid)です。世界を一定サイズ cellSize の正方形(3D なら立方体)セルに区切り、各オブジェクトをその座標が属するセルに登録します。セル座標は割り算だけで求まります。
セル座標の算出とバケットへの登録:
cx = floor(x / cellSize)
cy = floor(y / cellSize)
grid[cx][cy].add(object) # そのセルのリストに追加
衝突候補の列挙:
自分のセルと周囲8セル(3x3)のオブジェクトだけを相手にする
→ オブジェクトの大きさが cellSize 以下なら、
交差相手は必ずこの3x3の範囲に入る
セルの割り当てが定数時間、候補列挙も近傍セルの中身を見るだけなので、密度が均一なら極めて高速です。要はハッシュテーブルのバケットを2次元格子で作っているようなものです。弱点は cellSize の選び方に敏感なこと。セルを大きくしすぎると1セルに多数が入り総当たりに逆戻りし、小さくしすぎると大きなオブジェクトが多数のセルにまたがって登録コストが増えます。また世界が広大で疎らだと、空セルのための配列が巨大になりメモリを浪費します。密度が場所によって大きく偏る世界では、この均一な区切りが噛み合いません。
経験則として、cellSize はオブジェクトの平均的な大きさ(AABB の一辺)と同程度に取るのが良いバランスです。こうすると各オブジェクトが占めるセル数が少数(多くは1〜4)に収まり、かつ1セルあたりの平均オブジェクト数も過大になりません。移動速度が速いゲームでは、1フレームの移動量がセルを飛び越えないサイズにするとトンネリング(すり抜け)対策とも整合します。
quadtree / octree:粗密に適応する木
密度が偏る世界には、適応的に分割する木構造が向きます。quadtree(四分木)は2D空間を再帰的に4つの象限へ分割する木で、3Dへ拡張して8分割にしたものがoctree(八分木)です。あるノードに登録されたオブジェクト数が容量閾値 K に達したら、その領域を4(または8)等分し、オブジェクトを子ノードへ振り分けます。オブジェクトが少ない広い空き領域は浅いまま、密集した領域だけが深く分割されるので、空間の粗密に木の深さが自動的に追従します。
検索は根から降りていきます。ある矩形範囲や視錐台と交差する子ノードだけを再帰的に訪ね、交差しない枝は丸ごと枝刈りします。空きが大半を占める広大なマップで少数の密集地を探すとき、無関係な領域を根の一段で切り捨てられるため、平均で木の高さ、おおむね O(log n) 相当の探索で近傍に到達できます。可視性カリングでは、視錐台に完全に含まれるノードは中身を無条件に可視、完全に外れるノードは丸ごと不可視、と判定できるため、階層カリングと相性が良いのが利点です。
quadtree ノードの構造(擬似コード):
node = {
bounds, # このノードが覆う矩形
objects, # まだ子に振り分けていないオブジェクト
children[4] or nil # 分割済みなら4つの子、未分割なら nil
}
分割の判定: len(objects) が容量閾値以上 かつ 深さ上限未満なら subdivide()
弱点は、オブジェクトが複数ノードの境界をまたぐときの扱いです。境界に重なるものを親ノードに留める実装、関係する全ての葉に重複登録する実装(loose 化)などがあり、境界付近のオブジェクトが増えると木が肥大したり重複が増えたりします。木の再帰的なポインタ追跡はキャッシュ効率でも一様グリッドに劣りがちです。
spatial hash:無限の世界を有限テーブルへ
世界が事実上無限、あるいは非常に広くてグリッド配列を確保できない場合に効くのが空間ハッシュ(spatial hashing)です。考え方はグリッドと同じくセル座標 (cx, cy) を求めますが、それを2次元配列の添字にする代わりに、ハッシュ関数で1つの整数に畳み込み、ふつうのハッシュテーブルのキーにします。
セル座標を1つのハッシュ値へ畳み込む:
cx = floor(x / cellSize)
cy = floor(y / cellSize)
h = (cx * P1) XOR (cy * P2) # P1, P2 は大きな素数
bucket = table[h mod tableSize] # このバケットへ登録
候補列挙は grid と同じく周囲セルのハッシュ値を引く
この方式なら、確保するのは実際にオブジェクトが存在するセルの分だけで済み、空セルにメモリを払いません。座標の範囲に上限がない(プロシージャル生成で無限に広がるオープンワールドなど)場合でも、有限サイズのテーブルに写像できます。代償はハッシュ衝突で、異なるセルが同じバケットに落ちると、その分だけ無関係なオブジェクトが候補に混ざります。tableSize と素数の選び方、そして衝突時の再ハッシュや連鎖の設計が性能を左右します。負の座標で mod の符号がぶれない工夫も要ります。
| 構造 | 得意な状況 | 更新コスト | 弱点 |
|---|---|---|---|
| 一様グリッド | 密度が均一・世界が有限で密 | 低(座標→セルは定数時間) | 疎で広大だと空セルが浪費・cellSizeに敏感 |
| quadtree / octree | 粗密が激しい・空きが多い | 中〜高(再挿入で木を再構成) | 境界またぎ・キャッシュ効率・実装が複雑 |
| spatial hash | 無限/超広大・疎な世界 | 低(ハッシュ再登録のみ) | ハッシュ衝突が候補に混入・調整依存 |
動的オブジェクトの再挿入と更新コスト
ここまでは「登録済みの世界を検索する」話でしたが、ゲームではオブジェクトが毎フレーム動きます。あるオブジェクトが移動して別のセル(またはノード)へ移ったとき、索引はそれを反映しなければなりません。基本操作は再挿入(re-insertion)——古い所属セルから削除し、新しいセルへ追加する——です。
一様グリッドや空間ハッシュでは、これは「旧セルのリストから外し、新セルのリストへ入れる」だけで定数時間に近く、更新が軽いのが強みです。多くの実装は、移動後に新しいセル座標を計算し、前フレームのセル座標と異なるときだけ再登録します(同じセルに留まるなら何もしない)。quadtree/octree の場合はより重く、削除後にそのノードが空になれば子をまとめる(merge)、逆に移動先が閾値を超えれば分割する、といった木の再構成が絡みます。移動が激しいゲームで木を毎フレーム大きく作り替えると、検索が速くても更新で相殺されかねません。
1フレームの移動距離がセルサイズを超えるほど高速なオブジェクトは、間のセルを飛び越えて登録されるため、通過した相手との衝突を取りこぼす(トンネリング)恐れがあります。移動経路を掃過する連続衝突判定(CCD)や、速度に応じてセルを跨ぐ経路上のセルすべてに一時登録する対策が要ります。また複数セルにまたがる大きなオブジェクトは全該当セルへ登録するため、候補列挙のときに同じペアが複数回上がりえます。ペアを集合({A,B} を正規化したキー)で重複除去してからナローフェーズへ渡す配慮が必要です。
移動が激しい環境では、毎フレーム索引を丸ごと作り直す(rebuild)方が、部分更新を積み重ねるより速いことすらあります。特に一様グリッドは全消去して全登録し直しても定数時間×n で済むため、ほぼ全オブジェクトが動く弾幕系などでは rebuild が単純かつ高速です。逆にほとんどが静止し一部だけ動く世界では、静的オブジェクトを別の索引に固定し、動的なものだけを軽い構造で管理する静動分離が効きます。
- 目的: 総当たり O(n^2) の衝突・カリングを、空間索引で近傍のみに限定し平均計算量を下げること。返すのは候補ペアであり、厳密判定(ナローフェーズ)は別途行う。
- 一様グリッド: 座標をセルに割る最速・最単純。密度均一で有効、疎で広大だと空セルが無駄。cellSize は平均オブジェクトサイズが目安。
- quadtree/octree: 粗密に木の深さが追従。範囲・視錐台探索で無関係な枝を枝刈りでき階層カリングに好適。境界またぎと更新コストが課題。
- spatial hash: セル座標をハッシュで有限テーブルへ畳み込む。無限/超広大な世界向き。衝突で候補が濁るのが代償。
- 再挿入: 動的オブジェクトはセル移動時に削除+追加。グリッド/ハッシュは軽く、木は再構成で重い。全体rebuildや静動分離も選択肢。
描画のBVHとは目的が違う
木構造による空間分割というと、レイトレーシングで使うBVH(境界ボリューム階層)を思い浮かべる人もいるでしょう。実際、グラフィックス(レンダリング・GPU) の分野では、光線と膨大な三角形の交差を高速化するために BVH や kd-tree が中心的に使われます。しかし本稿で扱ったのは衝突判定と可視性カリングという別用途です。両者は「空間を階層で索引化する」点で親戚ですが、最適化の対象が異なります。レイトレ用の構造は「1本の光線がどの三角形に当たるか」を最短で解くために、静的なシーンを前提に構築時間をかけてでも探索を最速化する設計に寄ります。対して衝突・カリング用の構造は、毎フレーム動くオブジェクトの再挿入コストと検索コストのバランスが主眼で、更新の軽さが構造選択を左右します。同じ木でも、静的シーンを深く最適化するのか、動的世界を軽く更新し続けるのかで設計判断が変わる——この使い分けを押さえておくと、どの構造をどの場面に持ち込むべきかを見誤りません。物理エンジンや可視判定は、ゲームのメインループ(OS(プロセス・メモリ) が管理するCPU時間の中で毎フレーム回る処理)の負荷を大きく左右するため、ここでの構造選択がフレームレートに直結します。
まとめ
空間分割は、ゲーム世界のオブジェクトを位置で索引化し、衝突判定のブロードフェーズと可視性カリングを近傍だけに絞ることで、総当たりの O(n^2) を平均的に近傍数へ比例する規模まで下げるためのデータ構造群です。一様グリッドは座標をセルに割るだけの単純さで密度が均一なら最速、quadtree/octree は空きの多い粗密の激しい世界に木の深さが適応し範囲探索を枝刈りでき、spatial hash はセル座標をハッシュで有限テーブルに畳み込むことで無限に広い世界すら扱えます。いずれも動的オブジェクトにはセル移動時の再挿入が必須で、更新の軽さと検索の速さのトレードオフが構造選択の軸になります。移動が激しければ全体rebuild、静止が多ければ静動分離といった運用も併せ、密度分布・世界の広さ・移動の激しさという三点で最適な索引を選ぶのが実務の勘所です。レイトレのBVHとは「空間を階層化する」発想を共有しつつも、更新コストを重視する衝突・カリング用途では設計判断が別物になる点も、あわせて押さえておきましょう。
ゲーム開発の記事ガイド
ゲーム世界の空間分割を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ゲーム開発
比較で見る軸
難易度: advanced / カテゴリ: ゲーム開発 / タグ数: 6
導入後に効く点
一様グリッドは実装が単純で密度が均一なら最速、quadtree/octree は空きが多く粗密が偏る世界に適応的、spatial hash は無限・広大な世界を有限テーブルに畳み込める。密度分布と世界の広さで選ぶ。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- ゲーム開発
- タグ数
- 6
判断チェックリスト
- 自社の用途が「ゲーム開発 / 空間分割」に近いか確認する。
- 強みである「空間分割はオブジェクトを空間の領域ごとに索引化し、衝突や可視判定を近傍だけに限定する仕組み。総当たりの O(n^2) を、平均で近傍数に比例する規模まで下げるのが目的。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。