O(N ^ 2)関数の改善(すべてのエンティティが他のすべてのエンティティを反復処理する)


21

少し背景を説明しますが、エンティティシステムにENTTを使用して、C ++の友人と進化ゲームをコーディングしています。クリーチャーは2Dマップ内を歩き回り、緑や他のクリーチャーを食べ、繁殖し、その形質が変化します。

さらに、ゲームをリアルタイムで実行した場合のパフォーマンスは良好です(60fpsは問題ありません)が、大幅な変更を確認するために4時間待つ必要がないように大幅にスピードアップできるようにしたいと思います。だから、私はできるだけ早くそれを取得したい。

クリーチャーが食物を見つけるための効率的な方法を見つけるのに苦労しています。各クリーチャーは、彼らに十分近い最高の食べ物を探すことになっています。

ゲームのスクリーンショットの例

食べたい場合、中央に描かれた生き物は、149.64の半径(視界距離)で自分の周りを見回し、栄養、距離、およびタイプ(肉または植物)に基づいて、追求すべき食物を判断することになっています。 。

すべてのクリーチャーの食料を見つける機能は、実行時間の約70%を消費します。現在どのように書かれているかを単純化すると、次のようになります。

for (creature : all_creatures)
{
  for (food : all_entities_with_food_value)
  {
    // if the food is within the creatures view and it's
    // the best food found yet, it becomes the best food
  }
  // set the best food as the target for creature
  // make the creature chase it (change its state)
}

この関数は、クリーチャーが食物を見つけて状態が変わるまで、食物を探しているすべてのクリーチャーのティックごとに実行されます。また、すでに特定の食物を追いかけているクリーチャーに新しい食物が出現するたびに実行され、すべての人が利用可能な最高の食物を確実に追跡します。

このプロセスをより効率的にする方法についてのアイデアを受け入れています。O(N2)から複雑さを減らしたいのですが、それが可能かどうかはわかりません。

私がすでに改善している方法の1つは、all_entities_with_food_valueグループをソートして、クリーチャーが食べるには大きすぎる食物を繰り返したときに、そこで止まるようにすることです。その他の改善点は大歓迎です!

編集:返信いただきありがとうございます!私はさまざまな答えからさまざまなものを実装しました:

私はまず、罪悪感関数が5ティックごとに1回だけ実行されるように単純に作成しました。これにより、ゲームについて何も目に見えて変化することなく、ゲームが約4倍高速になりました。

その後、食品検索システムに、それが実行されるのと同じティックで生成された食品の配列を保存しました。このように、クリーチャーが追いかけている食べ物と登場した新しい食べ物を比較するだけです。

最後に、スペースパーティショニングの研究とBVHとクアッドツリーを検討した後、後者に進みました。これは私のケースにはるかに単純で、より適していると感じたからです。私はそれを非常に迅速に実装し、パフォーマンスを大幅に改善しました。食べ物の検索にはほとんど時間がかかりません!

今ではレンダリングが私の速度を遅くしているものですが、それは別の日の問題です。皆さん、ありがとうございました!


2
同時に実行している複数のCPUコアで複数のスレッドを試しましたか?
エドマーティ

6
平均して何体の生き物がいますか?スナップショットから判断すると、それほど高くはないようです。これが常に当てはまる場合、スペースの分割はあまり役に立ちません。ティックごとにこの関数を実行しないことを検討しましたか?たとえば、10ティックごとに実行できます。シミュレーションの結果が定性的に変化することはありません。
混乱

4
食品評価の最も費用のかかる部分を把握するために、詳細なプロファイリングを行いましたか?全体的な複雑さを調べるのではなく、特定の計算やメモリ構造へのアクセスが妨げられているかどうかを確認する必要があるかもしれません。
ハラベック

素朴な提案:現在行っているO(N ^ 2)の代わりに、4分木または関連するデータ構造を使用できます。
セイリア

3
@Harabeckが示唆したように、ループ内のすべての時間が費やされている場所を確認するために、さらに深く掘り下げます。たとえば、距離の平方根計算であれば、XY座標を最初に比較して多くの候補を除外してから、残りの候補に対して高価なsqrtを実行する必要があります。追加if (food.x>creature.x+149.64 or food.x<creature.x-149.64) continue;する方が、パフォーマンスが十分であれば、「複雑な」ストレージ構造を実装するよりも簡単です。(関連:内側のループにもう少しコードを投稿すると役立つ場合があります)
AC

回答:


34

これを衝突として概念化していないことは知っていますが、あなたがしているのは、クリーチャーを中心とした円とすべての食物との衝突です。

遠くにあることがわかっている食べ物を確認したくはありません。近くにあるものだけを確認します。これが衝突最適化の一般的なアドバイスです。衝突を最適化するための手法を検索することをお勧めします。検索時にC ++に限定しないでください。


生き物探しの食べ物

あなたのシナリオでは、世界をグリッドに配置することをお勧めします。衝突させる円の半径以上のセルを作成します。次に、クリーチャーが配置されている1つのセルと最大8つの隣接セルを選択し、最大9つのセルのみを検索できます。

:より小さなセルを作成できます。これは、検索している円が、隣接する範囲を超えて拡張することを意味し、そこで繰り返す必要があります。ただし、食べ物が多すぎるという問題がある場合は、細胞が小さいほど、全体で少ない食べ物を繰り返し処理することになる可能性があります。疑わしい場合は、テストしてください。

食べ物が動かない場合は、作成時にグリッドに食べ物のエンティティを追加できるので、セル内のエンティティを検索する必要はありません。代わりに、セルを照会すると、リストが表示されます。

セルのサイズを2のべき乗にすると、単純に座標を切り捨てることで、クリーチャーが配置されているセルを見つけることができます。

最も近いものを見つけるために比較しながら、2乗距離(別名sqrtを行わない)で作業できます。少ないsqrt操作は、より高速な実行を意味します。


新しい食べ物が追加されました

新しい食物が追加されたとき、近くの生き物だけが目を覚ます必要があります。セル内のクリーチャーのリストを取得する必要があることを除いて、同じ考えです。

さらに興味深いことに、クリーチャー内で追跡している食物からどれくらい離れているかという注釈を付けると、その距離に対して直接確認できます。

あなたを助けるもう一つのことは、どの生き物がそれを追いかけているのかを食物に認識させることです。これにより、食べたばかりの食物を追いかけているすべてのクリーチャーの食物を見つけるためのコードを実行できます。

実際、食物なしでシミュレーションを開始すると、どの生物も注釈付きの無限の距離を持ちます。その後、食べ物を追加し始めます。クリーチャーが移動するときに距離を更新します...食べ物が食べられたら、それを追いかけていたクリーチャーのリストを取り、新しいターゲットを見つけます。その場合に加えて、食べ物が追加されると、他のすべての更新が処理されます。


シミュレーションをスキップする

クリーチャーの速度を知っていれば、それが目標に到達するまでにどれだけあるかを知ることができます。すべてのクリーチャーの速度が同じである場合、最初に到達するクリーチャーは、注釈付きの距離が最小のものです。

食料を追加するまでの時間もわかっている場合... そして、繁殖と死亡について同様の予測可能性があることを願っています。あれば、次のイベントまでの時間(食料を追加するか、クリーチャーが食べる)を知っています。

その瞬間にスキップします。動き回るクリーチャーをシミュレートする必要はありません。


1
「そしてそこでのみ検索します。」セルはすぐ隣にあります-合計9個のセルを意味します。なぜ9 クリーチャーがセルの角にいるとしたらどうでしょう。
UKMonkey

1
@UKMonkey「セルを少なくとも衝突したい円の半径にする」、セル側が半径で、クリーチャーが角にある場合...まあ、その場合は4つだけを検索する必要があると思います。しかし、確かに、細胞を小さくすることができます。これは、食物が多すぎて生き物が少なすぎる場合に役立ちます。編集:明確にします。
Theraot

2
確かに、余分なセルを検索する必要がある場合に解決したい場合...しかし、ほとんどのセルには食べ物がありません(与えられた画像から); 検索する必要のある4つのセルを計算するよりも、9つのセルを検索する方が高速です。
UKMonkey

@UKMonkey。これが最初に言及しなかった理由です。
Theraot

16

BVHのようなスペース分割アルゴリズムを採用する必要がありますて、複雑さを軽減する必要があります。あなたのケースに固有であるためには、食物片を含む軸に沿った境界ボックスで構成されるツリーを作成する必要があります。

階層を作成するには、AABBで食品片を互いに近くに配置し、それらのAABBを、それらの間の距離によって、より大きなAABBに再び配置します。ルートノードができるまでこれを行います。

ツリーを使用するには、最初にルートノードに対してcircle-AABB交差テストを実行し、次に衝突が発生した場合、各連続ノードの子に対してテストします。最後に、食べ物のグループが必要です。

AABB.ccライブラリを利用することもできます。


1
それは確かに複雑さをN log Nに減らしますが、パーティショニングを行うのにも費用がかかります。ティックごとにパーティションを作成する必要があると思う(クリーチャーはすべてのティックを移動するため)パーティション化の頻度を減らすソリューションはありますか?
アレクサンドルロドリゲス

3
@AlexandreRodriguesは、ティックごとにツリー全体を再構築する必要はなく、移動する部分のみを更新し、特定のAABBコンテナの外に何かが出た場合にのみ行います。パフォーマンスをさらに向上させるには、ノードを太らせて(子の間にスペースを空けて)リーフ更新時にブランチ全体を再構築する必要がないようにします。
オセロット

6
ここではBVHは複雑すぎると思う-ハッシュテーブルとして実装された均一なグリッドで十分です。
スティーブン

1
@Stevenは、BVHを実装することで、将来のシミュレーションの規模を簡単に拡張できます。また、小規模なシミュレーションのためにそれを行う場合でも、実際に何かを失うことはありません。
オセロット

2

実際に説明されているスペースパーティションの方法は、主な問題がルックアップだけではない時間を短縮できます。大量のルックアップにより、タスクが遅くなります。したがって、内側のループを最適化しますが、外側のループも最適化できます。

問題は、データをポーリングし続けることです。後部座席に子供たちが千度目の「もうそこにいる」と尋ねるようなもので、運転手があなたがそこにいるときに通知する必要はありません。

代わりに、可能であれば、各アクションを完了まで解決してキューに入れ、それらのバブルイベントを解放するように努力する必要があります。これは、離散イベントシミュレーションと呼ばれます。この方法でシミュレーションを実装できる場合、より優れたスペースパーティションルックアップから得られるスピードアップをはるかに上回る、かなりのスピードアップを探しています。

以前のキャリアでポイントを強調するために、工場シミュレーターを作りました。この方法を使用して、数時間にわたる大きな工場/空港の全品目フローを各アイテムレベルでシミュレートしました。タイムステップベースのシミュレーションは、リアルタイムよりも4〜5倍しか高速にシミュレーションできませんでした。

また、非常に低い成果として、描画ルーチンをシミュレーションから切り離すことを検討してください。シミュレーションは単純ですが、描画のオーバーヘッドが残っています。さらに悪いことに、ディスプレイドライバーが1秒あたりx回の更新に制限しているのに、実際にはプロセッサが100倍の速度で処理できる場合があります。これにより、プロファイリングの必要性が軽減されます。


@Theraot描画物がどのように構成されているかはわかりません。しかし、とにかく十分に高速になれば、はいdrawcallsがボトルネックになります
joojaa

1

スイープラインアルゴリズムを使用して、複雑さをNlog(N)に減らすことができます。理論は、ボロノイ図の理論です。これは、クリーチャーを囲む領域を、他のクリーチャーに近いそのクリーチャーに近いすべてのポイントで構成される領域に分割します。

いわゆるFortuneのアルゴリズムは、Nlog(N)でそれを実行し、そのwikiページには、それを実装するための擬似コードが含まれています。ライブラリの実装も存在するはずです。 https://en.wikipedia.org/wiki/Fortune%27s_algorithm


GDSEへようこそ。お返事ありがとうございます。これをOPの状況にどの程度正確に適用しますか?問題の説明では、実体は視界内のすべての食物を考慮し、最適なものを選択する必要があると述べています。従来のボロノイは、他のエンティティに近い食品を範囲内で除外していました。ボロノイが機能しないと言っているわけではありませんが、説明から、OPが問題に対してどのように使用するかは明らかではありません。
ピカレック

私はこのアイデアが好きです、私はそれが拡大されるのを見たいです。ボロノイ図をどのように表現しますか(メモリデータ構造のように)?どのようにクエリしますか?
Theraot

@Theraotは、同じスイープラインのアイデアだけでボロノイ図を必要としません。
joojaa

-2

最も簡単な解決策は、物理エンジンを統合し、衝突検出アルゴリズムのみを使用することです。各エンティティの周りに円/球を作成し、物理エンジンに衝突を計算させます。2Dの場合、Box2DまたはChipmunk、およびBullet for 3D をお勧めします。

物理エンジン全体の統合が多すぎると感じたら、特定の衝突アルゴリズムを調べることをお勧めします。ほとんどの衝突検出ライブラリは、2つのステップで機能します。

  • ブロードフェーズ検出:この段階の目標は、衝突する可能性のあるオブジェクトの候補ペアのリストをできるだけ早く取得することです。2つの一般的なオプションは次のとおりです。
    • スイープとプルーニング:X軸に沿って境界ボックスを並べ替え、交差するオブジェクトのペアをマークします。他のすべての軸に対して繰り返します。候補ペアがすべてのテストに合格すると、次の段階に進みます。このアルゴリズムは、時間的一貫性の活用に非常に優れています。ソートされたエンティティのリストを保持し、フレームごとに更新できますが、ほとんどソートされているため、非常に高速です。また、空間コヒーレンスも活用します。エンティティは空間の昇順にソートされるため、衝突をチェックしているときに、次のエンティティはすべて遠くにあるため、エンティティが衝突しなくなったらすぐに停止できます。
    • 四分木、八分木、グリッドなどの空間パーティションデータ構造。グリッドは実装が簡単ですが、エンティティ密度が低い場合は非常に無駄が多く、無制限のスペースに実装するのは非常に困難です。静的な空間ツリーも簡単に実装できますが、その場でバランスをとったり更新したりするのは難しいため、フレームごとに再構築する必要があります。
  • 狭いフェーズ:広いフェーズで見つかった候補ペアは、より正確なアルゴリズムでさらにテストされます。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.