2018年12月7日金曜日

[C#, Unity]Coroutineを自前で実行する

C#のCoroutineへの理解を深めましょう。
UnityでMonoBehaviourが無くてもCoroutineを実行できる1ようになったり、Coroutineの拡張をできるようになったりできます。

Coroutine is 何

C#のCoroutineは「処理を一旦止めて途中から処理を再開できる」ブロック2です。
C#ではyieldというキーワードを用いて、メソッドやプロパティをCoroutineにできます。
Coroutineブロックの戻り値はIEnumeratorまたはIEnumerableでなければなりません。
このCoroutineを1回呼び出すには、IEnumerator.MoveNext()を使います。

EnumeratorCoroutineSample.cs
//  IEnumeratorを返すコルーチンメソッド
public IEnumerator<int> SomeCoroutine()
{
    yield return 1;
    //  最初の呼び出しではここまで実行される

    yield return 2;
    //  次の呼び出しではここまで

    yield break;
    //  これ以降は処理しない

    yield return 3;
}

public void Hoge()
{
    IEnumerator<int> coroutine = SomeCoroutine();

    //  1回目の呼び出し
    coroutine.MoveNext();
    Assert.AreEqual(1, coroutine.Current);  //  pass

    //  2回目の呼び出し
    coroutine.MoveNext();
    Assert.AreEqual(2, coroutine.Current);  //  pass
}
EnumerableCoroutineSample.cs
//  IEnumerableを返すコルーチンメソッド
public IEnumerable<int> SomeCoroutine()
{
    yield return 1;
    //  最初の呼び出しではここまで実行される

    yield return 2;
    //  次の呼び出しではここまで

    yield break;
    //  これ以降は処理しない

    yield return 3;
}

public void Hoge()
{
    IEnumerator<int> coroutine = SomeCoroutine().GetEnumerator();

    //  1回目の呼び出し
    coroutine.MoveNext();
    Assert.AreEqual(1, coroutine.Current);  //  pass

    //  2回目の呼び出し
    coroutine.MoveNext();
    Assert.AreEqual(2, coroutine.Current);  //  pass
}

yieldキーワードを用いたブロックはIEnumeratorを実装したクラスにコンパイルされます。
yieldはIEnumeratorを簡単に実装するための構文というわけですね。

Coroutineを最後まで実行する

OnNextがfalseになるまで実行すれば良いです。

CoroutineSample.cs
//  IEnumeratorを返すコルーチンメソッド
public IEnumerator<int> SomeEnumeratorCoroutine()
{
    yield return 1;
    //  最初の呼び出しではここまで実行される

    yield return 2;
    //  次の呼び出しではここまで

    yield break;
    //  これ以降は処理しない

    yield return 3;
}
//  IEnumerableを返すコルーチンメソッド
public IEnumerable<int> SomeEnumerableCoroutine()
{
    yield return 1;
    //  最初の呼び出しではここまで実行される

    yield return 2;
    //  次の呼び出しではここまで

    yield break;
    //  これ以降は処理しない

    yield return 3;
}

public void Hoge()
{
    // Enumeratorを最後まで実行
    {
        int count = 0;
        IEnumerator<int> e = SomeEnumeratorCoroutine();
        while (e.MoveNext())
        {
            count++;
        }
        Assert.AreEqual(2, count);  //  pass
    }

    // Enumerableを最後まで実行
    {
        int count = 0;
        IEnumerator<int> e = SomeEnumerableCoroutine().GetEnumerator();
        while (e.MoveNext())
        {
            count++;
        }
        Assert.AreEqual(2, count);  //  pass
    }

    // Enumerableはこれでもいい(コンパイラが↑の形に展開する)
    {
        int count = 0;
        foreach(int _ in SomeEnumerableCoroutine())
        {
            count ++;
        }
        Assert.AreEqual(2, count);  //  pass
    }
}

UnityのCoroutine

UnityではCoroutineをフレームを跨いだ継続処理のために利用します。
UnityのComponentモデルはシングルスレッドなのでUI処理を止めずにフレームを跨いだ処理を実行するには
処理を途中で止める必要があり、これを実装するのはCoroutineを使うのが簡単なわけです。

UnityのMonoBehaviour.StartCoroutine()は毎フレームMoveNext()を呼び出す実装です。
また、コルーチンの中でIEnumeratorをreturnすると、コルーチンをスタックして中のIEnumeratorのMoveNext()を呼び出すような仕組みになっていることでしょう。
StartCoroutineを自前で簡単に実装するならこのような形になるのではないでしょうか。実際にはStopCoroutineやら例外やらAsyncOperationやら何やらがあるのでもっと複雑でしょうけどね。

MyStartCoroutine.cs
public void StartCoroutineSync(IEnumerator coroutine)
{
    Coroutine c = new Coroutine(coroutine);
    while (c.MoveNext()) { }
}


public class CoroutineRunner : MonoBehaviour {
    public static List<Coroutine> Coroutines { get; private set; } = new List<Coroutine>();

    public void Update()
    {
        Coroutines.ForEach(c => c.MoveNext());
        Coroutines.RemoveAll(c => !c.HasNext);
    }

    public void StartCoroutine(IEnumerator coroutine)
    {
        Coroutine c = new Coroutine(coroutine);
        Coroutines.Add(c);
    }
}

public class Coroutine : IEnumerator {

    private Stack<IEnumerator> coroutineStack = new Stack<IEnumerator>();

    public Coroutine(IEnumerator coroutine)
    {
        coroutineStack.Push(coroutine);
    }

    public object Current { get; private set; }

    public bool HasNext { get; private set; } = true;

    public bool MoveNext()
    {
        while (coroutineStack.Count > 0)
        {
            IEnumerator peek = coroutineStack.Peek();

            bool hasNext = peek.MoveNext();

            if (!hasNext)
            {
                coroutineStack.Pop();
                continue;
            }

            if (peek.Current is IEnumerator)
            {
                coroutineStack.Push((IEnumerator)peek.Current);
                continue;
            }
            else
            {
                Current = peek.Current;
                HasNext = true;
                return true;
            }
        }

        HasNext = false;
        return false;
    }

    public void Reset()
    {
        throw new NotImplementedException();
    }
}

これでMonoBehaviour.StartCoroutine()が無くてもコルーチンの実行ができますね。

  1. CoroutineのテストがPlayModeでなくてもできる!

  2. iterator blockと呼びます。

0 コメント:

コメントを投稿