複数のIEnumratorを時間管理しつつ処理するCoroutine


作ったもの

TaskScheduler

作ろうと思ったきっかけ

ステージクリア型のパズルゲームのステージセレクト画面で縮小したマップを一気に3x4の12個動的に読み込みつつ、表示。みたいなことをStartCoroutineでやっていたらFPSがガタ落ちでUIの反応が悪くなってしまったので。

例えば

あるGameObjectはあるPrefabを10個Instantiateして子要素として付けておく必要があるとする。
しかしUnityのInstantiateは重い。 なので、StartCoroutineを使って、yield return nullしながら1個づつInstantiateすることにした。(よくある)

public class HogeObject : MonoBehaviour
{
    public GameObject prefab;
    public void Start()
    {
        StartCoroutine(CreateIterator());
    }

    private IEnumrator CreateIterator()
    {
        for(int i = 0;i < 10;++i)
        {
            var go = Instantiate(prefab) as GameObject;//heavy work
            go.transform.SetParent(this.transform,false);
            yield return null;
        }   
    }
}

しかし、このHogeObjectは画面上に100個あった!(よくある(?))
すると、StartCoroutineが100回呼ばれ、1フレームに100個Instantiateされることになってプチフリーズのような状態に。

何が悪いかって

UnityのStartCoroutineの仕組みには横の繋がりがなく、独立して動いているので、何個StartCoroutineされてようが知ったこっちゃないということ。

なので

冒頭に戻る。 自前StartCoroutineの作成。
上記例はこうなる。

public class HogeObject : MonoBehaviour
{
    public GameObject prefab;
    public void Start()
    {
        //StartCoroutine(CreateIterator());//この1行が
        TaskScheduler.Instance.AddIterator(CreateIterator());//これになる
    }

    private IEnumrator CreateIterator()
    {
        for(int i = 0;i < 10;++i)
        {
            var go = Instantiate(prefab) as GameObject;//heavy work
            go.transform.SetParent(this.transform,false);
            yield return null;
        }   
    }
}

StartCoroutineだった箇所をTaskScheduler.Instance.AddIteratorに変える事で、TaskScheduler(Singletone)で1本だけ走っているStartCoroutineが時間を計測しつつよしなにCreateIteratorを処理するので、HogeObject が100個あっても1000個あっても処理落ちはしない(はず)。

仕組み

  • UnityのStartCoroutineIEnumeratorを渡して実行しているが、仕組みとしてはIEnumerator#MoveNextを呼ぶ事で処理を中断→継続させている。このMoveNextを呼ぶ処理を自分で作れば自前StartCoroutineみたいな事が出来る。
  • そこで、複数のIEnumratorを受け取って、順次MoveNextを呼んでいく+処理時間が一定時間を超えていたらyield return null;するIEnumratorStartCoroutineしておく(何言ってんだ)
  • すなわち、UnityのStartCoroutineの拡張をUnityのStartCoroutineを使うことで実装した感じ(何言ってんだその2)。
        public void Start()
        {
            StartCoroutine(Iterator());
        }
        private IEnumerator Iterator()
        {
            while (true)
            {
                var ts = Time.realtimeSinceStartup;
                do
                {
                    if (iteratorList.Count <= 0) break;
                    foreach (var itr in iteratorList.ToList())
                    {
                        if (itr.Value.MoveNext() == false)
                        {
                            iteratorList.Remove(itr.Key);
                        }
                        if (Time.realtimeSinceStartup - ts > targetTime) yield return null;
                    }
                } while (Time.realtimeSinceStartup - ts <= targetTime);
                yield return null;
            }
        }

補足

  • Action 渡せるものもついでに作ったので、TaskScheduler.Instance.AddAction(()=>Instantiate(hogeObj));のようにラムダ式渡したりなんかも。
  • yield return new WaitForSeconds(ms); などには未対応。 というか、処理の負荷分散目的であって、順次処理目的でのStartCoroutineは普通にStartCoroutineでやってくれれば良いのでは。
  • WWW www;で yield return www; とかは価値があるけど未対応。 while(www.isDone == false){yield return null;}にすれば使えると思う(試してない)
  • 基本は独立のStartCoroutineに横のつながりをもたせたなら、Priorityの概念を入れるのが筋だとは思うが未実装。必要に駆られたら作ります。