2D City Builderで高価な関数のパフォーマンスを向上させる方法


9

すでに回答を検索しましたが、高価な関数や計算を処理するための最良のアプローチを見つけることができませんでした。

私の現在のゲーム(2dタイルベースの都市の建物)では、ユーザーは建物を配置したり、道路を構築したりできます。すべての建物は、ユーザーがマップの境界に配置する必要があるジャンクションへの接続を必要とします。建物がこのジャンクションに接続されていない場合、「道路に接続されていません」の標識が影響を受ける建物の上にポップアップ表示されます(それ以外の場合は、削除する必要があります)。ほとんどの建物には半径があり、互いに関連している可能性があります(たとえば、消防署は半径30タイル以内のすべての家を助けることができます)。これは、道路の接続が変更されたときにも更新/確認する必要があることです。

昨日、パフォーマンスの大きな問題に遭遇しました。次のシナリオを見てみましょう。ユーザーはもちろん、建物や道路を消去することもできます。したがって、ユーザーがジャンクションの直後に接続を切断した場合、同時に多くの建物を更新する必要があります。最初のアドバイスの1つはネストされたループを避けることです(これは間違いなくこのシナリオの大きな理由です)が、私は確認する必要があります...

  1. 道路タイルが削除された場合に建物がまだジャンクションに接続されている場合(私はその道路によって影響を受けた建物に対してのみそれを行います)。(このシナリオでは小さな問題である可能性があります)
  2. 半径タイルのリストと、半径内の建物を取得します(ネストされたループ-大きな問題!)

    // Go through all buildings affected by erasing this road tile.
    foreach(var affectedBuilding in affectedBuildings) {
        // Get buildings within radius.
        foreach(var radiusTile in affectedBuilding.RadiusTiles) {
            // Get all buildings on Map within this radius (which is technially another foreach).
            var buildingsInRadius = TileMap.Buildings.Where(b => b.TileIndex == radiusTile.TileIndex);  
    
            // Do stuff.
        }
    }

これはすべて、私のFPS を1秒間で60からほぼ10に分解します。

そうすることができます。私の考えは:

  • このスレッドのメインスレッド(更新関数)を使用せず、別のスレッド。マルチスレッドの使用を開始すると、ロックの問題が発生する可能性があります。
  • キューを使用して多くの計算を処理する(この場合、どの方法が最適ですか?)
  • より多くの計算を回避するために、オブジェクト(建物)により多くの情報を保持します(たとえば、半径内の建物)。

最後のアプローチを使用して、代わりにこのforeachの形式で1つのネストを削除できます。

// Go through all buildings affected by erasing this road tile.
foreach(var affectedBuilding in affectedBuildings) {
    // Go through buildings within radius.
    foreach(var buildingInRadius in affectedBuilding.BuildingsInRadius) {
        // Do stuff.
    }
}

しかし、これで十分かどうかはわかりません。Cities Skylinesのようなゲームは、プレイヤーが大きな地図を持っている場合、はるかに多くの建物を処理する必要があります。それらはそれらをどのように処理しますか?すべての建物が同時に更新するわけではないため、更新キューが存在する可能性があります。

あなたのアイデアやコメントを楽しみにしています!

どうもありがとう!


2
プロファイラーを使用すると、問題のあるコードのビットを特定するのに役立ちます。影響を受ける建物を見つける方法かもしれませんし、多分//ものを見つけるかもしれません。余談ですが、City Skylinesのような大きなゲームは、四分木などの空間データ構造を使用してこれらの問題に取り組みます。そのため、すべての空間クエリは、forループで配列を通過するよりもはるかに高速です。たとえば、あなたの場合、すべての建物の依存関係グラフがあり、そのグラフに従うことにより、反復なしで何が何に影響するかを即座に知ることができます。
Exaila 2017年

詳細情報をありがとう。依存関係のアイデアが好きです!あれを見てみよう!
Yheeky、2017年

あなたのアドバイスは素晴らしかったです!ジャンクション接続がまだ有効かどうかを確認するために、影響を受ける各建物のパスファインディング機能があることを示したVSプロファイラーを使用しました。もちろん、それは地獄のように高価です!約5 FPSですが、何もないよりはましです。これを取り除き、建物を道路タイルに割り当てるので、このパスファインディングチェックを何度も行う必要はありません。どうもありがとう!いいえ、半径の大きい建物を修正するだけで十分です。
Yheeky

役に立って嬉しいです:D
Exaila

回答:


3

建物カバレッジのキャッシュ

どの建物がエフェクターの建物の範囲内にあるか(エフェクターから、または影響を受ける場所にキャッシュすることができます)の情報をキャッシュするというアイデアは、間違いなく良いアイデアです。建物は(通常)移動しないため、これらの高価な計算をやり直す理由はほとんどありません。「この建物への影響」および「この建物への影響」は、建物が作成または削除されたときにのみ確認する必要があるものです。

これは、メモリのCPUサイクルの古典的な交換です。

地域ごとの取材情報の取り扱い

この情報を追跡するにはメモリを使いすぎていることが判明した場合は、そのような情報をマップ領域で処理できるかどうかを確認してください。マップをn*の正方形の領域に分割しますnタイル。地域が消防署で完全に覆われている場合、その地域のすべての建物も覆われています。したがって、カバレッジ情報は、個々の建物ではなく、地域ごとに保存するだけで済みます。リージョンが部分的にしかカバーされていない場合は、そのリージョンでの建物による接続の処理にフォールバックする必要があります。したがって、建物の更新機能は、まず「この建物が含まれている地域は消防署で覆われていますか?」そうでない場合は、「この建物は個別に消防署で覆われていますか?」消防署が削除された場合、2000の建物のカバレッジ状態を更新する必要がなくなるため、これにより更新が高速化されます。更新する必要があるのは100の建物と25の地域のみです。

遅延更新

実行できるもう1つの最適化は、すべてをすぐに更新するのではなく、同時にすべてを更新しないことです。

建物がまだ道路ネットワークに接続されているかどうかは、すべてのフレームをチェックする必要があるものではありません(ちなみに、グラフ理論を少し見て、これを特に最適化するいくつかの方法を見つけることもできます)。建物が建設された後、建物が数秒ごとに定期的にチェックするだけで十分です(道路ネットワークに変更があった場合)。同じことが建物の効果にも当てはまります。建物が数百フレームごとにチェックするだけでも問題はありません。「私に影響を与える少なくとも1つの消防署はまだアクティブですか?」

したがって、更新ループで、更新ごとに一度に数百の建物に対してこれらの高価な計算のみを実行することができます。現在画面上にある建物を優先して、プレーヤーがアクションのフィードバックをすぐに受け取るようにすることができます。

マルチスレッドについて

シティビルダーは、特にプレーヤーに非常に大きなビルドを許可したい場合や、シミュレーションの複雑性を高めたい場合に、計算コストが高くなる傾向があります。したがって、長期的には、ゲーム内のどの計算を非同期で処理できるかを考えることは間違いないかもしれません。


これは、SNES上のSimCityが電源を再接続するためにしばらく時間がかかる理由を説明しています。これは、他のエリア全体の影響でも発生すると思います。
lozzajp

有益なコメントをありがとう!また、より多くの情報をメモリに保持することで、ゲームをスピードアップできると思います。また、TileMapをリージョンに分割するというアイデアも気に入っていますが、このアプローチが長期にわたる最初の問題を取り除くのに十分かどうかはわかりません。アップデートの遅延について質問があります。FPSを60から45に下げる関数があるとしましょう。CPUが処理できる完全な量を処理するために計算を分割する最善の方法は何ですか?
Yheeky

@Yheekyこれには普遍的に適用できる解決策はありません。遅延できる計算、遅延できない計算、および適切な計算単位は状況に大きく依存するためです。
フィリップ2017年

私がこれらの計算を遅らせようとした方法は、「CurrentlyUpdating」フラグを持つアイテムを含むキューを作成することでした。このフラグがtrueに設定されているこのアイテムのみが処理されました。計算が完了すると、アイテムはリストから削除され、次のアイテムが処理されました。これはうまくいくでしょう?しかし、1つの計算自体でFPSがダウンすることがわかっている場合、どのような方法を使用できますか?
Yheeky

1
@Yheeky言ったように、普遍的に適用できる解決策はありません。私が通常試みること(その順序で):1.より適切なアルゴリズムやデータ構造を使用して、その計算を最適化できるかどうかを確認します。2.個別に遅延できるサブタスクに分割できるかどうかを確認します。3.別の脅威で実行できるかどうかを確認します。4.その計算を必要とするゲームメカニックを取り除き、計算コストの少ないものに置き換えることができるかどうかを確認します。
フィリップ2017

3

1.重複した作業

あなたaffectedBuildingsはおそらくお互いに近いので、異なる半径が重なるでしょう。更新が必要な建物にフラグを付けてから、更新します。

var toBeUpdated = new HashSet<Tiles>();
foreach(var affectedBuilding in affectedBuildings) {
    foreach(var radiusTile in affectedBuilding.RadiusTiles) {
         toBeUpdated.Add(radiusTile);

}
foreach (var tile in toBeUpdated)
{
    var buildingsInTile = TileMap.Buildings.Where(b => b.TileIndex == radiusTile.TileIndex);
    // Do stuff.
}

2.不適切なデータ構造。

var buildingsInTile = TileMap.Buildings.Where(b => b.TileIndex == radiusTile.TileIndex);

明らかにする必要があります

var buildingsInRadius = tile.Buildings;

どこ建物があるIEnumerable一定の反復時間で(例えばA List<Building>


いい視点ね!私はMoreLINQを使用してDistinct()を使用してみたと思いますが、これは重複をチェックするよりも速いかもしれないことに同意します。
Yheeky、2017年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.