作ったもの
作ろうと思ったきっかけ
ステージクリア型のパズルゲームのステージセレクト画面で縮小したマップを一気に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の
StartCoroutineはIEnumeratorを渡して実行しているが、仕組みとしてはIEnumerator#MoveNextを呼ぶ事で処理を中断→継続させている。このMoveNextを呼ぶ処理を自分で作れば自前StartCoroutineみたいな事が出来る。 - そこで、複数の
IEnumratorを受け取って、順次MoveNextを呼んでいく+処理時間が一定時間を超えていたらyield return null;するIEnumratorをStartCoroutineしておく(何言ってんだ) - すなわち、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の概念を入れるのが筋だとは思うが未実装。必要に駆られたら作ります。