クアッドツリーとグリッドベースの衝突検出


27

私は4人のプレイヤーが協力するrタイプのゲームを作成しており、衝突検出コードを実装しようとしています。衝突検出の処理方法に関する多くの記事や資料を読みましたが、何をすべきかを考えるのに苦労しています。クワッドツリーが最も一般的な方法であるように見えますが、一部のリソースではグリッドベースのソリューションに言及しています。前のゲームで検出にグリッドを使用したことで、私はそれに満足していますが、実際には四分木よりも優れていますか?どちらが最高のパフォーマンスを提供するかわかりません。また、少しベンチマークを実行しましたが、両方のソリューションに大きな違いはありません。

一方が他方より優れていますか?またはよりエレガント?どれを使うべきか本当に分かりません。

どんなアドバイスも大歓迎です。ありがとう。

回答:


31

正しい答えは、設計している実際のゲームによって少し異なります。どちらかを選択するには、実際に両方を実装し、特定のゲームでどちらがより時間的または空間的に効率的かを調べるプロファイリングを行う必要があります。

グリッド検出は、移動するオブジェクトと静的な背景の間の衝突の検出にのみ適用されるようです。これの最大の利点は、静的な背景が連続したメモリ配列として表されることであり、複数の読み取りを行う必要がある場合、各衝突ルックアップは良好な局所性を持つO(1)です(エンティティがグリッド内の複数のセルをカバーするため)。静的な背景が大きい場合の欠点は、グリッドのスペースがかなり無駄になる可能性があることです。

代わりに、静的な背景をクワッドツリーとして表す場合、個々のルックアップのコストは高くなりますが、背景の大きなブロックが少量のスペースを占有するため、メモリ要件が低下し、より多くの背景がキャッシュ。そのような構造でルックアップを行うのに10倍の読み取りが必要な場合でも、すべてがキャッシュ内にある場合、キャッシュミスのある単一のルックアップよりも10倍高速です。

私が選択に直面した場合は?私はグリッドの実装を採用します。なぜならば、それは簡単で、他のより興味深い問題に時間を費やすのが簡単だからです。ゲームの実行が少し遅いことに気付いたら、プロファイリングを行い、何が役立つかを確認します。ゲームが衝突検出に多くの時間を費やしているように見える場合は、クワッドツリーなどの別の実装を試し(最初に簡単な修正をすべて試した後)、それが助けになるかどうかを調べます。

編集:グリッドの衝突検出が複数のモバイルエンティティの衝突の検出にどのように関連するかについての手掛かりはありませんが、代わりに、空間インデックス(Quadtree)が反復ソリューションよりも検出パフォーマンスを改善する方法についてお答えします。素朴な(そして通常は完璧な)ソリューションは、次のようなものです。

foreach actor in actorList:
    foreach target in actorList:
        if (actor != target) and actor.boundingbox intersects target.boundingbox:
            actor.doCollision(target)

これは明らかにO(n ^ 2)付近のパフォーマンスを持ち、nは弾丸や宇宙船、エイリアンを含め、ゲームで現在生きている俳優の数です。また、小さな静的障害物を含めることもできます。

これは、そのようなアイテムの数が適度に少ない限り、素晴らしくうまく機能しますが、チェックするオブジェクトが数百を超えると少し貧弱に見え始めます。10個のオブジェクトの場合、衝突チェックは100個のみ、100個の場合は10,000個のチェックになります。1000は100万のチェックになります。

空間インデックス(クアッドツリーなど)は、幾何学的関係に従って収集するアイテムを効率的に列挙できます。これにより、衝突アルゴリズムが次のように変更されます。

foreach actor in actorList:
    foreach target in actorIndex.neighbors(actor.boundingbox):
       if (actor != target) and actor.boundingbox intersects target.boundingbox:
            actor.doCollision(target)

これの効率(エンティティの均一な分布を想定):通常O(n ^ 1.5 log(n))です。インデックスはトラバースするためにlog(n)の比較を必要とするため、約sqrt(n)の近隣が比較されます、確認するアクターはn人です。ただし、実際には、衝突が発生した場合、ほとんどの場合、オブジェクトの1つが削除されるか、衝突から遠ざかるので、近隣の数は常にかなり制限されます。したがって、O(n log(n))だけが得られます。10個のエンティティの場合、(約)10個の比較を行います。100個の場合は200個、1000個の場合は3000個です。

本当に巧妙なインデックスは、近隣検索と一括反復を組み合わせて、交差する各エンティティでコールバックを実行することさえできます。インデックスはn回クエリされるのではなく、1回スキャンされるため、これによりO(n)程度のパフォーマンスが得られます。


あなたが「静的な背景」と言うとき、あなたが何を参照しているかはわかりません。私が扱っているのは基本的に2Dシューティングゲームなので、宇宙船や宇宙人、弾丸、壁との衝突検出です。
ドットミン

2
あなたは私のプライベートな「グレートアンサー」バッジを獲得しました!
フェリキシー

これはばかげているように聞こえるかもしれませんが、実際にどのようにオブジェクトが衝突をテストする必要がある他のオブジェクトに対してクアッドツリーを使用するのですか?これがどのように行われるのかはわかりません。これで2つ目の質問が表示されます。別のノードの隣人ではないノードにオブジェクトがありますが、オブジェクトが十分に大きく、いくつかのノードにまたがっている場合、実際の衝突を確認するにはどうすればよいでしょうか? 「遠い」ノード内のオブジェクトと衝突するのに十分近い?ノードに完全に収まらないオブジェクトを親ノードに保持する必要がありますか?
ドットミン

2
クアットツリーは、バウンディングボックス検索の重複に対して本質的に準最適です。そのための最良の選択は、通常Rツリーです。四分木の場合、ほとんどのオブジェクトがほぼ点状の場合、はい、オブジェクトを内部ノードに保持し、ファジーネイバー検索で正確な衝突テストを実行するのが妥当です。インデックス内のほとんどのオブジェクトが大きく、衝突することなく重なっている場合、おそらくクアッドツリーは適切ではありません。これに関する技術的な質問がある場合は、stackoverflow.com
SingleNegationElimination

これはすべて非常に紛らわしいです!情報をありがとう。
ドットミン

3

古代の糸を復活させてすみませんが、私見の古いグリッドはこれらの場合には十分な頻度で使用されません。グリッドには、セルの挿入/削除が非常に安いという点で多くの利点があります。グリッドにはスパース表現を最適化する目的がないため、セルを解放する必要はありません。クワッドツリーをグリッドに置き換えるだけで、1200msから20msに至るまで、レガシーコードベースの多数の要素を選択する時間を短縮したと言います。ただし、公平のために、そのクアッドツリーは実際には実装が不十分で、要素のリーフノードごとに個別の動的配列を格納しています。

もう1つ非常に便利だと思うのは、図形を描画するための従来のラスター化アルゴリズムを使用してグリッドを検索できることです。たとえば、Bresenhamのラインラスタライゼーションを使用して、ラインと交差する要素を検索したり、スキャンラインラスタライゼーションを使用して、どのセルがポリゴンと交差するかを見つけたりすることができます。グリッド内の移動オブジェクトに対する交差を検出するために使用する画像にピクセルをプロットするために使用する最適化コード。

ただし、グリッドを効率的にするには、グリッドセルごとに32ビットを超える必要はありません。4メガバイト未満で100万個のセルを保存できるはずです。各グリッドセルは、セルの最初の要素にインデックスを付けるだけで、セルの最初の要素はセルの次の要素にインデックスを付けることができます。あらゆる種類の本格的なコンテナをすべてのセルに格納している場合、メモリの使用と割り当てが急激に増加します。代わりに、次のことができます。

struct Node
{
    int32_t next;
    ...
};

struct Grid
{
     vector<int32_t> cells;
     vector<Node> nodes;
};

そのようです:

ここに画像の説明を入力してください

さて、次は短所です。私は確かにバイアスとバイアスを優先してこれに来ていますが、それらの主な欠点はそれらがスパースではないということです。

座標が指定された特定のグリッドセルへのアクセスは一定時間であり、より安価なツリーを下る必要はありませんが、グリッドは密集しており、疎ではないため、必要以上のセルをチェックする必要があります。データが非常にまばらに分散している状況では、グリッドは、線や塗りつぶされた多角形、長方形、または境界円などの交差する要素を把握するために、より多くの方法をチェックする必要があります。グリッドは完全に空いている場合でもその32ビットセルを格納する必要があり、シェイプ交差クエリを実行しているときに、シェイプと交差する場合は空のセルをチェックする必要があります。

クアッドツリーの主な利点は、当然、スパースデータを格納し、必要なだけ分割する機能です。そうは言っても、特にすべてのフレームで動き回っている場合は、うまく実装するのが難しくなります。ツリーは、子ノードをその場で非常に効率的に再分割および解放する必要があります。そうしないと、親->子リンクを保存するためのオーバーヘッドを浪費する密なグリッドになります。グリッドについて上記で説明したものと非常に類似した手法を使用して効率的なクアッドツリーを実装することは非常に実行可能ですが、通常はより時間がかかります。そして、グリッドで私がするようにそれを行うと、それは必ずしも最適ではありません。それは、四分木ノードの4つの子すべてが連続して格納されることを保証する能力の損失につながるからです。

また、シーン全体の大部分にまたがる多数の大きな要素がある場合、四分木とグリッドの両方は素晴らしい仕事をしませんが、少なくともグリッドは平らなままで、そのような場合にはn次の程度に細分化しません。クアッドツリーは、そのような場合を合理的に処理するために葉だけでなく、ブランチに要素を格納する必要があります。このようなより病理学的なケースがあり、最も広い範囲のコンテンツを処理したい場合、クアッドツリーで注意する必要があります。たとえば、四分木に実際につまずく可能性がある別のケースは、一致する要素が大量にある場合です。その時点で、クアッドツリーが無限に細分化されないように、クアッドツリーの深さ制限を設定することに頼る人もいます。グリッドには、適切な仕事をするという魅力がありますが、

安定性と予測可能性は、ゲームコンテキストでも有益です。これは、まれなケースのシナリオでフレームレートが一時的に低下する可能性がある場合に、一般的なケースで可能な限り最速のソリューションが必ずしも必要ではない場合があるためです。周りにあるが、そのようなしゃっくりにつながることはなく、フレームレートをスムーズで予測可能に保ちます。グリッドにはそのような後者の品質があります。

とはいえ、それはプログラマ次第だと思います。グリッドとクアッドツリー、またはオクツリーとkdツリーとBVHのようなもので、使用するデータ構造に関係なく、非常に効率的なソリューションを作成した記録を持つ最も多くの開発者に投票します。マルチレベル、SIMD、キャッシュフレンドリーなメモリレイアウト、アクセスパターンなど、マイクロレベルにも多くの機能があります。一部の人々はそれらのミクロを考慮するかもしれませんが、彼らは必ずしもミクロの影響を持っているとは限りません。このようなことは、1つのソリューションから他のソリューションへの100倍の差を生む可能性があります。これにもかかわらず、個人的に数日与えられ、すべてのフレームを移動する要素の衝突検出を迅速に加速するためにデータ構造を実装する必要があると言われた場合、クワッドよりもグリッドを実装する方が良いでしょう-木。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.