A *( "A star")経路探索アルゴリズムを使用する単純なタイルベースの2Dゲームを作成しています。すべて正常に動作していますが、検索にパフォーマンスの問題があります。簡単に言えば、通行不可能なタイルをクリックすると、アルゴリズムはどうやらマップ全体を通過して、通行不可能なタイルへのルートを見つけます。
どうすればこれを回避できますか?画面領域へのパスファインディングを減らすことができたと思いますが、おそらくここで明らかな何かを見逃していますか?
A *( "A star")経路探索アルゴリズムを使用する単純なタイルベースの2Dゲームを作成しています。すべて正常に動作していますが、検索にパフォーマンスの問題があります。簡単に言えば、通行不可能なタイルをクリックすると、アルゴリズムはどうやらマップ全体を通過して、通行不可能なタイルへのルートを見つけます。
どうすればこれを回避できますか?画面領域へのパスファインディングを減らすことができたと思いますが、おそらくここで明らかな何かを見逃していますか?
回答:
失敗したパスを完全に引き起こす検索を回避するためのいくつかのアイデア:
A *検索を効果的に高速で完了する最も安価な方法の1つは、検索をまったく行わないことです。すべてのエージェントがエリアを本当に通行できない場合は、ロード時(またはパイプライン内)に一意のアイランドIDを各エリアに塗りつぶします。場合は、チェックを経路探索すると島IDパスの起点は、一致した島のIDを送信先の。一致しない場合、検索を実行するポイントはありません。2つのポイントは、別個の接続されていない島にあります。これは、すべてのエージェントに対して真に通過できないノードがある場合にのみ役立ちます。
検索できるノードの最大数の上限を制限します。これは、通過できない検索を永久に実行するのに役立ちますが、非常に長い一部の通過可能な検索が失われる可能性があることを意味します。この数は調整する必要があり、実際には問題を解決しませんが、長時間の検索に関連するコストを軽減します。
時間がかかりすぎていることがわかっている場合は、次の手法が役立ちます。
検索が別のスレッドで実行されるか、フレームごとに少しずつ実行されるので、ゲームが検索を待って停止することはありません。検索が終了するのを待っている間に、キャラクターが頭を掻いたり足を踏みつけたりするのをアニメーション化します。これを効果的に行うには、検索の状態を別のオブジェクトとして保持し、複数の状態が存在できるようにします。パスが要求されたら、空き状態オブジェクトを取得し、アクティブ状態オブジェクトのキューに追加します。パスファインディングの更新で、アクティブなアイテムをキューの先頭から引き出し、A。が完了するか、B。反復の制限が実行されるまでA *を実行します。完了したら、状態オブジェクトを空き状態オブジェクトのリストに戻します。完了していない場合は、「アクティブな検索」の最後に配置し、次の検索に移動します。
正しいデータ構造を使用していることを確認してください。StateObjectの仕組みを次に示します。すべてのノードは、パフォーマンス上の理由から、有限数(1024または2048など)に事前に割り当てられています。ノードの割り当てを高速化するノードプールを使用します。また、データ構造にポインターの代わりにインデックスを格納することもできます。これは、u16(または255個の最大ノードがある場合はu8、一部のゲームで行う)です。経路探索では、オープンリストに優先度キューを使用して、Nodeオブジェクトへのポインターを保存します。これはバイナリヒープとして実装され、浮動小数点値は常に正であり、プラットフォームの浮動小数点比較が遅いため、整数としてソートします。閉じたマップにハッシュテーブルを使用して、アクセスしたノードを追跡します。キャッシュサイズを節約するために、ノードではなくNodeIDを保存します。
初めてノードにアクセスして目的地までの距離を計算するときは、State Objectに保存されているノードにキャッシュします。ノードに再度アクセスする場合は、キャッシュされた結果を再度計算する代わりに使用してください。私の場合、再訪したノードで平方根を実行する必要はありません。事前に計算してキャッシュできる他の値がある場合があります。
さらに調査できる領域:双方向のパスファインディングを使用して、両端から検索します。私はこれをしていませんが、他の人がこれが役立つかもしれないと指摘したように、それは警告なしではありません。私が試してみるべきもう1つのことは、階層パス検索、またはクラスタリングパス検索です。HavokAIのドキュメントには興味深い説明があります。クラスタリングの概念については、ここで説明したHPA *実装とは異なります。
幸運を祈ります。見つけたものをお知らせください。
AStarは完全な計画アルゴリズムです。つまり、ノードへのパスが存在する場合、AStarはそれを見つけることが保証されます。そのため、目的のノードに到達できないと判断する前に、開始ノードからすべてのパスをチェックアウトする必要があります。ノードが多すぎる場合、これは非常に望ましくありません。
これを軽減する方法:
ノードが到達不能であることをアプリオリに知っている場合(例:隣接ノードがない、またはマークされているUnPassable
)、No Path
AStarを呼び出さずに戻ります。
AStarが終了する前に展開するノードの数を制限します。オープンセットを確認します。大きくなりすぎた場合は、終了してを返しNo Path
ます。ただし、これによりAStarの完全性が制限されます。そのため、最大長のパスのみを計画できます。
AStarがパスを見つけるのにかかる時間を制限します。時間切れになったら、終了してを返しNo Path
ます。これにより、以前の戦略と同様に完全性が制限されますが、コンピューターの速度に応じて拡張されます。多くのゲームでは、これは望ましくないことに注意してください。これは、コンピューターの処理速度が速い場合も遅い場合も、ゲームのプレイ方法が異なるためです。
ターゲットの周囲にアクセスできるタイルが6つのみで、オリジンにアクセス可能なタイルが1002ある場合、検索は6(デュアル)反復で停止します。
一方の検索が他方の訪問済みノードを見つけるとすぐに、検索範囲を他方の訪問済みノードに制限し、より速く終了することもできます。
問題が宛先に到達できないと仮定します。また、ナビゲーションメッシュは動的ではありません。これを行う最も簡単な方法は、ナビゲーショングラフをよりスパースにし(フルランスルーが比較的速いほど十分にスパース)、パスが可能な場合にのみ詳細なグラフを使用することです。
特性の異なる複数のアルゴリズムを使用する
A *にはいくつかの素晴らしい特徴があります。特に、存在する場合は常に最短パスを見つけます。残念ながら、あなたはいくつかの悪い特性も見つけました。この場合、ソリューションが存在しないことを認める前に、考えられるすべてのパスを徹底的に検索する必要があります。
A *で発見している「欠陥」は、トポロジーを認識していないことです。2次元の世界を持っているかもしれませんが、これはわかりません。知っている限りでは、あなたの世界の隅にある階段は、世界の真下を目的地に導く階段です。
トポロジを認識する2番目のアルゴリズムを作成することを検討してください。最初のパスとして、10または100スペースごとに「ノード」で世界を満たし、これらのノード間の接続性のグラフを維持します。このアルゴリズムは、開始点と終了点の近くでアクセス可能なノードを見つけてから、グラフ上のノード間のパスを探します(存在する場合)。
これを行う簡単な方法の1つは、各タイルをノードに割り当てることです。各タイルに1つのノードのみを割り当てる必要があることを示すのは簡単です(グラフで接続されていない2つのノードにアクセスすることはできません)。次に、グラフのエッジは、異なるノードを持つ2つのタイルが隣接する任意の場所に定義されます。
このグラフには欠点があります:最適なパスを見つけられません。それは単に見つけ、パスを。ただし、A *が最適なパスを見つけることができることが示されています。
また、A *関数を作成するために必要な過小評価を改善するためのヒューリスティックを提供します。これは、現在、ランドスケープについての詳細がわかっているためです。後退する必要があることを知る前に、行き止まりを完全に探索する必要はほとんどありません。
マップが静的な場合は、A *を実行する前に、各セクションに独自のコードを用意し、最初にこれを確認するだけです。これは、マップの作成時に行うことも、マップにコーディングすることもできます。
通行不能なタイルにはフラグが必要で、そのようなタイルに移動するときは、A *を実行したり、その隣の到達可能なタイルを選択しないように選択できます。
頻繁に変更される動的なマップがある場合、ほとんど運がありません。完了前にアルゴリズムを停止するか、セクションのチェックを頻繁に行うかを決定する必要があります。
ノードを通過できないとA *をより迅速に結論付けるにはどうすればよいですか?
Node.IsPassable()
関数のプロファイルを作成し、最も遅い部分を見つけて、それらを高速化します。
ノードが通過可能かどうかを判断するときは、最も可能性の高い状況を一番上に置き、ほとんどの場合、関数があいまいな可能性を確認することなくすぐに戻るようにします。
ただし、これは単一ノードのチェックを高速化するためです。ノードの照会に費やされる時間を確認するためにプロファイルを作成できますが、チェックされているノードが多すぎることが問題のようです。
通行不能なタイルをクリックすると、アルゴリズムはどうやらマップ全体を通過して通行不能なタイルへのルートを見つけます
宛先タイル自体が通過できない場合、アルゴリズムはタイルをまったくチェックしません。パス検索を開始する前に、宛先タイルにクエリを実行して、可能かどうかを確認し、不可能な場合はパスなしの結果を返します。
目的地自体は通過可能であるが、パスがないように通過不可能なタイルで囲まれている場合、A *がマップ全体をチェックするのは正常です。他に経路がないことをどのように知るでしょうか?
後者の場合、双方向検索を実行することで速度を上げることができます。この方法では、宛先から開始する検索でパスがないことがすぐにわかり、検索を停止できます。この例を参照して、目的地を壁で囲み、双方向と単一方向を比較してください。
マップに到達できないタイルの大きな連続領域がない場合のみ、これは機能します。パス検索では、到達可能なマップ全体を検索するのではなく、囲まれた到達不可能なエリアのみを検索します。
通行不能なタイルをクリックすると、アルゴリズムはどうやらマップ全体を通過して通行不可能なタイルへのルートを見つけます。
他の答えは素晴らしいですが、私は明白なことを指摘しなければなりません-パスファインディングを通過不可能なタイルに実行するべきではありません。
これは、アルゴリズムを早期に終了する必要があります。
if not IsPassable(A) or not IsPasable(B) then
return('NoWayExists');
2つのノード間のグラフで最長距離を確認するには:
(すべてのエッジの重量が同じであると仮定)
v
。v
これを呼び出しますd
。u
。u
ますw
。これを呼び出します。u
とは、w
グラフ内の最長の距離です。証明:
D1 D2
(v)---------------------------r_1-----------------------------(u)
|
R | (note it might be that r1=r2)
D3 | D4
(x)---------------------------r_2-----------------------------(y)
y
し、x
大きいです!D2 + R < D3
D2 < R + D3
v
とはx
のそれよりも大きいv
とu
?u
第1段階で選ばれなかったであろう。教授の功績 シュロミ・ルビンスタイン
重み付きエッジを使用している場合、BFSの代わりにダイクストラを実行して最も遠い頂点を見つけることにより、多項式時間で同じことを実現できます。
接続グラフであると仮定していることに注意してください。私はそれが無向だと仮定しています。
A *は、単純な2Dタイルベースのゲームにはあまり役に立ちません。なぜなら、私が正しく理解すれば、クリーチャーが4方向に動くと仮定すると、BFSは同じ結果を達成するからです。クリーチャーが8方向に移動できる場合でも、ターゲットにより近いノードを好むレイジーBFSは同じ結果を達成します。A *は修正版のダイクストラであり、BFSを使用する場合よりもはるかに計算コストが高くなります。
BFS = O(| V |)おそらくO(| V | + | E |)ですが、実際にはトップダウンマップの場合はそうではありません。A * = O(| V | log | V |)
わずか32 x 32タイルのマップがある場合、BFSのコストは最大1024で、真のA *のコストはなんと10,000になります。これは0.5秒と5秒の差であり、キャッシュを考慮するとさらに大きくなる可能性があります。そのため、A *が、目的のターゲットに近いタイルを好むレイジーBFSのように動作することを確認してください。
A *は、意思決定プロセスでエッジのコストが重要であるナビゲーションマップに役立ちます。単純なオーバーヘッドタイルベースのゲームでは、エッジのコストはおそらく重要な考慮事項ではありません。イベント(異なるタイルのコストが異なる)の場合、キャラクターを遅くするタイルを通過するパスを延期し、ペナルティを科すBFSの修正バージョンを実行できます。
そのため、多くの場合、タイルに関してはBFS> A *です。
log|V|
あなたの表記を使用してログ(| | V)はsqrt()について- A *の複雑さには本当にオープンセット、またはフリンジの大きさ、およびグリッドのためには、それは非常に小さいです写像することを維持することから来ています。ログ| V | ハイパー接続グラフにのみ表示されます。これは、最悪の場合の複雑さの単純なアプリケーションが誤った結論を与える例です。