UnityのInstantiateについて今更掘り下げてみた

# はじめに
Unityは2019までバージョンが出ています。
僕がUnityを使い始めたのはまだ標準での2DのUI機能が乏しかったUnity4.xの時代でした。
あの頃から振り返るととてもいい時代になったなと思います。

今更ながらInstantiateについて掘り下げてみたいと思います。

# 環境
PC: MacBookPro(2015), Windows10
ツール: Untiy2018.3.4f

# Instantiateとは
Unity.Objectを複製する関数。
GameObjectの複製でよく使われているイメージがあります。

# 重いと言われてるイメージがある
Google先生に聞いてみると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528566/69a35bb1-b0d1-4a05-5827-96ddcf46f113.png)
やはり候補に上がっている。

# サクッと適当なテストコードで重いのか調べてみる

``` SampleUI.cs
// シンプルに「Start, Updateを持ったComponent」を一つアタッチされたPrefab
public class SampleUI: MonoBehabiour
{
    void Start() {}
    void Update() {}
}
```

以下、n回生成するコード

``` SampleCreate.cs
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には[イベント関数の実行順](https://docs.unity3d.com/ja/2017.4/Manual/ExecutionOrder.html)があります。
PrefabをInstantiateした場合、Prefabに紐づくイベント関数は**生成した同一フレームあるいは次フレーム**で実行されます。
Instantiateが重いと感じる時に**これが原因で重くなっている可能性**があります。
実際に以下のコードを各所に埋め込み、確認しました。

```テスト.cs
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...)が含まれている場合、実行タイミングに合わせてレンダリング処理が実行されます。

# 問題例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528566/a29c405d-0bf3-e491-675f-6b4e7b13269f.png)


スクロールビューで初回で全生成あるいは画面領域外まで生成するケース。
上記は1000件のセル情報を生成している。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528566/c256561d-973b-8baf-1174-ddaeb45a08ce.png)
プロファイル結果だとこんな感じ。

**※画面表示外は生成しない、共通のインスタンスは足りる分は流用、足りない分は生成などの実装工夫で回避することはできる。**
**※この例は「スクロールビューを改善する」ためのものではなく、「スクロールビューでインスタンスを全生成した時に重いという例」として上げています。**

# 対策
## 予めPrefabを非アクティブにしておく
そうすることで生成後のコストはInstantiateのみで済む。
ただし開発していく上でこの手段が現実的なのかは疑問は残る。
何故現実的ではないと思うのか。

- プレハブを編集する毎にアクティブのオン/オフを切り替えなければならない。
  - 人の手で行われる作業なので設定漏れなど発生する恐れがあるため。
- 制作側だけで判断が難しい。
  - どういう基準でオン/オフにするのか、基本的にはオフのままにするのか、など。
  - このあたりは意識しなくてもいい作りであるのが個人的には健全。  

## AwakeでGameObjectを非アクティブにする
例えば

``` SampleCreate.cs
var instance = Instantiate(prefab);
// ここで非アクティブにする
instance.SetActive(false);
```

上記のように生成後すぐに非アクティブにする場合、`Awake`の他に`OnEnable`も実行される。
これを回避する場合、**Instance側のAwakeで非アクティブ**にすることでOnEnableの実行も回避することができる。

``` SampleUI.cs
public class SampleUI: MonoBehabiour
{
    void Awake()
    {
        gameObject.SetActive(false);
    }
    void OnEnable()
    {
        // 複製すぐは実行されない
        Debug.Log("OnEnable");
    }
    void Start() {}
    void Update() {}
}
```

問題例で活用する場合は画面領域外でオフにすることで生成コストを下げることができる。

# 検証

試しに全オフにする

``` SampleUI.cs
private void Awake()
{
    gameObject.SetActive(false);
}
```

結果、さっきの結果と比べて半分近くになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528566/2e61fd75-0cb1-7853-f379-f12ac6e326ee.png)

セルは生成されている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528566/631c9386-dc83-4118-0df8-c5d2185dccdb.png)

画面領域内だけを描画する

``` Scroll.cs

for(var i = 0; i < count; i++)
{
    var instance = Instantiate(samplePrefab, root);
    // 一旦画面では6個までは領域内なので雑に先頭6個だけ表示する、というロジックでテスト
    // AwakeはInstantiate実行後即呼ばれるのでその後にアクティブにしても正常に呼び出される。
    if (i < 6) {
        instance.SetActive(true);
    }
}
```

結果は以下の通り
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528566/7981c9b9-7191-d063-3cff-8d0a584cbe61.png)

先頭の6個だけアクティブ状態。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528566/9e462a52-0b14-9722-1a0e-594694bebb07.png)


# まとめ
- Instantiateが重いと感じる場合、どういう負荷がかかっているかよく調べたほうがいい。
  - 実は思いもよらぬところで負荷がかかっている可能性がある。
- Instantiate単体だけでもそこそこ重い
  - Instantiateが本当に重いですか？という指摘の記事で軽いという結論を出したいわけではないので念の為の補足。
- イベント関数の実行順は改めて理解を深められてよかった。
  - Instantiate後の挙動はどこにも書かれてない(と思う)のでしっかり把握するのは大切だなと思った。
  - 今回Editorでの計測で実機での順序は確認できていません。
- 今回具体的な対策のための記事ではないということ。
  - こういう問題が起きているかもしれないので調べる、どうしたら解決できるのかは改めて各々で考えてもらえたらなと思います。

# 最後に
Instantiateが重いという話は調べると出てくるが何故重いのか、という取り上げ方の記事はあまり見かけないなと思っています。
だからこそ上げてみようと思いましたが、実はこういう理由なのでは、こういうのもあるよということがあればコメントでご指摘いただければと思います。

# おまけ
``` test.cs
    IEnumerator Start()
    {
        yield return null;
        Debug.Log($"Start {Time.frameCount}");
    }
    void Update()
    {
        if (Time.frameCount == 2) {
            Debug.Log($"Update {Time.frameCount}");
        }
    }
```

結果…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528566/177e0813-5ac9-5143-a9f1-7a74d2858f2d.png)

イベント関数の処理順を見てみると…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528566/57f36056-015e-288c-0b6f-00bffe675556.png)
上記になっているのでそりゃそうかという。