LoginSignup
2
2

More than 5 years have passed since last update.

UnityのCoroutineを自作する

Posted at

基本の実装

UnityにはStartCoroutineという関数があります。
これはIEnumeratorを受け取って、MoveNext()を毎フレーム呼び出すようになっています。
これと同じような仕組みを自作します。

public class CoroutineEx {
    IEnumerator routine;

    public bool IsDone { get; private set; }

    // コンストラクタ
    public CoroutineEx(IEnumerator routine) {
        this.routine = routine;
    }

    // 更新処理
    public void Update() {
        IsDone = !routine.MoveNext();
    }
}

基本の実装は上記で完了します。
引数として実行したいIEnumeratorを受け取り、毎フレームUpdate関数が呼び出されるものとします。

実行が完了したかどうかをIsDoneに設定して確認できるようにしています。

実装の拡張

もう少し便利に利用するため以下の仕様を追加します。

  • MoveNext処理でエラーが発生した場合、エラーを取得できるようにする。
  • yield returnで別のIEnumeratorが返されたとき、それを待機するようにする。
  • AsyncOperationが返されたとき、isDoneがtrueになるまで待機する。
  • その他のオブジェクトが返されたとき、そのオブジェクトを取得できるようにする。
public class CoroutineEx {
    public Stack<object> stack = new Stack<object>();

    public object Current { get; private set; }

    public Exception Error { get; private set; }

    public bool IsDone { get; private set; }

    // コンストラクタ
    public CoroutineEx(IEnumerator routine) {
        stack.Push(routine);
    }
}
  • 発生したエラーを取得できるようにするため、Errorプロパティを定義。
  • 複数のIEnumeratorを待機することを可能にするため、routineはStackで持つように変更。
  • MoveNextで返された最後のオブジェクトを取得するため、Currentプロパティを定義。

更新処理の拡張

更新処理では、routineの型によって処理を分岐します。

public void Update() {
    if (stack.Count == 0) {
        Done();
        return;
    }

    var peek = stack.Peek();
    if (peek == null) {
        stack.Pop();
        Current = null;
    }
    else if (peek is IEnumerator) {
        var e = (IEnumerator)peek;
        try {
            if (e.MoveNext()) {
                stack.Push(e.Current);
                Update();
            } else {
                stack.Pop();
                Update();
            }
        }
        catch (Exception error) {
            stack.Clear();
            Error = error;
            Done();
        }
    }
    else if (peek is AsyncOperation) {
        if (!((AsyncOperation)peek).isDone) {
            Current = null;
        } else {
            stack.Pop();
            Update();
        }
    }
    else {
        stack.Pop();
        Current = peek;
    }
}

void Done() {
    Current = null;
    IsDone = true;
}

stackが空になっている場合は処理が完了しているので、IsDoneをtrueにします。

peek == null

IEnumeratorをコルーチンとして利用するとき、yield return nullによって
頻繁にnullを受け取るので、nullの場合の分岐を先頭に入れています。

peek is IEnumerator

IEnumeratorの場合はエラーハンドリングをしつつMoveNextを呼び出します。

yield returnで返された値がCurrentから取得できるので、stackにPushしてUpdateを再帰呼び出しします。

IEnumeratorが完了していた場合はstackから取り除き、再帰呼び出しします。
この際stackが空になっていればDoneが呼び出されて処理が完了します。

peek is AsyncOperation

AsyncOperationを受け取った場合はstackの頭にAsyncOperationを残しつつ、
Currentにはnullを設定して処理を継続させます。

完了していればstackから取り除き、Updateを再帰します。

else

その他のオブジェクトの場合、Currentに設定して終わりです。

IEnumeratorインターフェースの実装

CoroutineExをUnityのStartCoroutineに渡しても動作するように、IEnumeratorインターフェースを実装します。

public class CoroutineEx : IEnumerator {
    bool IEnumerator.MoveNext() {
        Update();
        return !IsDone;
    }

    void IEnumerator.Reset() { }
}

IEnumeratorインターフェースのうち、Currentは実装済みのため省略します。

MoveNextは毎フレーム呼び出される処理なので、Updateを呼び出し、
処理を継続するかどうかのboolとして!IsDoneを返します。

Resetはコルーチンとしては利用しない関数のため、実装は空です。

実装のまとめ

ここまでの実装により完成したコードを貼ります。

public class CoroutineEx : IEnumerator {
    public Stack<object> stack = new Stack<object>();

    public object Current { get; private set; }

    public Exception Error { get; private set; }

    public bool IsDone { get; private set; }

    // コンストラクタ
    public CoroutineEx(IEnumerator routine) {
        stack.Push(routine);
    }

    // 更新処理
    public void Update() {
        if (stack.Count == 0) {
            Done();
            return;
        }

        var peek = stack.Peek();
        if (peek == null) {
            stack.Pop();
            Current = null;
        }
        else if (peek is IEnumerator) {
            var e = (IEnumerator)peek;
            try {
                if (e.MoveNext()) {
                    stack.Push(e.Current);
                    Update();
                } else {
                    stack.Pop();
                    Update();
                }
            }
            catch (Exception error) {
                stack.Clear();
                Error = error;
                Done();
            }
        }
        else if (peek is AsyncOperation) {
            if (!((AsyncOperation)peek).isDone) {
                Current = null;
            } else {
                stack.Pop();
                Update();
            }
        }
        else {
            stack.Pop();
            Current = peek;
        }
    }

    void Done() {
        Current = null;
        IsDone = true;
    }

    bool IEnumerator.MoveNext() {
        Update();
        return !IsDone;
    }

    void IEnumerator.Reset() { }
}

上記実装に加えて

  • 処理を中断する関数を実装したり
  • 完了やエラーのコールバックを実装したり
  • 完了後に継続して行う処理を追加できるようにしたり

など、Unity標準のCoroutineではできなかった処理が
拡張次第で書けるようになります。

そもそも標準のCoroutineが何の変数も関数もないオブジェクトで
持ってても意味無いので、こういう拡張クラスがあると便利です。

割と勢いで書いたので編集リクエスト歓迎です。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2