Unity

CoroutineSequence - Unityのコルーチンを組み合わせて実行する

More than 1 year has passed since last update.


概要

Unityにおいて、コルーチンは、複数フレームに渡るような処理を分かりやすく記述することができる仕組みである。

Unity - マニュアル: コルーチン

コルーチンは、処理Aのあとに処理Bを行うというような直列処理のみの記述には便利だが、処理Aと処理Bを同時に行ったあと処理Cを行うというような、並列処理と直列処理が入り交じった処理の記述には一手間必要になる。

この記述を簡略化するため、CoroutineSequenceというクラスを作成した。

CoroutineSequenceによって、前述のような複雑な処理も、直感的で簡潔に記述することができる。

この記事の最後のほうにCoroutineSequenceクラスの全文を記載している。


複雑なコルーチン

例えば、以下の二つのメソッドを用意する。


二つのコルーチン用メソッド

    IEnumerator Test1Enumerator()

{
yield return new WaitForSeconds(1);
Debug.Log("Test1");
}

IEnumerator Test2Enumerator()
{
yield return new WaitForSeconds(2);
Debug.Log("Test2");
}


Test1EnumeratorTest2Enumeratorを単純に同時に実行したいときは、


同時実行

    void Test()

{
StartCoroutine(Test1Enumerator());
StartCoroutine(Test2Enumerator());
}

と記述すれば実現できる。

一方、Test1EnumeratorTest2Enumeratorの実行がどちらも終了した後、別の処理を行いたい場合はどうだろう。

色々な方法があると思うが、Test1EnumeratorTest2Enumeratorをいじらないとすれば、以下のようなコードが考えられる。


同時実行した後に別の処理

    void Test()

{
StartCoroutine(ParallelEnumerator());
}

IEnumerator ParallelEnumerator()
{
int counter = 0;
StartCoroutine(HelperEnumerator(Test1Enumerator(), () => counter++));
StartCoroutine(HelperEnumerator(Test2Enumerator(), () => counter++));
yield return new WaitUntil(() => counter == 2);
Debug.Log("Finish!");
}

IEnumerator HelperEnumerator(IEnumerator enumerator, Action callback)
{
yield return enumerator;
callback();
}


上のコードでは、Test1EnumeratorTest2Enumeratorの実行が終了した後、Debug.Log("Finish!")が実行される。

これを実現するために、ParallelEnumeratorHelperEnumeratorという二つのコルーチン用メソッドを追加した。

HelperEnumeratorによって、コルーチンが終了した後コールバックが呼ばれる仕組みを作り、ParallelEnumeratorで二つのコルーチンが終了するのをWaitUntilで監視するという仕組みである。

このように、複数のコルーチンを組み合わせるためには、新たにコルーチン用のメソッドを作る必要があり、コードが煩雑になりやすいという問題がある。


CoroutineSequence

複雑なコルーチンの組み合わせを簡潔に記述することが出来るようにするため、CoroutineSequenceというクラスを作成した。

このクラスを使うと、複数のコルーチンを並列で組み合わせたり、直列で組み合わせたり、特定のタイミングでコールバックを実行したり、といったことを簡潔に記述することが可能になる。


DOTweenのSequenceクラス

Unityで使えるTweenエンジンの一つにDOTweenがある。

DOTweenでは、複数のTweenを組み合わせるために、Sequenceというクラスを用意している。

CoroutineSequenceは、DOTweenのSequenceを参考にして設計しており、DOTweenに馴染みのある人なら利用しやすいものになっているはずである。


機能一覧


コンストラクタ

CoroutineSequenceのコンストラクタには、MonoBehaviourを指定する。

ここで渡すMonoBehaviour(がアタッチされたGameObject)がコルーチンを動かすことになる。

コルーチンは、誰がそれを動かしているかというのが非常に重要で、コルーチンを動かしているGameObjectが非アクティブになったり、Destroyされたりすると、そのコルーチンも止まるという仕様になっている。


Insert系

Insert系のメソッドは、時刻を指定してコルーチン等を実行するのに用いる。

メソッド
説明

Insert(float atPosition, IEnumerator enumerator)
atPosition秒後にIEnumerator(コルーチン)を実行する

Insert(float atPosition, CoroutineSequence sequence)
atPosition秒後にCoroutineSequenceを入れ子にして実行する

InsertCallback(float atPosition, Action callback)
atPosition秒後にコールバックを実行する


Append系

Append系のメソッドは、直列にコルーチン等を実行するのに用いる。

AをAppendした後にBをAppendすれば、Aを実行した後Bを実行させることができる。

注意点として、Appendされた処理は、Insertされた処理が全て終了した後に実行される。

メソッド
説明

Append(IEnumerator enumerator)
IEnumerator(コルーチン)を直列実行する

Append(CoroutineSequence sequence)
CoroutineSequenceを入れ子にして直列実行する

AppendCallback(Action callback)
コールバックを直列実行する

AppendInterval(float seconds)
待ち時間を加える(seconds秒何もしない)


OnCompleted

AppendCallbackでも記述可能だが、特別に全ての処理が終わった後に実行する処理をOnCompletedメソッドて追加できる。


Play

実行する。

逆に言えばこのメソッドを呼ばなければ何も動かない。

返り値はCoroutine型なので、これ自体をyield returnすることも可能。


Stop

実行を止める。

入れ子のコルーチンに対するStopCoroutineというのはなかなか曲者で、基本的に入れ子のコルーチンは一つのStopCoroutineでは止まらない。

CoroutineSequenceでは、出来る限り入れ子のコルーチンの実行も止めるため、内部のコルーチンやIEnumeratorを管理して全てに対してStopCoroutineする。

Stopしたあとに再びPlayすることは想定していない。


先程ParallelEnumeratorを使って実現した処理は、以下のコードで実現可能である。


CoroutineSequence利用例

    void Test()

{
var sequence = new CoroutineSequence(this);
sequence.Insert(0f, Test1Enumerator());
sequence.Insert(0f, Test2Enumerator());
sequence.OnCompleted(() => Debug.Log("Finish!"));
sequence.Play();
}

Insert系とAppend系とOnCompletedはthisを返すので、以下のようにメソッドチェーン風に記述することもできる。


CoroutineSequence利用例(メソッドチェーン)

    void Test()

{
new CoroutineSequence(this)
.Insert(0f, Test1Enumerator())
.Insert(0f, Test2Enumerator())
.OnCompleted(() => Debug.Log("Finish!"))
.Play();
}

Aを実行した後、BとCを並列で実行し、その後Cを実行する、なら、CoroutineSequenceを入れ子にして、以下のように記述可能である。


CoroutineSequence利用例(入れ子のCoroutineSequence)

    void Test()

{
new CoroutineSequence(this)
.Append(A())
.Append(new CoroutineSequence(this)
.Insert(0f, B())
.Insert(0f, C()))
.Append(D())
.Play();
}


CoroutineSequenceのコード全文


CoroutineSequence.cs

/*

* CoroutineSequence.cs
*
* Copyright (c) 2016 Kazunori Tamura
* This software is released under the MIT License.
* http://opensource.org/licenses/mit-license.php
*/

using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;

/// <summary>
/// コルーチンを組み合わせて実行するためのSequenceクラス
/// </summary>
public class CoroutineSequence
{
/// <summary>
/// Insertで追加されたEnumeratorを管理するクラス
/// </summary>
private class InsertedEnumerator
{
/// <summary>
/// 位置
/// </summary>
private float _atPosition;

/// <summary>
/// 内部のIEnumerator
/// </summary>
public IEnumerator InternalEnumerator { get; private set; }

/// <summary>
/// コンストラクタ
/// </summary>
public InsertedEnumerator(float atPosition, IEnumerator enumerator)
{
_atPosition = atPosition;
InternalEnumerator = enumerator;
}

/// <summary>
/// Enumeratorの取得
/// </summary>
public IEnumerator GetEnumerator(Action callback)
{
if (_atPosition > 0f)
{
yield return new WaitForSeconds(_atPosition);
}
yield return InternalEnumerator;
if (callback != null)
{
callback();
}
}
}

/// <summary>
/// Insertされたenumerator
/// </summary>
private List<InsertedEnumerator> _insertedEnumerators;

/// <summary>
/// Appendされたenumerator
/// </summary>
private List<IEnumerator> _appendedEnumerators;

/// <summary>
/// 終了時に実行するAction
/// </summary>
private Action _onCompleted;

/// <summary>
/// コルーチンの実行者
/// </summary>
private MonoBehaviour _owner;

/// <summary>
/// 内部で実行されたコルーチンのリスト
/// </summary>
private List<Coroutine> _coroutines;

/// <summary>
/// 追加されたCoroutineSequenceのリスト
/// </summary>
private List<CoroutineSequence> _sequences;

/// <summary>
/// コンストラクタ
/// </summary>
public CoroutineSequence(MonoBehaviour owner)
{
_owner = owner;
_insertedEnumerators = new List<InsertedEnumerator>();
_appendedEnumerators = new List<IEnumerator>();
_coroutines = new List<Coroutine>();
_sequences = new List<CoroutineSequence>();
}

/// <summary>
/// enumeratorをatPositionにInsertする
/// atPosition秒後にenumeratorが実行される
/// </summary>
public CoroutineSequence Insert(float atPosition, IEnumerator enumerator)
{
_insertedEnumerators.Add(new InsertedEnumerator(atPosition, enumerator));
return this;
}

/// <summary>
/// CoroutineSequenceをatPositionにInsertする
/// </summary>
public CoroutineSequence Insert(float atPosition, CoroutineSequence sequence)
{
_insertedEnumerators.Add(new InsertedEnumerator(atPosition, sequence.GetEnumerator()));
_sequences.Add(sequence);
return this;
}

/// <summary>
/// callbackをatPositionにInsertする
/// </summary>
public CoroutineSequence InsertCallback(float atPosition, Action callback)
{
_insertedEnumerators.Add(new InsertedEnumerator(atPosition, GetCallbackEnumerator(callback)));
return this;
}

/// <summary>
/// enumeratorをAppendする
/// Appendされたenumeratorは、Insertされたenumeratorが全て実行された後に順番に実行される
/// </summary>
public CoroutineSequence Append(IEnumerator enumerator)
{
_appendedEnumerators.Add(enumerator);
return this;
}

/// <summary>
/// CoroutineSequenceをAppendする
/// </summary>
public CoroutineSequence Append(CoroutineSequence sequence)
{
_appendedEnumerators.Add(sequence.GetEnumerator());
_sequences.Add(sequence);
return this;
}

/// <summary>
/// callbackをAppendする
/// </summary>
public CoroutineSequence AppendCallback(Action callback)
{
_appendedEnumerators.Add(GetCallbackEnumerator(callback));
return this;
}

/// <summary>
/// 待機をAppendする
/// </summary>
public CoroutineSequence AppendInterval(float seconds)
{
_appendedEnumerators.Add(GetWaitForSecondsEnumerator(seconds));
return this;
}

/// <summary>
/// 終了時の処理を追加する
/// </summary>
public CoroutineSequence OnCompleted(Action action)
{
_onCompleted += action;
return this;
}

/// <summary>
/// シーケンスを実行する
/// </summary>
public Coroutine Play()
{
Coroutine coroutine = _owner.StartCoroutine(GetEnumerator());
_coroutines.Add(coroutine);
return coroutine;
}

/// <summary>
/// シーケンスを止める
/// </summary>
public void Stop()
{
foreach (Coroutine coroutine in _coroutines)
{
_owner.StopCoroutine(coroutine);
}
foreach (InsertedEnumerator insertedEnumerator in _insertedEnumerators)
{
_owner.StopCoroutine(insertedEnumerator.InternalEnumerator);
}
foreach (IEnumerator enumerator in _appendedEnumerators)
{
_owner.StopCoroutine(enumerator);
}
foreach (CoroutineSequence sequence in _sequences)
{
sequence.Stop();
}
_coroutines.Clear();
_insertedEnumerators.Clear();
_appendedEnumerators.Clear();
_sequences.Clear();
}

/// <summary>
/// callbackを実行するIEnumeratorを取得する
/// </summary>
private IEnumerator GetCallbackEnumerator(Action callback)
{
callback();
yield break;
}

/// <summary>
/// seconds秒待機するIEnumeratorを取得する
/// </summary>
private IEnumerator GetWaitForSecondsEnumerator(float seconds)
{
yield return new WaitForSeconds(seconds);
}

/// <summary>
/// シーケンスのIEnumeratorを取得する
/// </summary>
private IEnumerator GetEnumerator()
{
// InsertされたIEnumeratorの実行
int counter = _insertedEnumerators.Count;
foreach (InsertedEnumerator insertedEnumerator in _insertedEnumerators)
{
Coroutine coroutine = _owner.StartCoroutine(insertedEnumerator.GetEnumerator(() =>
{
counter--;
}));
_coroutines.Add(coroutine);
}
// InsertされたIEnumeratorが全て実行されるのを待つ
while (counter > 0)
{
yield return null;
}
// AppendされたIEnumeratorの実行
foreach (IEnumerator appendedEnumerator in _appendedEnumerators)
{
yield return appendedEnumerator;
}
// 終了時の処理
if (_onCompleted != null)
{
_onCompleted();
}
}
}



あとがき

Unityでの非同期処理をどうするか、というのは自分の中でなかなかの課題で、CoroutineSequenceは、自分が所属しているプロジェクトで導入してみて、ある程度重宝されている(と思っている)が、もっといい方法があるのではないか、とも思っている。

今後、UniRxの利用が当たり前になったり、Unity5.5以降でasync/awaitが使えるようになったりして、Unityの非同期処理事情は変わっていくのかもしれないが、これからも最善を模索していきたい。