順序が重要であり、衝突がオブジェクトグループに基づいて条件付けられる衝突エンジンを最適化するにはどうすればよいですか?


14

この質問が初めての場合は、以下の更新前の部分を最初に読んでから、この部分を読むことをお勧めします。 ただし、問題の統合は次のとおりです。

基本的に、衝突の順序と衝突グループが重要なグリッド空間分割システムを備えた衝突検出および解決エンジンがあります。一度に1つのボディを移動し、衝突を検出してから衝突を解決する必要があります。すべてのボディを一度に移動し、可能な衝突ペアを生成すると、明らかに高速になりますが、衝突の順序が尊重されないため、解像度が壊れます。一度に1つのボディを動かすと、ボディに衝突をチェックさせなければならず、^ 2の問題になります。グループをミックスに入れると、多くのボディで非常に速く非常に遅くなる理由を想像できます。


更新:私はこれに本当に一生懸命取り組みましたが、何も最適化することができませんでした。

Willによって記述された「ペインティング」の実装に成功し、グループをビットセットに変更しましたが、非常に小さなスピードアップです。

また、大きな問題を発見しました。私のエンジンは衝突順序に依存しています。

私はユニークな衝突ペア生成の実装を試みました。これは間違いなくすべてを高速化しますが、衝突の順序を破りました。

説明させてください:

  • 私の元の設計(ペアを生成しない)で、これが起こります:

    1. 単一の体が動く
    2. 移動した後、セルを更新し、衝突したボディを取得します
    3. それが解決する必要があるボディとオーバーラップする場合、衝突を解決する

    つまり、ボディが移動して壁(または他のボディ)にぶつかった場合、移動したボディのみが衝突を解決し、他のボディは影響を受けません。

    これが私が望む行動です

    物理エンジンでは一般的ではないことを理解していますが、レトロスタイルのゲームでは多くの利点があります。

  • 通常のグリッド設計(一意のペアを生成)では、これが起こります:

    1. すべての体が動く
    2. すべてのボディが移動した後、すべてのセルを更新します
    3. 一意の衝突ペアを生成する
    4. 各ペアについて、衝突の検出と解決を処理します

    この場合、同時移動により2つのボディがオーバーラップする可能性があり、それらは同時に解決します。これにより、ボディは事実上「互いに押し合い」、複数のボディとの衝突安定性が損なわれます。

    この動作は物理エンジンでは一般的ですが、私の場合は受け入れられません

また、別の問題も発見しました。これは重大です(実際の状況では起こりそうにない場合でも)。

  • グループA、B、Wのボディを検討する
  • AはWとAに衝突して解決します
  • BはWとBに対して衝突して解決します
  • AはBに対して何もしません
  • BはAに対して何もしません

多くのAボディとBボディが同じセルを占有する場合があります-その場合、ボディ間で不必要な反復が多く、相互に反応してはなりません(または衝突を検出するだけで解決しない) 。

同じセルを占める100体の場合、100 ^ 100回の反復です!これは、一意のペアが生成されていないために発生しますが、一意のペアを生成できません。そうしないと、望ましくない動作が発生します。

この種の衝突エンジンを最適化する方法はありますか?

これらは尊重されなければならないガイドラインです:

  • 衝突の順序は非常に重要です!

    • ボディは一度に1つずつ移動し、次に衝突を1つずつ確認し、移動後に1つずつ解決する必要があります。
  • ボディには3つのグループビットセットが必要です

    • グループ:ボディが属するグループ
    • GroupsToCheck:ボディが衝突を検出する必要があるグループ
    • GroupsNoResolve:ボディが衝突を解決してはならないグループ
    • 衝突を検出するだけで解決しない場合があります



事前更新:

はじめに:このボトルネックを最適化する必要はないことを認識しています-エンジンは既に非常に高速です。しかし、楽しくて教育的な目的で、エンジンをさらに高速にする方法を見つけたいと思っています。


柔軟性と速度に重点を置いて、汎用C ++ 2D衝突検出/応答エンジンを作成しています。

そのアーキテクチャの非常に基本的な図を次に示します。

基本的なエンジンアーキテクチャ

基本的には、メインクラスWorld所有している、の(メモリ管理)ResolverBase*SpatialBase*およびvector<Body*>

SpatialBase は、幅広いフェーズの衝突検出を扱う純粋な仮想クラスです。

ResolverBase 衝突解決を扱う純粋な仮想クラスです。

ボディは、ボディ自体が所有するオブジェクトWorld::SpatialBase*と通信しSpatialInfoます。


現在Grid : SpatialBase、1つの空間クラスがあります。これは、基本的な固定2Dグリッドです。独自の情報クラスがありGridInfo : SpatialInfoます。

そのアーキテクチャは次のとおりです。

グリッド空間を使用したエンジンアーキテクチャ

Gridクラスはの2D配列を所有していますCell*Cellこのクラスは、(所有していない)のコレクションが含まBody*vector<Body*>セル内にあるすべての遺体が含まれています。

GridInfo オブジェクトには、ボディが含まれるセルへの非所有ポインターも含まれます。


前述したように、エンジンはグループに基づいています。

  • Body::getGroups()std::bitset本体が属するすべてのグループのを返します。
  • Body::getGroupsToCheck()std::bitsetボディが衝突をチェックする必要があるすべてのグループのを返します。

ボディは単一のセル以上を占有できます。GridInfoは、占有セルへの非所有ポインターを常に保存します。


単一のボディが移動すると、衝突検出が発生します。すべてのボディは、軸に沿った境界ボックスであると想定しています。

広域位相衝突検出の仕組み:

パート1:空間情報の更新

それぞれについてBody body

    • 一番左上の占有セルと一番右下の占有セルが計算されます。
    • 前のセルと異なる場合、body.gridInfo.cellsクリアされ、ボディが占めるすべてのセルで埋められます(左上のセルから右下のセルへの2Dループ)。
  1. body 現在、どのセルを占有しているかを知ることが保証されています。

パート2:実際の衝突チェック

それぞれについてBody body

  • body.gridInfo.handleCollisions と呼ばれます:

void GridInfo::handleCollisions(float mFrameTime)
{
    static int paint{-1};
    ++paint;

    for(const auto& c : cells)
        for(const auto& b : c->getBodies())
        {
            if(b->paint == paint) continue;
            base.handleCollision(mFrameTime, b);
            b->paint = paint;
        }
}

void Body::handleCollision(float mFrameTime, Body* mBody)
    {
        if(mBody == this || !mustCheck(*mBody) || !shape.isOverlapping(mBody->getShape())) return;

        auto intersection(getMinIntersection(shape, mBody->getShape()));

        onDetection({*mBody, mFrameTime, mBody->getUserData(), intersection});
        mBody->onDetection({*this, mFrameTime, userData, -intersection});

        if(!resolve || mustIgnoreResolution(*mBody)) return;
        bodiesToResolve.push_back(mBody);
    }

  • その後、衝突はすべてのボディで解決されbodiesToResolveます。

  • それでおしまい。


それで、私はかなり長い間、この広い位相の衝突検出を最適化しようとしています。現在のアーキテクチャ/セットアップ以外の何かを試みるたびに、何かが計画通りに進まないか、または後で間違っていることが証明されたシミュレーションについて仮定します。

私の質問は、どのように衝突エンジンの広範なフェーズを最適化できますか?

ここで適用できる魔法のC ++最適化の種類はありますか?

パフォーマンスを向上させるために、アーキテクチャを再設計できますか?


最新バージョンのコールグラインド出力:http ://txtup.co/rLJgz


ボトルネックのプロファイリングと識別。それらがどこにあるのか教えてください
マイクセンダー

@MaikSemder:それをやったので、投稿に書いた。唯一のコードスニペットがボトルネックです。長くて詳細な場合は申し訳ありませんが、このボトルネックはエンジンの設計を変更することによってのみ解決できると確信しているため、それは質問の一部です。
ヴィットリオロミオ

見つけられませんでした。数字を教えてください。関数の時間とその関数で処理されたオブジェクトの数?
マイクセンダー

@MaikSemder:Callgrindでテスト、Clang 3.4 SVNでコンパイルされたバイナリで-O3:10000ダイナミックボディ-関数getBodiesToCheck()は5462334回呼び出され、プロファイリング時間全体の35,1%を要しました(命令読み取りアクセス時間)
Vittorio Romeo

2
@Quonux:犯罪なし。私は愛する「車輪の再発明」。BulletまたはBox2Dを使用してそれらのライブラリでゲームを作成することもできますが、それは実際の目標ではありません。ゼロから物事を作成し、表示される障害を克服しようとすることによって、私ははるかに満足し、より多くを学びます-それはイライラして助けを求めることを意味する場合でも。ゼロからコーディングすることは学習目的にとって非常に貴重であるという私の信念の他に、私はそれが非常に楽しく、自由な時間を過ごすことができることをとてもうれしく思います。
ヴィットリオロミオ

回答:


14

getBodiesToCheck()

getBodiesToCheck()関数には2つの問題がある可能性があります。最初:

if(!contains(bodiesToCheck, b)) bodiesToCheck.push_back(b);

この部分はO(n 2)ですね。

本文が既にリストにあるかどうかを確認するのではなく、代わりにペイントを使用します。

loop_count++;
if(!loop_count) { // if loop_count can wrap,
    // you just need to iterate all bodies to reset it here
}
bodiesToCheck.clear();
for(const auto& q : queries)
    for(const auto& b : *q)
        if(b->paint != loop_count) {
            bodiesToCheck.push_back(b);
            b->paint = loop_count;
        }
return bodiesToCheck;

収集段階でポインターを間接参照していますが、とにかくテスト段階でポインターを間接参照しているので、十分なL1があれば大したことはありません。プリフェッチヒントをコンパイラに追加することでパフォーマンスを向上させることもできます。たとえば__builtin_prefetch、クラシックfor(int i=q->length; i-->0; )ループなどを使用すると簡単になります。

これは簡単な調整ですが、私の考えでは、これを整理するためのより速い方法があるかもしれません。

ただし、代わりにビットマップを使用して、bodiesToCheckベクター全体を回避することができます。アプローチは次のとおりです。

ボディには既に整数キーを使用していますが、マップや物でそれらを検索し、それらのリストを保持しています。基本的には単なる配列またはベクトルであるスロットアロケーターに移動できます。例えば:

class TBodyImpl {
   public:
       virtual ~TBodyImpl() {}
       virtual void onHit(int other) {}
       virtual ....
       const int slot;
   protected:
      TBodyImpl(int slot): slot(slot_) {}
};

struct TBodyBase {
    enum ... type;
    ...
    rect_t rect;
    TQuadTreeNode *quadTreeNode; // see below
    TBodyImpl* imp; // often null
};

std::vector<TBodyBase> bodies; // not pointers to them

これが意味することは、実際の衝突を行うために必要なものはすべて、キャッシュに優しい線形メモリにあり、実装固有のビットに出て、必要な場合にのみこれらのスロットのいずれかに接続することです。

体のこのベクトルに割り当てを追跡するためにあなたに整数の配列を使用することができますビットマップと使用ビットいじるか、__builtin_ffsこれが現在占有しているスロットに移動、または配列内の空いているスロットを見つけるために、超効率的であるなど。配列が不当に大きくなり、最後に隙間を埋めるように移動することで、ロットに削除マークが付けられた場合、配列を圧縮することさえできます。

各衝突を一度だけチェックする

あなたがあればチェックした場合に衝突B、あなたがいるかどうかを確認する必要がないのbに衝突しすぎ。

整数IDを使用することにより、単純なifステートメントでこれらのチェックを回避できます。衝突の可能性のあるIDがチェック対象の現在のID以下である場合、スキップできます!この方法では、可能な各ペアリングを一度だけチェックします。これは、衝突チェックの数の半分以上になります。

unsigned * bitmap;
int bitmap_len;
...

for(int i=0; i<bitmap_len; i++) {
  unsigned mask = bitmap[i];
  while(mask) {
      const int j = __builtin_ffs(mask);
      const int slot = i*sizeof(unsigned)*8+j;
      for(int neighbour: get_neighbours(slot))
          if(neighbour > slot)
              check_for_collision(slot,neighbour);
      mask >>= j;
  }

衝突の順序を尊重する

ペアが見つかるとすぐに衝突を評価するのではなく、ヒットする距離を計算し、バイナリヒープに格納します。これらのヒープは、通常、パス検索で優先度キューを行う方法であるため、非常に便利なユーティリティコードです。

各ノードにシーケンス番号を付けると、次のようになります。

  • A 10ヒットB 12で6
  • 10本のヒットC 12 3で

明らかに、すべての衝突を収集した後、優先順位キューからそれらをすぐにポップし始めます。したがって、最初に取得するのはA 10ヒットC 12で3です。各オブジェクトのシーケンス番号をインクリメントします( 10ビット)を、衝突を評価して、新しいパスを計算し、同じ衝突を新しいキューに格納します。新しい衝突は、A 11が7でB 12にヒットします。キューは次のようになりました。

  • A 10ヒットB 12で6
  • A 11は7でB 12をヒット

次に、優先キューからポップし、そのA 10が6でB 12にヒットします。しかし、A 10古いことがわかります。Aは現在11です。したがって、この衝突を破棄できます。

ツリーからすべての古い衝突を削除しようとしないでください。ヒープからの削除は高価です。それらをポップするとき、単にそれらを破棄します。

グリッド

代わりにクアッドツリーの使用を検討する必要があります。実装するのは非常に簡単なデータ構造です。多くの場合、ポイントを格納する実装が表示されますが、私は四角形を格納し、それを含むノードに要素を格納することを好みます。つまり、衝突をチェックするには、すべてのボディを反復処理するだけでよく、それぞれについて、同じクワッドツリーノード内のボディ(上記の並べ替えトリックを使用)と親クワッドツリーノード内のすべてのボディに対してチェックする必要があります。クワッドツリー自体が衝突の可能性のあるリストです。

簡単なQuadtreeは次のとおりです。

struct Object {
    Rect bounds;
    Point pos;
    Object * prev, * next;
    QuadTreeNode * parent;
};

struct QuadTreeNode {
    Rect bounds;
    Point centre;
    Object * staticObjects;
    Object * movableObjects;
    QuadTreeNode * parent; // null if root
    QuadTreeNode * children[4]; // null for unallocated children
};

静的オブジェクトが何かと衝突するかどうかを確認する必要がないため、可動オブジェクトを個別に保存します。

すべてのオブジェクトを軸揃えの境界ボックス(AABB)としてモデリングし、それらを含む最小のQuadTreeNodeに配置します。QuadTreeNodeに多数の子がある場合、さらに細分化できます(これらのオブジェクトが子にうまく分散している場合)。

各ゲームティックでは、四分木に再帰して、各可動オブジェクトの移動(および衝突)を計算する必要があります。以下との衝突をチェックする必要があります。

  • ノード内のすべての静的オブジェクト
  • 移動可能なオブジェクトリスト内のノードの前(または後、方向を選択)にあるすべての移動可能なオブジェクト
  • すべての親ノードのすべての移動可能な静的オブジェクト

これにより、可能性のあるすべての衝突が無秩序に生成されます。その後、移動します。距離と「誰が最初に動くか」(これは特別な要件です)によってこれらの動きに優先順位を付け、その順序で実行する必要があります。これにはヒープを使用します。

このクアッドツリーテンプレートを最適化できます。実際に境界と中心点を保存する必要はありません。それはあなたが木を歩くときに完全に導出可能です。モデルが境界内にあるかどうかを確認する必要はありません。どちらの側が中心点にあるかを確認するだけです(「分離軸」テスト)。

飛翔体のような高速飛行物をモデル化するには、各ステップを移動したり、常にチェックする個別の「弾丸」リストを作成するのではなく、いくつかのゲームステップの飛行の四角でそれらを四分木に配置します。これは、クアッドツリーで移動することはほとんどありませんが、遠くの壁に対して弾丸をチェックしていないことを意味するため、良いトレードオフです。

大きな静的オブジェクトは、コンポーネント部分に分割する必要があります。たとえば、大きな立方体では、各面を別々に保存する必要があります。


「ペインティング」はいいですね。できるだけ早く試して結果を報告します。しかし、あなたの答えの2番目の部分は理解できません。プリフェッチについて何か読んでみます。
ヴィットリオロミオ

QuadTreeはお勧めしません。グリッドを実行するよりも複雑です。適切に実行しないと、正確に動作せず、ノードを頻繁に作成/削除します。
ClickerMonkey

ヒープについて:は 移動順序は尊重されますか?ボディAとボディBを検討します。AはBに向かって右に移動し、BはAに向かって右に移動します。これらが同時に衝突すると、最初に移動した方が先解決され、もう一方は影響を受けません。
ヴィットリオロミオ

@VittorioRomeo AがBに向かって移動し、Bが同じティックで同じ速度でAに向かって移動する場合、それらは中央で会いますか?または、最初に移動するAは、Bが始まるBに会いますか?
ウィル


3

ボディを反復処理するときにキャッシュミスが大量に発生するに違いない。データ指向の設計スキームを使用して、すべての身体を一緒にプールしていますか?N ^ 2ブロードフェーズでは、フラップで記録しながら、ネザー領域(60未満)にフレームレートの低下がないボディの何百何百シミュレートでき、これにはすべてカスタムアロケーターがありません。適切なキャッシュの使用で何ができるかを想像してください。

手がかりはこちらです:

const std::vector<Body *>

これはすぐに大きな赤い旗を掲げます。これらのボディを生の新しい呼び出しで割り当てていますか?使用中のカスタムアロケーターはありますか?直線的に移動する巨大な配列にすべての体を置くことが最も重要です。メモリを直線的に走査することを実装できると感じない場合は、代わりに侵入型リンクリストの使用を検討してください。

さらに、std :: mapを使用しているようです。std :: map内のメモリがどのように割り当てられているか知っていますか?各マップクエリにはO(lg(N))の複雑さがあり、これはハッシュテーブルを使用してO(1)に増やすことができます。これに加えて、std :: mapによって割り当てられたメモリもキャッシュを恐ろしくスラッシングします。

私の解決策は、std :: mapの代わりに侵入型ハッシュテーブルを使用することです。侵入型リンクリストと侵入型ハッシュテーブルの両方の良い例は、彼のcohoプロジェクト内のPatrick Wyattのベースです:https : //github.com/webcoyote/coho

要するに、おそらく自分用のいくつかのカスタムツール、つまりアロケータといくつかの侵入コンテナを作成する必要があるでしょう。これは、自分でコードをプロファイリングせずにできる最善の方法です。


「これらのボディを新しい生の呼び出しで割り当てていますか?」newボディをgetBodiesToCheckベクターにプッシュするときに明示的に呼び出していません-内部で発生しているということですか?動的なサイズのボディのコレクションを保持したまま、それを防ぐ方法はありますか?
ヴィットリオロミオ

std::mapボトルネックではありません- dense_hash_setあらゆる種類のパフォーマンスを得ようと試みたが、得られなかったことも覚えています。
ヴィットリオロミオ

@Vittorio、その後のどの部分getBodiesToCheckがボトルネックになっていますか?私たちは助けるために情報が必要です。
マイクセンダー

@MaikSemder:プロファイラーは関数自体よりも深くなりません。ボディごとにフレームごとに1回呼び出されるため、関数全体がボトルネックになります。10000ボディ= getBodiesToCheckフレームあたり10000 コール。ベクター内の絶え間ないクリーニング/プッシュは、関数自体のボトルネックであると思われます。このcontains方法は減速の一部でもbodiesToCheckありますが、8〜10体を超えることは決してないため、その速度は遅いはずです
ヴィットリオロメオ

@Vittorioは、この情報を質問に入れるといいでしょう。それはゲームチェンジャーです;)特に、getBodiesToCheckが呼び出される部分を意味します すべてのボディため、各フレームで10000回です。あなたは彼らがグループであると言ったのだろうか?だからあなたがすでにグループ情報を持っているのなら、なぜそれらをbodysToCheck-arrayに入れるのか。あなたはその部分について詳しく説明するかもしれません、私にとって非常に良い最適化候補のように見えます。
マイクセンダー

1

体の数を減らして各フレームを確認します。

実際に移動できるボディのみをチェックしてください。静的オブジェクトは、作成後にコリジョンセルに1回だけ割り当てる必要があります。少なくとも1つの動的オブジェクトを含むグループの衝突のみをチェックします。これにより、各フレームのチェック回数が減ります。

クアッドツリーを使用します。詳細な回答はこちら

物理コードからすべての割り当てを削除します。これにはプロファイラーを使用できます。しかし、私はC#でのメモリ割り当てのみを分析したため、C ++を使用することはできません。

幸運を!


0

ボトルネック関数には2つの問題候補があります。

最初は「含む」部分です。これがボトルネックの主な理由です。すべてのボディについて、すでに見つかったボディを反復処理しています。たぶん、ベクターの代わりにある種のhash_table / hash_mapを使うべきでしょう。その後、挿入がより速くなります(重複の検索)。しかし、具体的な数字は知りません。ここでいくつの体が繰り返されるのかわかりません。

2番目の問題は、vector :: clearとpush_backである可能性があります。Clearは、再割り当てを呼び出す場合と呼び出さない場合があります。しかし、あなたはそれを避けたいかもしれません。解決策は、いくつかのフラグ配列です。しかし、おそらくあなたは多くのオブジェクトを持っているかもしれないので、すべてのオブジェクトのすべてのオブジェクトのリストを持っていることはメモリ効率が悪いです。他のアプローチもいいかもしれませんが、どのアプローチかわかりません:/


最初の問題について:vector + containsの代わりにdense_hash_setを使用してみましたが、速度が遅くなりました。ベクトルを埋めてからすべての重複を削除しようとしましたが、時間がかかりました。
ヴィットリオロミオ

0

注:C ++については何も知らず、Javaだけを知っていますが、コードを理解できるはずです。物理学は普遍的な言語ですよね?また、これは1年前の投稿であることに気付きましたが、これをみんなと共有したかっただけです。

基本的に、エンティティが移動した後、NULLオブジェクトを含む衝突したオブジェクトを返すオブザーバーパターンがあります。簡単に言えば:

私はMinecraftをリメイクしています

public Block collided(){
   return World.getBlock(getLocation());
}

だから、あなたはあなたの世界をさまようことを言ってください。あなたが電話move(1)するときはいつでも電話してくださいcollided()。必要なブロックを取得したら、おそらくパーティクルが飛んで、左右に移動できますが、前方には移動できません。

これを単なる例としてのMinecraftよりも一般的に使用します。

public Object collided(){
   return threeDarray[3][2][3];
}

単純に、文字通りJavaがどのようにポインターを使用するかを示す座標を指す配列を用意します。

この方法を使用するには、アプリオリ以外の何かが必要です。するには、衝突検出の方法。これをループすることもできますが、それは目的に反します。これをブロード、ミッド、およびナローコリジョンの手法に適用できますが、それだけで、特に3Dおよび2Dゲームで非常にうまく機能する場合に役立ちます。

もう一度見てみましょう。これは、Minecraftのcollide()メソッドに従って、ブロックの内側に移動するため、プレーヤーを外側に移動する必要があることを意味します。プレーヤーをチェックする代わりに、どのブロックがボックスの両側にヒットしているかをチェックする境界ボックスを追加する必要があります。問題が修正されました。

正確性が必要な場合、上記の段落はポリゴンではそれほど簡単ではないかもしれません。正確にするために、正方形ではなく、モザイク状ではないポリゴン境界ボックスを定義することをお勧めします。そうでない場合は、長方形で十分です。

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