Unityの検索機能

C# のデータ構造と Unity API を使って最適に作業を行いましょう

Last updated: December 2018

このページで学ぶ内容:Unity API とデータ構造を適切に処理するための解決策。Particle System API を使用した再帰的なイテレーション、無駄なメモリ割り当て、配列を返す API、不適切なデータ構造の選択、辞書の誤用による過剰なオーバーヘッドなどの課題を網羅します。

ただし、その多くはより複雑な処理を伴い、結果的にメンテナンスのオーバーヘッドやバグの増大を増やします。したがって、ヒントを利用する際は、事前にコードをプロファイリングしてください。

すべてのヒントは Unite のセッション、Squeezing Unity: Tips for raising performance で紹介されています。Unity のアーキテクチャがデータ指向設計に発展したことを考慮した、Ian の新しいヒントを、 ベストプラクティスを発展させるでご覧ください。

Unity API

問題:パーティクルシステム API を使用した再帰的なイテレーション:

Start、Stop、IsAlive() など、Particle System の主な API を呼び出すと、デフォルトで設定されている Particle System のヒエラルキー内のすべての子要素に対して再帰的に反復処理されます。

  • API はパーティクルシステムの Transform 内のすべての子要素を探し出し、全 Transform でGetComponent を呼び出し、Particle System がある場合は、適切な内部メソッドを呼び出します。
  • それらの子の Transform に独自の子要素がある場合は、Transform のヒエラルキー内のすべての子要素、孫要素を含め、最下位まで再帰的に処理され繰り返されます。深いTransform 階層の場合、これは問題になる可能性があります。

解決法 :

これらの API には、デフォルトで true になるwithChildrenパラメータがあります。この再帰的な振る舞いを回避するには、false に設定してください。withChildren が false に設定されているとき、直接呼び出しているパーティクルシステムのみが変更されます。

問題:同時に開始および停止する複数のパーティクルシステム

パーティクルシステムを複数の子の Transforms に分散させたビジュアルエフェクトをアーティストが作成するのは一般的なことです。そのすべてについて、同時に開始および停止することをお勧めします。

解決法 :

初期化時に、 list GetComponent を呼び出して、Particle Systems のリストをキャッシュする MonoBehaviour を作成します。次に、Particle Systems を変更する必要があるときは、Start、Stop など順番に呼び出し、withChildren パラメータとして false を渡すようにします。

問題:頻繁なメモリの割り当て

Simulate C# クロージャがローカル変数を閉じるとき、C# ランタイムは、変数を追跡するためにヒープに参照を割り当てる必要があります。Unity バージョン 5.4から 2017.1 には、呼び出されると、内部的にクロージャを使用するParticle System API がいくつかあります。これらの Unity バージョンでは、パーティクルシステムがすでに停止している場合でも、Stop およびSimulate へのすべての呼び出しはメモリを割り当てます。

解決法 :

すべての Particle System API は、Unity のほとんどのパブリック API と同様に、API の実際の作業を実行する内部関数の C# ラップアラウンド関数です。これらの内部関数の中には、ピュアな C# の関数もありますが、ほとんどが Unity エンジンの C++コアを呼び出すことになります。Particle System API の場合、すべての内部 API は便利に「Internal_」という名前で、その後にパブリック関数の名前(以下参照)が続きます。

work-optimally-with-Unity-APIs-table-graph

上記に挙げたような関数が続きます。

Unity のパブリック API にクロージャを介して内部ヘルパー機能を呼び出させる代わりに、拡張メソッドを記述するか、Reflection を介して内部メソッドを処理する、そして後で使用するために参照をキャッシュすることができます。関連する関数シグネチャは以下の通りです。

work-optimally-with-Unity-APIs-functions-signatures

最初の引数を除き、すべての引数はパブリック API と同じ意味を持ちます。最初の引数は、停止またはシミュレートしたいパーティクルシステムへの参照です。

問題:配列を返す API

配列を返す Unity API にアクセスするたびに、その配列の新しいコピーが割り当てられます。これは、配列を返す関数を使うときと、配列を返すプロパティを使うときの両方に起こります。この一般的な例が Mesh.vertices です:これにアクセスするとメッシュの頂点の新しいコピーが得られます。また、Input.touches は、現在のフレームの間にユーザーが行ったすべてのタッチ情報のコピーを提供します。

解決法 :

多くの Unity API には現在割り当てられていないバージョンがありますので、代わりにこれらを使用してください。たとえば、Input.touches の代わりに、Input.GetTouch と Input.touchCount を使用します。バージョン 5.3 以降、すべての Physics クエリ API の割り当てなしのバージョンも導入されました。また、2D アプリケーションについては、すべての Physics2D クエリ API の割り当てなしバージョンもあります。Unity の Physics API の割り当てなしバージョンについては、こちらをご覧ください。 こちら

最後に、GetComponents あるいは GetComponentsInChildren を使用している場合は、汎用テンプレートリストを受け付けるバージョンがあります。これらの API は GetComponentsの呼び出しの結果でリストを書き入れます。これは単なる割り当てなしではありません: GetComponents 呼び出しの結果がリストの処理能力を超えた場合、リストのサイズが変更されます。ただし、リストを再利用またはプールしている場合は、少なくとも割り当ての頻度はアプリケーションの寿命期間中に低下します。

データ構造の最適活用

問題:誤ったデータ構造の選択

使用するだけの便利なデータ構造の選択を回避します。代わりに、あなたが記述しているアルゴリズムやゲームシステムを使って、最もよく一致するパフォーマンス特性を持つものを選択します。

解決法 :

配列またはリストへのインデックス付けは非常に安価: それは、フードの下で少しの追加を必要とし、一定の時間で行われます。したがって、ランダムにインデックス付け、またはイタレーションを行っても、配列やリストを選ぶと、オーバーヘッドは非常に少なくなります。

一定時間の追加または削除をする必要がある場合は、おそらく Dictionary かHashset を使うでしょう(これについては以下で詳しく)。

データをキーバリュー方式で関連付ける場合、そこでデータが一方向に関連付けられる場合は、Dictionary を使用します。

Hashsets & Dictionaries に関する詳細: これらのデータ構造は両方ともハッシュテーブルでバックアップされています。ハッシュテーブルには多くのバケットがあります。 各バケットは基本的に、特定のハッシュコードを持つすべての値を保持するリストです。C# では、このハッシュコードはGetHashCodeメソッドから来ています。指定されたバケット内の値の数は、通常、Hashset または Dictionary の合計サイズよりはるかに小さいので、バケットへの値の追加および削除は、リストまたは配列からランダムに値を追加または削除よりもずっと一定時間に近くなります。正確な違いは、ハッシュテーブルのサイズとそこに格納しているアイテムの数によって異なります。

Hashset または Dictionary 内の指定された値の有(または 無)のチェックは、同じ理由から非常に安価です:ハッシュコード値を表すバケット内の(比較的少さい)値を簡単に確認できます。

問題:辞書の誤用によるオーバーヘッドが多すぎる

フレームごとにデータ項目のペアを反復処理したい場合は、辞書は得策なのでよく使われます。これに付随する問題点は、ハッシュテーブルを反復処理しているということです。つまり、ハッシュテーブル内のすべての単一のバケットは、値が含まれているかどうかにかかわらず、繰り返し実行する必要があります。これは、特に、内部に格納されている値が少ないハッシュテーブルを反復処理するとき、かなりのオーバーヘッドを追加します。

解決法 :

代わりに、構造体またはタプルを作り、データ関連を含むリストか、それらの構造体またはタプルの配列を格納します。Dictionary を反復処理するのではなく、この List/配列を反復処理します。

14時25分頃からのイアン氏のセッションでは、 [講演はこちら]オーバーヘッドを減らすために InstanceID-keyed(有鍵)辞書をどのようにそしていつ使うべきかについての簡単なヒントを紹介します。

問題:複数の懸念が存在する場合

現実の世界では、もちろん、ほとんどの問題で複数の要件が重複しているため、すべてのニーズを満たす1つのデータ構造はありません。

一般的な例としては、Update マネージャーが挙げられます。 [詳細]これは、アーキテクチャ的なプログラミングパターンによってオブジェクト(通常は MonoBehaviour)が Update コールバックをゲーム内のさまざまなシステムに配布します。システムが Update を受信したい場合、Update Manager のオブジェクトを介して購読します。このようなシステムでは、オーバーヘッドの少ないイテレーション、一定の時間間隔で処理を実行、そして一定の時間間隔で重複チェックが必要です。

解決策:2つのデータ構造を使う

  • イテレーション用のリストまたは配列を管理します。
  • リストを変更する前に、Hashset(または別の種類のインデックスセット)を使用して、追加または削除するアイテムが実際に存在するようにします。
  • 削除が問題になる場合は、リンクリストまたは侵入型リンクリスト(intrusively-linked list)の実装を検討してください (これにより、コスト面でのメモリコストとイテレーションオーバーヘッドがわずかに増加することにご注意ください) 。
その他のリソース

今後のためにうかがいます。このコンテンツはいかがでしたか?

もっと読みたい いまいちです
OK

弊社のウェブサイトは最善のユーザー体験をお届けするためにクッキーを使用しています。詳細については、クッキーのポリシーのページをご覧ください。