[Unity] InstantiateAsync を使用する

### はじめに
```InstantiateAsync```を使用して、大量のGameObjectをInstantiateする際のスパイクを緩和してみました。

<iframe width="560" height="315" src="https://www.youtube.com/embed/kmQRuV5E5PE?si=ALKqaMvFdj91Hnsd" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

### サンプルコード
3DObject>Cubeにアタッチしてください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59911/3d9add79-fc4c-16ad-2107-6c4a27c2d741.png)
<details><summary>サンプルコードを開く</summary>

```InstantiateAsyncCopy.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

namespace AsyncDemo
{
    public class InstantiateAsyncCopy : MonoBehaviour
    {
        static public int maxCount;

        [SerializeField, Range(0, 5)] int m_maxDepth = 3;
        [SerializeField] bool m_useAsync = true;
        [SerializeField] bool m_immediately = false;
        [SerializeField] TMPro.TMP_Text m_text;
        [SerializeField] Material[] m_materials;
        int m_myDepth = 0;

        //int myDepth { get { return m_myDepth; } set { m_myDepth = value; } }
        Vector3[] m_positions;
        GameObject m_baseObj;

        // Start is called once before the first execution of Update after the MonoBehaviour is created
        async void Start()
        {
            if(m_myDepth == 0)
            {
                m_baseObj = Instantiate(gameObject);
                m_baseObj.SetActive(false);
                List<Vector3> posList = new List<Vector3>();
                posList.Add(new Vector3(1, 0, 0));
                posList.Add(new Vector3(-1, 0, 0));
                posList.Add(new Vector3(0, 1, 0));
                posList.Add(new Vector3(0, -1, 0));
                posList.Add(new Vector3(0, 0, 1));
                posList.Add(new Vector3(0, 0, -1));
                m_positions = posList.ToArray();

                maxCount = 0;
            }

            maxCount++;
            int sum = (int)((Mathf.Pow(6, m_maxDepth + 1) - 1) / 5);
            string resultTxt = $"Async: {m_useAsync},   Count: {maxCount}/{sum}";

            if (m_text != null)
            {
                m_text.text = resultTxt;
            }
            else
            {
                Debug.Log(resultTxt);
            }
            if (m_myDepth < m_maxDepth)
            {
                await Awaitable.WaitForSecondsAsync(2.0f);
                createCube(m_myDepth + 1);
            }
        }

        // Update is called once per frame
        void Update()
        {
            transform.Rotate(new Vector3(1f,1.3f,1.7f) * (m_myDepth+1) * Time.deltaTime);
        }

        public void Prepare(int _idx, int _depth, GameObject baseObj, Vector3[] _positions)
        {
            m_myDepth = _depth;
            m_baseObj = baseObj;
            m_positions = _positions;
            if(m_materials.Length > 0)
            {
                gameObject.GetComponent<MeshRenderer>().material = m_materials[_depth % m_materials.Length];
            }
            transform.localScale = Vector3.zero;
            transform.localPosition = m_positions[_idx] * 3f;
            gameObject.SetActive(true);
            StartCoroutine(appearCo(_idx));
        }

        IEnumerator appearCo(int _idx)
        {
            float timer = m_immediately ? 1 : 0;
            while (timer < 1)
            {
                timer += Time.deltaTime;
                transform.localScale = Vector3.one * timer * 0.5f;
                yield return null;
            }
            transform.localScale = Vector3.one * 0.5f;
        }

        async void createCube(int _depth)
        {
            Quaternion rot = transform.rotation * Quaternion.Euler(11,21,31);
            Vector3[] posArr = new Vector3[m_positions.Length];
            Quaternion[] rotArr = new Quaternion[m_positions.Length];
            for (int i = 0; i < m_positions.Length; i++)
            {
                posArr[i] = Vector3.zero;
                rotArr[i] = rot;
            }
            GameObject[] objArr;
            if (m_useAsync)
            {
                objArr = await InstantiateAsync(m_baseObj, m_positions.Length, transform, posArr, rotArr);
            }
            else
            {
                List<GameObject> objList = new List<GameObject>();
                for (int i = 0; i < m_positions.Length; i++)
                {
                    objList.Add(Instantiate(m_baseObj, posArr[i], rotArr[i], transform));
                }
                objArr = objList.ToArray();
            }
            for (int i = 0; i < objArr.Length; i++)
            {
                InstantiateAsyncCopy cubeScr = objArr[i].GetComponent<InstantiateAsyncCopy>();
                cubeScr?.Prepare(i, _depth, m_baseObj, m_positions);
            }
        }
    }
}
```

</details>

<iframe width="560" height="315" src="https://www.youtube.com/embed/QwVOSGcyZjw?si=2yqXtrAUllOPbp8P" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

### 使用方法

[InstantiateAsync](https://docs.unity3d.com/ScriptReference/Object.InstantiateAsync.html)

```GameObject[] objArr = await InstantiateAsync();```の```objArr```にInstantiateしたgameObjectが返ってくるので、追加で設定したい場合はここで行います。

### 重たいPrefabをInstantiateAsyncする
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59911/ff5ce493-d901-99b1-f791-15c2fdfb00d6.png)
合計216個のGameObjectからなる重たいPrefabを２００個InstantiateAsyncしてみたところ、スパイクが発生しました。
重たいPrefabをInstantiateAsyncした場合は効果が薄いようです。

```
await InstantiateAsync(m_prefab, m_numInstantiatePerLoop, transform, vector3s, rotArray);
```

ちなみに１個だけInstantiateAsyncした場合ではスパイクは発生しません。
そこで、同数のprefabをループの中で分割してInstantiateAsyncしてみると、複数のワーカースレッドで並列処理されるようになり、スパイクが軽減しました。
[Youtube-UnityJapan](https://www.youtube.com/shorts/Q9K-3zkEijQ)
```
for (int i = 0; i < m_numLoopInstantiate; i++)
{
  await InstantiateAsync(m_prefab, m_numInstantiatePerLoop, transform, vector3s, rotArray);
}
```
<details><summary>サンプルコードを開く</summary>

```InstantiateAsyncSimple.cs
using System.Collections.Generic;
using UnityEngine;

namespace AsyncDemo
{
    [HelpURL("https://docs.unity3d.com/ScriptReference/Object.InstantiateAsync.html")]
    public class InstantiateAsyncSimple : MonoBehaviour
    {
        [SerializeField] GameObject m_prefab = null;
        [SerializeField, Range(1, 500)] int m_numInstantiatePerLoop = 5;
        [SerializeField, Range(1, 100)] int m_numLoopInstantiate = 5;
        [SerializeField, Range(0, 10)] float m_randRadius = 3f;
        [SerializeField] bool m_useAsync = true;
        [SerializeField] TMPro.TMP_Text m_text;
        int m_numInstantiate;
        List<GameObject> m_gameObjList;

        // Start is called once before the first execution of Update after the MonoBehaviour is created
        async void Start()
        {
            List<Vector3> posList = new List<Vector3>();
            List<Quaternion> rotList = new List<Quaternion>();
            m_gameObjList = new List<GameObject>();
            m_numInstantiate = m_numInstantiate * m_numInstantiatePerLoop;

            //https://www.youtube.com/shorts/Q9K-3zkEijQ
            AsyncInstantiateOperation.SetIntegrationTimeMS(10);

            for (int i = 0; i < m_numLoopInstantiate; i++)
            {
                for (int j = 0; j < m_numInstantiatePerLoop; j++)
                {
                    Vector3 pos = Random.insideUnitSphere * m_randRadius;
                    posList.Add(pos);
                }
                Quaternion rot = Random.rotation;
                rotList.Add(rot);
            }

            await Awaitable.WaitForSecondsAsync(3.0f);
            if (m_useAsync)
            {
                Quaternion[] rotArray = rotList.ToArray();
                for (int i = 0; i < m_numLoopInstantiate; i++)
                {
                    Vector3[] vector3s = posList.GetRange(i * m_numInstantiatePerLoop, m_numInstantiatePerLoop).ToArray();
                    GameObject[] goArr = await InstantiateAsync(m_prefab, m_numInstantiatePerLoop, transform, vector3s, rotArray);
                    m_gameObjList.AddRange(goArr);
                }
            }
            else
            {
                for(int i = 0; i < m_numLoopInstantiate; i++)
                {
                    for (int j = 0; j < m_numInstantiatePerLoop; j++)
                    {
                        int idx = i * m_numInstantiatePerLoop + j;
                        GameObject go = Instantiate(m_prefab, posList[idx], rotList[idx% m_numLoopInstantiate], transform);
                        m_gameObjList.Add(go);
                    }
                }
            }
        }

        // Update is called once per frame
        void Update()
        {
            Vector3 rotVec = new Vector3(1, 2, 3) * Time.deltaTime;
            transform.Rotate(rotVec);
            if (m_text != null)
            {
                m_text.text = $"Async: {m_useAsync} / {m_gameObjList.Count}";
            }
        }
    }
}
```

</details>

### 既知の問題点

本記事作成時点で最新のバージョン```6000.0.25f1```では
```InstantiateAsync```中にエディターを停止するとエラーが表示され、再度実行するとクラッシュするようです（報告済：IN-88119）。
追記：6000.0.30f1で修正されているのを確認
<iframe width="560" height="315" src="https://www.youtube.com/embed/rrSHF-4yktI?si=4gfbfu0CJt11ZAGd" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

以上