膨大な数の円の衝突をチェックする最良の方法は何ですか?
2つの円の間の衝突を検出するのは非常に簡単ですが、すべての組み合わせをチェックすると、それはO(n2)これは間違いなく最適なソリューションではありません。
円オブジェクトには次のプロパティがあると想定できます。
速度は一定ですが、方向は変わる可能性があります。
私は2つの解決策を考え出しましたが、もっと良い解決策があるかもしれません。
ソリューション1
スペース全体を重なり合う正方形に分割し、同じ正方形内にある円との衝突のみを確認します。正方形はオーバーラップする必要があるため、円がある正方形から別の正方形に移動するときに問題は発生しません。
ソリューション2
最初に、円のすべてのペア間の距離を計算する必要があります。
距離が小さい場合、これらのペアはいくつかのリストに保存され、更新のたびに衝突をチェックする必要があります。
距離が大きい場合は、更新後に衝突が発生する可能性があることを保存します(距離と速度がわかっているため、計算できます)。ある種の優先キューに保存する必要があります。以前に計算された更新数の後に、距離を再度チェックする必要があります。次に、同じ手順を実行します。リストに追加するか、優先度キューに再度配置します。
マークバイアーズの質問への回答
単純な剛体球分子動力学シミュレーションをしていると思いますよね?私はモンテカルロと分子動力学シミュレーションで同じ問題を何度も経験しました。どちらのソリューションも、シミュレーションに関する文献で頻繁に言及されています。個人的に私は好きです解決策1ですが、少し変更されています。
ソリューション1
スペースを重ならない長方形のセルに分割します。したがって、1つの円の衝突をチェックするときは、最初の円であるセル内のすべての円を探し、周囲の各方向にX個のセルを探します。 Xの多くの値を試しましたが、X = 1が最速のソリューションであることがわかりました。したがって、スペースを各方向のセルサイズに次のように分割する必要があります。
_Divisor = SimulationBoxSize / MaximumCircleDiameter;
CellSize = SimulationBoxSize / Divisor;
_
除数は3より大きくする必要があります。そうしないと、エラーが発生します(小さすぎる場合は、シミュレーションボックスを拡大する必要があります)。
この場合、アルゴリズムは次のようになります。
正しく書くと、O(N)複雑さについて何かがわかります。これは、9セル(2Dの場合)または27セル(3Dの場合)内の円の最大数が任意の総数に対して一定であるためです。サークルの。
ソリューション2
通常、これは次のように行われます。
R < R_max
_にある円のリストを作成し、リストを更新するまでの時間を計算します(_T_update = R_max / V_max
_について、V_maxは現在の最大速度です)。T_update
_より大きい場合は、1に進みます。リストを使用するこのソリューションは、_R_max_2 > R_max
_と独自の_T_2
_有効期限を持つ別のリストを追加することで改善されることがよくあります。このソリューションでは、この2番目のリストを使用して最初のリストを更新します。もちろん、_T_2
_の後、O(N ^ 2)であるすべてのリストを更新する必要があります。また、このT
および_T_2
_の時間にも注意してください。衝突によって速度が変化する可能性がある場合、これらの時間は変化するためです。また、システムにいくつかのフォアを導入すると、速度も変化します。
ソリューション1 + 2衝突検出にはリストを使用し、リストの更新にはセルを使用できます。ある本では、これが最善の解決策であると書かれていましたが、(私の例のように)小さなセルを作成する場合は、解決策1の方が優れていると思います。しかし、それは私の意見です。
その他
シミュレーションの速度を向上させるために他のことを行うこともできます。
r = sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2) + ...)
を計算するときは、平方根演算を行う必要はありません。 _r^2
_をある値と比較することができます-それは大丈夫です。また、すべての_(x1-x2)*(x1-x2)
_操作を実行する必要はありません(つまり、すべての次元に対して)。_x*x
_が一部の_r_collision^2
_よりも大きい場合、他のすべての_y*y
_などまとめると、もっと大きくなるでしょう。剛体球の場合、時間内にステップを実行しない効果的なアルゴリズムもありますが、代わりに時間内に最も近い衝突を探し、この時間にジャンプしてすべての位置を更新します。衝突の可能性があまり高くない、密度の低いシステムに適しています。
後ですばやく比較できるように円を保存するための " spatial index " data-structuresがあります。 Quadtree、r-tree、kd-treeがその例です。
解決策1は空間インデックスのようであり、解決策2は、ペアを再計算するたびに空間インデックスの恩恵を受けます。
問題を複雑にするために、オブジェクトは動いています-それらは速度を持っています。
ゲームやシミュレーションでオブジェクトに空間インデックスを使用するのは通常のことですが、ほとんどの場合、静止オブジェクト、および通常は移動によって衝突に反応しないオブジェクトに使用されます。
ゲームでは通常 であり、設定された時間間隔(離散)ですべてを計算するため、2つのオブジェクトが互いに通過する可能性がありますが、移動速度が非常に速いため、気付かない可能性があります。多くのゲームは、実際には衝突を厳密な時系列で評価することすらしていません。それらには、静止オブジェクトの空間インデックスがあります。壁、および徹底的にチェックするすべての移動オブジェクトのリスト(ただし、概説したように、緩和された個別のチェックを使用します)。
正確な連続衝突検出と、シミュレーションでオブジェクトが衝突に反応する場所は、通常、はるかに要求が厳しくなります。
あなたが概説したペアアプローチは有望に聞こえます。ペアを次の衝突でソートしたままにし、適切な新しい位置で衝突したときに再挿入することができます。 2つのオブジェクトに対して新しく生成された衝突リスト(O(n lg n))を並べ替えてから、2つのリスト(各オブジェクトの新しい衝突と既存の衝突リスト)をマージするだけです。新しい衝突を挿入し、それらを削除します。 O(n)である衝突した2つのオブジェクトをリストした古い衝突。
これに対する別の解決策は、空間インデックスを適応させて、オブジェクトを厳密に1つのセクターではなく、最後の計算以降に通過した各セクターに格納し、個別に処理することです。これは、動きの速いオブジェクトを空間構造に格納することを意味し、この場合は最適化する必要があります。
リンクリストまたはポインタのリストは、最近のプロセッサでは非常に キャッシュに悪い であることを忘れないでください。サークルのコピー(とにかく衝突検出のための重要なプロパティ)を、任意の空間インデックスの各セクターの配列(シーケンシャルメモリ)、または上記で概説したペアに保存することをお勧めします。
マークがコメントで述べているように、計算を並列化するのは非常に簡単かもしれません。
考えられる手法の1つは、円の中心に Delaunay三角形分割 を使用することです。
各円の中心を考慮し、ドロネー三角形分割を適用します。これにより、サーフェスが三角形にテッセレートされます。これにより、 graph を作成できます。ここで、各ノードは三角形の中心を格納し、各エッジは隣接する円の中心に接続します。上で操作されたテッセレーションは、ネイバーの数を妥当な値に制限します(平均で6つのネイバー)
これで、円が移動するときに、衝突を考慮すべき円のセットが制限されます。次に、移動の影響を受ける円のセットにテッセレーションを再度適用する必要がありますが、この操作には、非常に小さな円のサブセット(移動する円の隣接円、および隣接する円の一部)のみが含まれます。
重要な部分は最初のテッセレーションであり、実行には時間がかかりますが、後のテッセレーションは問題になりません。そしてもちろん、時間と空間の観点からグラフを効率的に実装する必要があります...
スペースをリージョンに分割し、各リージョンの中心にある円のリストを維持します。
Center.xで並べ替えられたリストにすべての円を配置するなど、非常に単純なスキームを使用する場合でも、処理を大幅に高速化できます。特定の円をテストするには、リスト内の円の両側にある円に対してテストするだけで、x座標が半径以上離れている円に到達するまで外に出ます。
どの答えが最も効率的かは、円の密度に多少依存します。密度が低い場合は、低解像度のグリッドをマップ上に配置し、円を含むグリッド要素にマークを付けるのが最も効率的です。これには、更新ごとに約O(N*m*k)
がかかります。ここで、N
は円の総数、m
はグリッドポイントあたりの平均円数、k
は、1つの円でカバーされるグリッドポイントの平均数です。 1つの円が1ターンに複数のグリッドポイントを移動する場合は、スイープしたグリッドポイントの数を含めるようにm
を変更する必要があります。
一方、密度が非常に高い場合は、グラフウォーキングアプローチを試すことをお勧めします。各円に距離R
内のすべての隣接円が含まれるようにします(すべての円の半径_r_i
_に対してR
> _r_i
_)。次に、移動する場合は、「順方向」のすべての円に隣接する円を照会し、D内にあるすべての円を取得します。次に、Dよりも遠い後方方向の円をすべて忘れます。完全な更新にはO(N*n^2)
が必要です。ここで、n
は半径R
。間隔の狭い六角形の格子のようなものの場合、これにより、上記のグリッド法よりもはるかに優れた結果が得られます。
提案される「空間インデックス」の特別な(そして実装が非常に簡単な)ケースである「球体ツリー」の2Dバージョンを作成できます。アイデアは、「膨大な数の円」を「含む」単一の円が得られるまで、円を「含む」円に「結合」することです。
「含む円」(頭のてっぺん)の計算の単純さを示すために、次のようにします。1)2つの円の中心位置を(ベクトルとして)追加し、1/2でスケーリングします。円2)2つの円の中心位置を(ベクトルとして)減算し、半径を追加して、包含円の半径である1/2でスケーリングします。
彼の回答で述べたように、空間パーティションツリーはこの問題の一般的な解決策です。ただし、これらのアルゴリズムでは、移動するオブジェクトを効率的に処理するために微調整が必要になる場合があります。移動のほとんどのステップでバケットを変更するためにオブジェクトを必要としないように、緩いバケットフィッティングルールを使用することをお勧めします。
この問題に使用され、「衝突ハッシュ」と呼ばれる「ソリューション1」を見たことがあります。扱っているスペースが管理しやすいほど小さく、オブジェクトが少なくとも漠然と均一に分散していると予想される場合は、うまく機能します。オブジェクトがクラスター化されている可能性がある場合、それがどのように問題を引き起こすかは明らかです。各ハッシュボックス内である種のパーティションツリーのハイブリッドアプローチを使用すると、これを支援し、純粋なツリーアプローチを同時にスケーリングしやすいものに変換できます。
重複する領域は、ツリーバケットまたはハッシュボックスの境界にまたがるオブジェクトを処理する1つの方法です。より一般的な解決策は、隣接するボックス内のすべてのオブジェクトに対してエッジを横切るオブジェクトをテストするか、オブジェクトを両方のボックスに挿入することです(ただし、トラバーサルの中断を回避するために追加の処理が必要です)。
提案-私はゲーム開発者ではありません
衝突が発生する時期を事前に計算してみませんか
あなたが指定するように
円オブジェクトには次のプロパティがあると想定できます。
-コーディネート
-半径
-速度
-方向
速度は一定ですが、方向は変わる可能性があります。
次に、1つのオブジェクトの方向が変わったら、影響を受けるペアを再計算します。この方法は、方向があまり頻繁に変わらない場合に効果的です。
コードが「ティック」に依存している場合(およびオブジェクトがティックでオーバーラップしているかどうかをテストする)、次のようになります。
オブジェクトが「速すぎる」動きをしているときは、衝突せずに互いにスキップします
複数のオブジェクトが同じティックで衝突する場合、最終結果(たとえば、オブジェクトがどのように跳ね返るか、どの程度のダメージを受けるかなど)は、衝突が発生する/発生する順序ではなく、衝突をチェックする順序に依存します。 まれに、これによりゲームがロックされる可能性があります(たとえば、3つのオブジェクトが同じティックで衝突します。object1とobject2は衝突に合わせて調整され、object2とobject3は衝突に合わせて調整されます。 object1と再び衝突するため、object1とobject2の間の衝突をやり直す必要がありますが、これによりobject2がobject3と再び衝突するため、...)。
注:理論的には、この2番目の問題は「再帰的なティックの細分割」によって解決できます(3つ以上のオブジェクトが衝突する場合は、ティックの長さを半分に分割し、2つのオブジェクトだけがその「サブティック」で衝突します)。これにより、ゲームがロックしたりクラッシュしたりする可能性もあります(3つ以上のオブジェクトがまったく同じ瞬間に衝突すると、「永遠に繰り返される」シナリオになります)。
加えて;ゲーム開発者が「ティック」を使用する場合、「1固定長ティック= 1 /可変フレームレート」と言うこともあります。これは、固定長であるはずの何かが何か変数に依存できないため、ばかげています(たとえば、GPUが毎秒60フレームを達成できないと、シミュレーション全体がスローモーションになります。そして、これを行わず、代わりに「可変長ティック」がある場合、「ティック」の両方の問題が大幅に悪化し(特に低フレームレートで)、シミュレーションが非決定的になります(これはマルチゲームで問題になる可能性があります)プレーヤーであり、プレーヤーがゲームを保存、ロード、または一時停止すると、異なる動作が発生する可能性があります)。
唯一の正しい方法は、寸法(時間)を追加し、各オブジェクトに「開始座標と終了座標」として記述された線分と「終了座標後の軌道」を与えることです。オブジェクトが軌道を変更すると(予期しないことが起こったため、または「終了座標」に達したため)、「2本の線の間の距離<(object1.radius + object2.radius)」を実行することで、「最も早い」衝突を見つけることができます。変更されたオブジェクトと他のすべてのオブジェクトの計算。次に、両方のオブジェクトの「終了座標」と「終了座標後の軌道」を変更します。
外側の「ゲームループ」は次のようになります。
while(running) {
frame_time = estimate_when_frame_will_be_visible(); // Note: Likely to be many milliseconds after you start drawing the frame
while(soonest_object_end_time < frame_time) {
update_path_of_object_with_soonest_end_time();
}
for each object {
calculate_object_position_at_time(frame_time);
}
render();
}
これを最適化するには、次のような複数の方法があることに注意してください。
世界を「ゾーン」に分割します-例:そのため、object1がゾーン1と2を通過することがわかっている場合は、ゾーン1またはゾーン2も通過しない他のオブジェクトと衝突することはありません。
オブジェクトを「end_time%bucket_size」バケットに保持して、「次の最も早い終了時間」を見つけるのにかかる時間を最小限に抑えます
複数のスレッドを使用して「calculate_object_position_at_time(frame_time);」を実行します。並行して各オブジェクトに対して
すべての「次のフレーム時間までの事前シミュレーション状態」の作業を「render()」と並行して実行します(特に、ほとんどのレンダリングがGPUによって実行され、CPUが空いている場合)。
パフォーマンスの場合:
衝突の発生頻度が低い場合は、「ティック」よりも大幅に高速になる可能性があります(比較的長期間、ほとんど作業を行うことができません)。また、時間に余裕がある場合(プレーヤーがゲームを一時停止したなどの理由で)、将来の状況を日和見的に計算できます(事実上、パフォーマンスの急上昇を回避するために、時間の経過とともにオーバーヘッドを「平滑化」します)。
衝突が頻繁に発生すると、正しい結果が得られますが、同じ条件下で誤った結果が得られる壊れたジョークよりも遅くなる可能性があります。
また、「シミュレーション時間」と「リアルタイム」の間に任意の関係を持たせることも簡単になります。早送りやスローモーションなどでは、何も中断することはありません(シミュレーションがハードウェアで処理できる速度で実行されている場合でも、低速で実行されている場合でも)。何かが動いているかどうかを判断するのは難しいこと); (予測不可能な場合)将来の任意の時間に先んじて計算でき、(古い「オブジェクトラインセグメント」情報を期限切れ時に破棄するのではなく保存する場合)過去の任意の時間にスキップできます、および(ストレージコストを最小限に抑えるために特定の時点でのみ古い情報を保存する場合)、保存された情報で記述された時間にスキップして、任意の時間に進むことができます。これらを組み合わせることで、「インスタントスローモーション再生」なども簡単に行えます。
最後に;また、すべてのオブジェクトの「新しい場所」をすべてのティックですべてのクライアントに送信する大量の帯域幅を無駄にしたくないマルチプレイヤーシナリオでも便利です。
もちろん、欠点は複雑です-加速/減速(重力、摩擦、飛び跳ねる動き)、滑らかな曲線(楕円軌道、スプライン)、またはさまざまな形状のオブジェクト(たとえば、球ではなく任意のメッシュ/ポリゴン)などを処理したい場合はすぐに/ circles)最も早く衝突が発生する時期の計算に関係する数学は、非常に難しく、より高価になります。これが、ゲーム開発者が、直線運動を伴うN個の球または円の場合よりも複雑なシミュレーションに対して劣った「ティック」アプローチに頼る理由です。