概要
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");
}
Test1Enumerator
とTest2Enumerator
を単純に同時に実行したいときは、
void Test()
{
StartCoroutine(Test1Enumerator());
StartCoroutine(Test2Enumerator());
}
と記述すれば実現できる。
一方、Test1Enumerator
とTest2Enumerator
の実行がどちらも終了した後、別の処理を行いたい場合はどうだろう。
色々な方法があると思うが、Test1Enumerator
とTest2Enumerator
をいじらないとすれば、以下のようなコードが考えられる。
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();
}
上のコードでは、Test1Enumerator
とTest2Enumerator
の実行が終了した後、Debug.Log("Finish!")
が実行される。
これを実現するために、ParallelEnumerator
とHelperEnumerator
という二つのコルーチン用メソッドを追加した。
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
を使って実現した処理は、以下のコードで実現可能である。
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を返すので、以下のようにメソッドチェーン風に記述することもできる。
void Test()
{
new CoroutineSequence(this)
.Insert(0f, Test1Enumerator())
.Insert(0f, Test2Enumerator())
.OnCompleted(() => Debug.Log("Finish!"))
.Play();
}
Aを実行した後、BとCを並列で実行し、その後Cを実行する、なら、CoroutineSequenceを入れ子にして、以下のように記述可能である。
void Test()
{
new CoroutineSequence(this)
.Append(A())
.Append(new CoroutineSequence(this)
.Insert(0f, B())
.Insert(0f, C()))
.Append(D())
.Play();
}
CoroutineSequenceのコード全文
/*
* 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の非同期処理事情は変わっていくのかもしれないが、これからも最善を模索していきたい。