Unityの負荷対策、ゲーム開発者なら避けては通れない道です。
GetComponentはなるべくUpdateで回さずに変数にキャッシュして使いまわす、
UIは書き換えタイミングが同じもの毎になるべくCanvasを分割する、
とか対策方法は色々ありますよね。
その中で、オブジェクトの管理方法としてオブジェクトプール パターン(Object Pool Pattern)という手法があります。
同じオブジェクトを大量に使う時に都度Instantiate/Destroyするのではなく、
あらかじめオブジェクトを生成しておき、SetActiveでアクティブを切り替えることでオブジェクトを使いまわす、
という手法です。
Instantiate/Destroyは、Unityにおける処理の中でもかなり重めの処理のため、
多くのオブジェクトを扱うシューティングゲームや最近流行りのヴァンパイアサバイバー系のゲームなどでは、オブジェクトプールパターンはかなり有効な負荷軽減対策になります。
オブジェクトプールパターンを使用しているのでオブジェクト生成の負荷対策はバッチリ!
…と思いきや、SetActive()も割と処理負荷的には大きいみたいなので、今回はどの処理がどのくらい時間かかるのかの検証と対策方法についてお話していきます。
もしオブジェクトプールについて詳しく知りたい方はこちらをご覧ください。
オブジェクトプール(Object Pool)による負荷軽減率
ローポリスライムでのオブジェクトプールの場合
![](https://huchat-gamedev.net/wp-content/uploads/2024/01/image-32.png)
まずは
・ポリゴン数1009
・ボーン数10
・テクスチャサイズ512×512が5枚
のスライムで検証してみます。
![](https://huchat-gamedev.net/wp-content/uploads/2024/01/image-35-1024x578.jpg)
各処理を1万回ずつ繰り返した結果は次の通りです。
処理 | 処理時間(ms) |
Instantiate() | 863 |
Destroy() | 606 |
SetActive(true) | 565 |
SetActive(false) | 230 |
各処理にかかった時間としては、
SetActive(true)がInstantiate()より約35%減
SetActive(false)がDestroy()より約62%減
という結果でした。
いずれもオブジェクトプールパターンにすることで処理負荷は結構抑えられていますが、
オブジェクト生成→オブジェクトアクティブ化にした方は負荷軽減率は少なめなようです。
ミドルポリドラゴンでのオブジェクトプールの場合
![](https://huchat-gamedev.net/wp-content/uploads/2024/01/image-34.png)
次に
・ポリゴン数4832
・ボーン数119
・テクスチャサイズ2048×2048が1枚
のドラゴンで検証します。
![](https://huchat-gamedev.net/wp-content/uploads/2024/01/image-35-1-1024x570.jpg)
各処理を1万回ずつ繰り返した結果は次の通りです。
処理 | 処理時間(ms) |
Instantiate() | 3868 |
Destroy() | 1908 |
SetActive(true) | 1471 |
SetActive(false) | 529 |
各処理にかかった時間としては、
SetActive(true)がInstantiate()より約62%減
SetActive(false)がDestroy()より約72%減
という結果でした。
先ほどのスライムに比べて、
オブジェクト生成→オブジェクトアクティブ化にした際の負荷軽減率は高くなっていました。
この結果から、より描画負荷が高いオブジェクトの時はオブジェクトプールパターンを使用する時のメリットがより高くなる可能性があるのかもしれません。
ただ、Instantiate/Destroyするよりも負荷が軽減されているとはいえ、
一度にSetActive()を回すと、まだそこそこ負荷がかかっているのが分かります。
どうすればもっと負荷を抑えられるでしょうか?
その他の負荷軽減方法
一例にはなりますが、その他の負荷軽減方法としては以下の方法があります。
・オブジェクトのアクティブを制御するのではなく、コンポーネントのアクティブを制御する
・アクティブを制御するのではなく、見えない位置に移動させる
今回はこちらの方法を試してみます。
コンポーネントのアクティブ制御(enable)による対策
この対策方法はオブジェクトを消したいタイミングで、オブジェクト自体のアクティブを変えるのではなく、オブジェクトに紐づいているコンポーネントのアクティブを制御する方法です。
オブジェクト自体のアクティブの制御より、コンポーネントのアクティブの制御の方が処理負荷は低いらしいです。
今回テストに使ってるモデルは「Skinned Mesh Renderer」と「Animator」しかコンポーネントが付いていないので、これらのアクティブを制御して試してみます。
プログラムの内容としてはざっくりこんな形です。
List<SkinnedMeshRenderer> _skinMeshComponents = new List<SkinnedMeshRenderer>();
List<Animator> _animatorComponents = new List<Animator>();
public void SetActiveComponent(bool isActiveComponent){
foreach (var obj in _objects)
{
_skinMeshComponents.AddRange(obj.GetComponentsInChildren<SkinnedMeshRenderer>());
_animatorComponents.AddRange(obj.GetComponentsInChildren<Animator>());
}
foreach (var component in _skinMeshComponents)
{
component.enabled = isActiveComponent;
}
foreach (var component in _animatorComponents)
{
component.enabled = isActiveComponent;
}
}
これを先ほどと同じく、先ほどのドラゴン1万体に対して一度に実行してみた結果がこちらです。
処理(ドラゴン1万体に対して) | 処理時間(ms) |
予めコンポーネントをキャッシュした状態で enable = true | 147 |
予めコンポーネントをキャッシュした状態で enable = false | 23 |
コンポーネントの取得も含めて enable = true | 346 |
コンポーネントの取得も含めて enable = false | 248 |
この結果から、
GetComponentsInChildren()を含めた処理でもコンポーネントのアクティブ制御の方が、オブジェクトのアクティブ制御よりもかなり速くなっているのが分かります。
予めコンポーネントをキャッシュしている場合は、更に半分以下に負荷が軽減されていますね。
ただし、
今回のようなやり方では、どのコンポーネントを管理するかを事前に決めとかなければならないため、
色んな種類のオブジェクトに対して管理する場合、管理が少し面倒になってしまう欠点があります。
コンポーネントの取得方法や管理方法についてはひと工夫必要そうですね。
また、管理するコンポーネントの数や種類によっては処理負荷が変動するため、
オブジェクトのアクティブ制御とコンポーネントのアクティブ制御でどのくらい処理負荷が変わるかは、実際に試してみて検討した方が良さそうです。
オブジェクトの移動(position)による対策
この対策方法はオブジェクトを消したいタイミングで、オブジェクトをレンダリング範囲外の位置に移動させることで、疑似的にオブジェクトを消したように見せる方法です。
これを先ほどと同じく、ドラゴン1万体に対して一度に実行してみた結果がこちらです。
処理(ドラゴン1万体に対して) | 処理時間(ms) |
position = new Vector3(0, -100, 0) | 10 |
こちらの結果を見て分かる通り、オブジェクトの移動処理自体にはほぼ処理負荷はかからないようです。
ただし、
レンダリング範囲外に移動させているのでレンダリングによる負荷は減りますが、
オブジェクトやコンポーネント自体はアクティブ状態のままなので、その他の処理負荷はかかったままになってしまいます。
一瞬の高負荷に対する軽減対策(スパイク対策)にはなりますが、
必要ないオブジェクトをずっとアクティブ状態にしておくのはかなり無駄な負荷となってしまうので、その後にオブジェクトを削除/非アクティブにする処理は必要です。
例えば一気にオブジェクトを消したいときは、
一時的にポジションを移動
→その後複数フレームに分割してオブジェクトを非アクティブにしていく
という形にすれば同時にかかる負荷は軽減しつつ上手くオブジェクトの管理が出来そうです。
注意点
今回の検証ではCPUでの処理時間の比較しかしておらず、メモリ使用量については考慮されていません。
実際にオブジェクトプールパターンを使用する際や、今回紹介した他2つの方法を使う際、全てのオブジェクトで行えばいいのかというとそういう訳ではなく、メモリの使用量についても注意は必要です。
オブジェクトプールを作成しすぎてメモリ不足になる、などのケースもあるので、
不要なオブジェクトは、なるべく1フレームでの負荷が大きくなりすぎないようにした上で、
徐々にDestroyしていくのがいいと思います。
まとめ
- Instantiate/DestroyはオブジェクトのSetActiveに変更することで、1.5~4倍以上処理負荷が軽減された
- より描画負荷が高いオブジェクトの方がSetActiveにすることによる負荷軽減率が大きかった
- オブジェクトのSetActiveよりもコンポーネントのenable切り替えの方が2~4倍ほど処理負荷が軽減された
- オブジェクトの位置移動はほぼ処理負荷がかからなかった
- メモリ使用量も気にしながら、不要なオブジェクトは徐々に削除しよう
コメント