Hero background image
オブジェクトプールを使ってUnityのC#スクリプトのパフォーマンスを向上させる

Unityプロジェクトに一般的なゲームプログラミングデザインパターンを実装することで、クリーンで整理された読みやすいコードベースを効率的に構築し、維持することができます。デザインパターンは、リファクタリングやテストに費やす時間を削減するだけでなく、オンボーディングや開発プロセスをスピードアップし、ゲーム、開発チーム、そしてビジネスを成長させるための強固な基盤に貢献する。

デザイン・パターンは、コードにコピー&ペーストできる完成されたソリューションではなく、正しく使用すれば、より大規模でスケーラブルなアプリケーションを構築するのに役立つ追加ツールだと考えてほしい。

このページでは、オブジェクトプールと、それがゲームのパフォーマンスを向上させるのに役立つ方法について説明します。Unityのビルトインオブジェクトプーリングシステムをプロジェクトに実装する方法の例も含まれています。

ここでの内容は無料電子書籍に基づいています、 ゲームプログラミングパターンでコードをレベルアップよく知られているデザインパターンを説明し、Unityプロジェクトでそれらを使用するための実践的な例を共有しています。

Unityゲームプログラミングパターンシリーズの他の記事は、Unityベストプラクティスハブでご覧いただけます:

4-2 階層
Unity におけるオブジェクトプーリングについて

オブジェクト・プーリングは、生成と破棄を繰り返す呼び出しに必要なCPUの処理能力を削減することで、パフォーマンスを最適化するデザイン・パターンである。その代わり、オブジェクト・プールを使えば、既存のGameObjectを何度でも再利用できる。

オブジェクト・プーリングの重要な機能は、オンデマンドでオブジェクトを作成・破棄するのではなく、あらかじめオブジェクトを作成し、プールに保存しておくことである。オブジェクトが必要になったらプールから取り出して使用し、不要になったら破棄するのではなくプールに戻す。

上の画像は、オブジェクト・プーリングの一般的な使用例を示している。この例を順を追って説明しよう。

オブジェクト・プール・パターンでは、生成して破棄する代わりに、初期化されたオブジェクトのセットを使用し、非アクティブ化プールで待機させる。そしてこのパターンは、ゲームプレイの前の特定の瞬間に必要なオブジェクトをすべて事前に定義する。このプールは、ローディング画面中など、プレイヤーがスタッターに気づかないようなタイミングを見計らって作動させる必要がある。

プールのGameObjectが使用されると、それらは非アクティブ化され、ゲームが再びそれらを必要とするときに使用できるようになる。オブジェクトが必要になったとき、アプリケーションはまずそれをインスタンス化する必要はない。その代わりに、プールからリクエストし、アクティベートとデアクティベートを行い、破棄する代わりにプールに戻すことができる。

このパターンは、次のセクションで説明するように、ガベージコレクションを実行するためにメモリ管理から必要とされる重労働のコストを削減することができる。

Unityプロファイリング電子書籍
メモリ割り当て

オブジェクト・プーリングを活用する方法の例に入る前に、オブジェクト・プーリングが解決する根本的な問題を簡単に見ておこう。

プーリング技術は、インスタンス化や破棄操作に費やすCPUサイクルを削減するためだけに役立つわけではない。また、メモリの確保と解放、コンストラクターとデストラクターの呼び出しが必要なオブジェクトの生成と破棄のオーバーヘッドを減らすことで、メモリ管理を最適化する。

Unityのマネージドメモリ

UnityのC#スクリプト環境は、マネージドメモリシステムを提供しています。メモリの解放を管理してくれるので、コードを通して手動で要求する必要はない。メモリ管理システムはまた、メモリアクセスを保護し、使用しなくなったメモリが解放されるようにし、あなたのコードにとって有効でないメモリへのアクセスを防止する。

Unityはガベージコレクタを使用して、アプリケーションとUnityが使用しなくなったオブジェクトからメモリを回収します。なぜなら、管理されたメモリを割り当てるにはCPUにとって時間がかかり、ガベージコレクション(GC)がそのタスクを完了させるまで、CPUが他の仕事をするのを止めてしまうかもしれないからである。

Unityで新しいオブジェクトを作成したり、既存のオブジェクトを破棄したりするたびに、メモリが割り当てられたり解放されたりします。ここでオブジェクト・プーリングが活躍する:ガベージコレクションのスパイクに起因するスタッタリングが軽減される。GCスパイクは、メモリの割り当てによる大量のオブジェクトの作成や破棄にしばしば伴う。早すぎるガベージコレクションに加え、このプロセスはメモリの断片化を引き起こし、連続した空きメモリ領域を見つけることを難しくする。

同じ既存のオブジェクトを非アクティブにしたりアクティブにしたりして再利用することで、画面外に何百発もの弾丸を発射するような効果を作り出すことができる。

メモリ管理については アドバンスプロファイリングガイドをご覧ください。

オブジェクト・プール・サンプル・プロジェクト
UnityEngine.Poolの使用

オブジェクトプールを実装するために独自のカスタムシステムを作成することもできますが、プロジェクトでこのパターンを効率的に実装するために使用できるUnityの組み込みObjectPoolクラスがあります(Unity 2021 LTS以降で利用可能)。

UnityEngine.PoolAPIを使ったビルトインオブジェクトプーリングシステムの活用方法を、Githubで公開されているサンプルプロジェクトで見てみましょう。Githubのページにアクセスしたら、Assets>7 Object Pool >Scripts >ExampleUsage2021と進み、ファイルを探してください。

注:Unity Learnのチュートリアルで、以前のバージョンのUnityのオブジェクトプールの例を見ることができます。

この例では、マウスボタンが押されると、タレットが発射弾(デフォルトでは毎秒10発に設定されている)を高速で発射する。それぞれの発射体はスクリーンを横切って移動し、スクリーンから離れたら破壊する必要がある。オブジェクト・プールがないと、前のセクションで説明したように、CPUとメモリ管理にかなりの負担がかかる。

オブジェクト・プーリングを使うことで、何百発もの弾丸が画面外で発射されているように見えるが、実際は無効化されて何度もリサイクルされているだけだ。

サンプル・スクリプトのコードは、プール・サイズが同時にアクティブなオブジェクトを表示するのに十分な大きさであることを確認し、同じオブジェクトが常に再利用されているという事実をカモフラージュするのに役立つ。

Unityのパーティクルシステムを使ったことがある人なら、オブジェクトプールを実際に体験したことがあるでしょう。パーティクルシステムコンポーネントには、パーティクルの最大数の設定があります。これは利用可能な粒子を再利用し、効果が最大数を超えるのを防ぐ。オブジェクト・プールも同様に機能するが、好きなGameObjectを使うことができる。

Unpacking RevisedGun.cs

のコードを見てみよう。 のコードを見てみよう。これはGithubデモのAssets>7 Object Pool >Scripts >ExampleUsage2021にあります。

まず注目すべきは、プールの名前空間が含まれていることだ:

using UnityEngine.Pool;

UnityEngine.Pool APIを使用すると、スタックベースの オブジェクトプールクラスを取得します。必要に応じて、CollectionPoolクラス(List、HashSet、Dictionaryなど)を使用することもできます。

次に、銃の発射特性について、スポーンするPrefab(RevisedProjectile型のprojectilePrefabという名前)を含む特定の設定を適用します。

ObjectPoolインターフェイスはRevisedProjectile.cs(次のセクションで説明する)から参照され、Awake関数で初期化される。

private void Awake()

{
objectPool = new ObjectPool<RevisedProjectile>(CreateProjectile、

OnGetFromPool、OnReleaseToPool、

OnDestroyPooledObject,collectionCheck,defaultCapacity,maxSize);
}

ObjectPool<T0>コンストラクタを調べてみると、いくつかのロジックを設定する便利な機能が含まれていることがわかる:

まず、プールを構成するためのプール項目を作成する。

プールからアイテムを取り出す

アイテムをプールに戻す

プールされたオブジェクトの破棄(最大制限に達した場合など)

組み込みのObjectPoolクラスには、デフォルト・プール・サイズと最大プール・サイズの両方のオプションが含まれていることに注意してください。Releaseを呼び出すとトリガーされ、プールが満杯なら代わりに破壊される。

このコード例では、特定のユースケースに応じて、Unityがオブジェクトプールを効率的に処理する方法を指定するいくつかのアクションを取る方法を見てみましょう。

まず、プールが空のときに新しいインスタンスを生成するために使用されるcreateFuncが渡されます。この場合は、新しいプロファイルPrefabをインスタンス化するCreateProjectile()です。

private RevisedProjectile CreateProjectile()

{
RevisedProjectile projectileInstance = Instantiate(projectilePrefab);
projectileInstance.ObjectPool = objectPool;

return projectileInstance;
}

OnGetFromPoolはGameObjectのインスタンスを要求するときに呼ばれるので、デフォルトではプールから取得するGameObjectを有効にする。

private void OnGetFromPool(RevisedProjectile pooledObject).

{
pooledObject.gameObject.SetActive(true);
}

OnReleaseToPoolは、GameObjectが不要になり、プールに戻されるときに使われる。

private void OnReleaseToPool(RevisedProjectile pooledObject)

{
pooledObject.gameObject.SetActive(false);
}

OnDestroyPooledObjectは、プールされているアイテムの最大許容数を超えたときに呼び出されます。プールが満杯の場合、オブジェクトは破棄される。

private void OnDestroyPooledObject(RevisedProjectile pooledObject)

{
Destroy(pooledObject.gameObject);
}

collectionChecksはIObjectPoolの初期化に使われ、すでにプール・マネージャーに返されたGameObjectをリリースしようとすると例外を投げるが、このチェックはEditorでのみ行われる。これをオフにすることで、CPUサイクルを節約することができるが、すでに再アクティブ化されたオブジェクトが返されるリスクがある。

その名の通り、defaultCapacityは、要素を格納するスタック/リストのデフォルトのサイズであり、したがって、どれだけのメモリ割り当てを前もってコミットしたいかである。maxPoolSizeはスタックの最大サイズとなり、作成されるプールされたGameObjectはこのサイズを決して超えてはならない。つまり、満杯のプールにアイテムを戻した場合、代わりにアイテムは破壊される。

そうすれば、FixedUpdate()では、弾を発射するロジックを実行するたびに新しい発射体をインスタンス化する代わりに、プールされたオブジェクトを取得することができます。

RevisedProjectile bulletObject = objectPool.Get();

簡単なことだ。

RevisedProjectile.csの解凍

それでは、RevisedProjectile.csスクリプトを見てみよう。

ObjectPoolへの参照を設定することで、オブジェクトをプールに戻すのがより便利になるほかにも、いくつか興味深い詳細がある。

timeoutDelayは、発射体がいつ「使用」され、再びゲームプールに戻ることができるかを追跡するために使用される。

Deactivate()関数は、DeactivateRoutine(float delay)というコルーチンをアクティブにし、objectPool.Release(this)で発射体をプールに戻すだけでなく、移動するRigidbodyの速度パラメータもリセットします。

このプロセスは、"ダーティー・アイテム "の問題に対処するものである。

この例でわかるように、UnityEngine.Pool APIを使用すると、オブジェクトプールを効率的にセットアップできます。

GameObjectsだけに限りません。プーリングは、GameObject、インスタンス化されたPrefab、C#辞書など、あらゆるタイプのC#エンティティを再利用するためのパフォーマンス最適化テクニックである。Unityは、辞書をサポートするDictionaryPool<T0,T1>やHashSetをサポートするHashSetPool<T0>など、他のエンティティ用の代替プーリングクラスをいくつか提供しています。詳しくはドキュメントをご覧ください。

LinkedPoolは、再利用のためにオブジェクト・インスタンスのコレクションを保持するためにリンクリストを使用します。これは、実際にプールに格納されている要素にのみメモリを使用するため、(場合にもよりますが)メモリ管理をより良くすることができます。

ObjectPoolは、C#スタックとC#配列の下を使うだけなので、連続したメモリの大きな塊を含む。欠点は、デフォルトサイズ(defaultSize)と最大サイズ(maxSize)を利用してニーズを設定できるObjectPoolよりも、LinkedPoolでこのデータ構造を管理する方が、アイテムごとに多くのメモリと多くのCPUサイクルを費やすことだ。

ブログカバー
オブジェクト・プーリングを実装する他の方法

オブジェクト・プールをどのように使うかは用途によって異なるが、先の例で示したように、武器が複数の弾丸を発射する必要がある場合に、このパターンがよく現れる。

ガベージコレクションのスパイクを引き起こす危険性があるため、経験則では、大量のオブジェクトをインスタンス化するたびにコードをプロファイリングするのがよい。ゲームプレイがスタッタリングするような大きなスパイクが検出される場合は、オブジェクトプールの使用を検討してください。ただ、オブジェクト・プールは、プールの複数のライフサイクルを管理する必要があるため、コードベースがより複雑になる可能性があることを覚えておいてほしい。さらに、早すぎるプールを作ることで、ゲームプレイに必ずしも必要でないメモリを確保してしまう可能性もある。

先に述べたように、この記事に含まれる例以外にも、オブジェクト・プーリングを実装する方法はいくつかある。ひとつの方法は、ニーズに合わせてカスタマイズできる独自の実装を作ることだ。しかし、型やスレッドセーフの複雑さ、カスタムオブジェクトのアロケーション/ディロケーションの定義に注意する必要がある。

嬉しいことに、Unity Asset Storeには時間を節約するための素晴らしい代替手段がいくつか用意されています。

Unityでプログラミングするための高度なリソース

電子書籍 ゲーム・プログラミング・パターンでコードをレベルアップしようには、簡単なカスタム・オブジェクト・プール・システムのより詳細な例が掲載されている。Unity Learnでは、オブジェクトプーリングの入門も提供しており、こちらでご覧いただけます。また、2021 LTSの新しいビルトインオブジェクトプーリングシステムを使用するための完全なチュートリアルもあります。

すべての高度なテクニカル電子書籍と記事は Unityベストプラクティスハブにあります。電子書籍は でもご覧いただけます。 高度なベストプラクティスページでもご覧いただけます。

このコンテンツにご満足いただけましたか?