はじめに
Unityは2019までバージョンが出ています。
僕がUnityを使い始めたのはまだ標準での2DのUI機能が乏しかったUnity4.xの時代でした。
あの頃から振り返るととてもいい時代になったなと思います。
今更ながらInstantiateについて掘り下げてみたいと思います。
環境
PC: MacBookPro(2015), Windows10
ツール: Untiy2018.3.4f
Instantiateとは
Unity.Objectを複製する関数。
GameObjectの複製でよく使われているイメージがあります。
重いと言われてるイメージがある
サクッと適当なテストコードで重いのか調べてみる
// シンプルに「Start, Updateを持ったComponent」を一つアタッチされたPrefab
public class SampleUI: MonoBehabiour
{
void Start() {}
void Update() {}
}
以下、n回生成するコード
public class SampleCreate: MonoBehabiour
{
[SerializeField] int count;
[SerializeField] SampleUI samplePrefab;
void Start()
{
// ここから計測し、
for(var i = 0; i < count; i++)
{
Instantiate(samplePrefab);
}
// ここまでを結果とする。
}
}
上記をそれぞれ10回ずつ結果をとり、実行にかかった時間の平均値をおおよその値にした結果が以下。
生成数 | 実行時間 |
---|---|
10 | 3.5ms |
1000 | 360ms |
上記はInstantiateだけでかかった時間のみです。
この結果だけで見るとそんなに重くないように思います。
でも実際開発していくと重いと感じることはある。何故?
本当に重いのはInstantiateなのか
Unityにはイベント関数の実行順があります。
PrefabをInstantiateした場合、Prefabに紐づくイベント関数は生成した同一フレームあるいは次フレームで実行されます。
Instantiateが重いと感じる時にこれが原因で重くなっている可能性があります。
実際に以下のコードを各所に埋め込み、確認しました。
Debug.Log($"frame: {Time.frameCount}, time: {Time.realtimeSinceStartup}");
それぞれ、以下の結果になります。
生成タイミング | Awake(OnEnable) | Start | Update |
---|---|---|---|
Start | 生成後すぐ | 1フレーム目 | 1フレーム目 |
Update | 生成後すぐ | 1フレーム目 | 2フレーム目 |
LateUpdate | 生成後すぐ | 1フレーム目 | 2フレーム目 |
実行箇所にもよりますが生成後に上記のイベント関数が実行されていました。
もしPrefabにGraphic(RawImage
, Image
, Text
etc...)が含まれている場合、実行タイミングに合わせてレンダリング処理が実行されます。
問題例
スクロールビューで初回で全生成あるいは画面領域外まで生成するケース。
上記は1000件のセル情報を生成している。
※画面表示外は生成しない、共通のインスタンスは足りる分は流用、足りない分は生成などの実装工夫で回避することはできる。
※この例は「スクロールビューを改善する」ためのものではなく、「スクロールビューでインスタンスを全生成した時に重いという例」として上げています。
対策
予めPrefabを非アクティブにしておく
そうすることで生成後のコストはInstantiateのみで済む。
ただし開発していく上でこの手段が現実的なのかは疑問は残る。
何故現実的ではないと思うのか。
- プレハブを編集する毎にアクティブのオン/オフを切り替えなければならない。
- 人の手で行われる作業なので設定漏れなど発生する恐れがあるため。
- 制作側だけで判断が難しい。
- どういう基準でオン/オフにするのか、基本的にはオフのままにするのか、など。
- このあたりは意識しなくてもいい作りであるのが個人的には健全。
AwakeでGameObjectを非アクティブにする
例えば
var instance = Instantiate(prefab);
// ここで非アクティブにする
instance.SetActive(false);
上記のように生成後すぐに非アクティブにする場合、Awake
の他にOnEnable
も実行される。
これを回避する場合、Instance側のAwakeで非アクティブにすることでOnEnableの実行も回避することができる。
public class SampleUI: MonoBehabiour
{
void Awake()
{
gameObject.SetActive(false);
}
void OnEnable()
{
// 複製すぐは実行されない
Debug.Log("OnEnable");
}
void Start() {}
void Update() {}
}
問題例で活用する場合は画面領域外でオフにすることで生成コストを下げることができる。
検証
試しに全オフにする
private void Awake()
{
gameObject.SetActive(false);
}
画面領域内だけを描画する
for(var i = 0; i < count; i++)
{
var instance = Instantiate(samplePrefab, root);
// 一旦画面では6個までは領域内なので雑に先頭6個だけ表示する、というロジックでテスト
// AwakeはInstantiate実行後即呼ばれるのでその後にアクティブにしても正常に呼び出される。
if (i < 6) {
instance.SetActive(true);
}
}
まとめ
- Instantiateが重いと感じる場合、どういう負荷がかかっているかよく調べたほうがいい。
- 実は思いもよらぬところで負荷がかかっている可能性がある。
- Instantiate単体だけでもそこそこ重い
- Instantiateが本当に重いですか?という指摘の記事で軽いという結論を出したいわけではないので念の為の補足。
- イベント関数の実行順は改めて理解を深められてよかった。
- Instantiate後の挙動はどこにも書かれてない(と思う)のでしっかり把握するのは大切だなと思った。
- 今回Editorでの計測で実機での順序は確認できていません。
- 今回具体的な対策のための記事ではないということ。
- こういう問題が起きているかもしれないので調べる、どうしたら解決できるのかは改めて各々で考えてもらえたらなと思います。
最後に
Instantiateが重いという話は調べると出てくるが何故重いのか、という取り上げ方の記事はあまり見かけないなと思っています。
だからこそ上げてみようと思いましたが、実はこういう理由なのでは、こういうのもあるよということがあればコメントでご指摘いただければと思います。
おまけ
IEnumerator Start()
{
yield return null;
Debug.Log($"Start {Time.frameCount}");
}
void Update()
{
if (Time.frameCount == 2) {
Debug.Log($"Update {Time.frameCount}");
}
}