僕はStartCoroutine()
をよく使います。
コルーチンは便利なものです。
ただ処理を記述する際に、もう少し何とかならないかと思う箇所もあります。
その部分を解決するために、今回はCustomYieldInstruction
を使ってカスタムコルーチンの作成にチャレンジしてみました。
さあ、僕はうまく扱うことができるのでしょうか。
この記事にはUnity2017.1.0f3を使用しています。
- IEnumeratorで返す関数の結果が欲しくて……
- CustomYieldInstructionを使ってみたけど……
- yieldを本当はよく分かっていなくて……
- でも頑張れば何とかなるんじゃ……
- そもそも普通のクラスで解決出来たじゃないか……
- 参考リンク
IEnumeratorで返す関数の結果が欲しくて……
コルーチンで処理中の関数内で、IEnumerator
を返す関数をyield return
で使えばその関数の終わりまで処理を待ってくれて便利です。
でもIEnumerator
を返している以上、この関数に成否があった場合に取得が困難です。
僕は引数にコールバック関数を渡して判断する方法を主に用いています。
using UnityEngine; using System.Collections; public class TestRoutine : MonoBehaviour { private IEnumerator Start() { Debug.LogFormat( "Start Time={0} Frame={1}", Time.time, Time.frameCount ); bool result = false; System.Action<bool> onFinished = i_result => result = i_result; yield return WaitProcess( onFinished ); Debug.LogFormat( "Finish Result={0} Time={1} Frame={2}", result, Time.time, Time.frameCount ); } IEnumerator WaitProcess( System.Action<bool> i_onFinished ) { yield return new WaitForSeconds( 3.0f ); // 何かこの処理に対して"結果"というものがあるとして…. bool result = Random.Range( 0, 2 ) == 0 ? true : false; i_onFinished( result ); } } // class TestRoutine
ただ、ラムダ式でコールバック関数を用意するなど、関数を呼ぶための前準備が多く必要なので行数もかさむし、そもそも面倒です。
他にもそのクラスのメンバ変数に値を入れて参照する方式もありますが……。
using UnityEngine; using System.Collections; public class TestRoutine : MonoBehaviour { private bool m_result = false; private IEnumerator Start() { Debug.LogFormat( "Start Time={0} Frame={1}", Time.time, Time.frameCount ); yield return WaitProcess(); Debug.LogFormat( "Finish Result={0} Time={1} Frame={2}", m_result, Time.time, Time.frameCount ); } IEnumerator WaitProcess() { yield return new WaitForSeconds( 3.0f ); // 何かこの処理に対して"結果"というものがあるとして…. bool result = Random.Range( 0, 2 ) == 0 ? true : false; m_result = result; } } // class TestRoutine
ただ個人的にはちょっと……。
この関数が複数同時に呼ばれた場合には対応できないですし、クラス内のすべての関数でアクセス出来るため、他の関数で間違えて書き換えてしまったり、そもそもこの変数の結果が残りっぱなしになってしまいます。
ただ近所の噂では、CustomYieldInstruction
という凄いやつがいるという話を聞きました。
これを使えば、この問題が解決されるのではないでしょうか。
CustomYieldInstructionを使ってみたけど……
スクリプトリファレンスを見たけど、いまいち使い方がわかりません。
どう使えばよいのでしょうか。
ここは先人の方々の知恵をお借りしましょう。
CustomYieldInstruction
はUnity 5.3から追加された機能。
これならば誰かがいい感じの使っているはずです。
こちらの方の記事を参考にして使ってみましょう。
using UnityEngine; using System.Collections; public class TestRoutine : MonoBehaviour { private IEnumerator Start() { Debug.LogFormat( "Start Time={0} Frame={1}", Time.time, Time.frameCount ); var process = new WaitProcess(); yield return process; Debug.LogFormat( "Finish Result={0} Time={1} Frame={2}", process.Result, Time.time, Time.frameCount ); } } // class TestRoutine public class WaitProcess : CustomYieldInstruction { public WaitProcess() { m_routine = Routine(); } private IEnumerator m_routine = null; public bool Result { get; private set; } public override bool keepWaiting { get { return m_routine.MoveNext(); } } private IEnumerator Routine() { yield return new WaitForSeconds( 3.0f ); // 何かこの処理に対して"結果"というものがあるとして…. Result = Random.Range( 0, 2 ) == 0 ? true : false; } }
Start Time=0 Frame=1
Finish Result=True Time=0.02 Frame=2
yield return new WaitForSeconds()
の処理が正しく機能していないような気が……。
確かに参考にした記事ではyield return null
を使っていたなぁ。
機能していないというより、1フレームしか待ってないね。
どうしてこんなことになるんでしょうか。
yieldを本当はよく分かっていなくて……
うーん、これはyield
について、僕がよく理解していないのが問題のようだなぁ。
C#を知っていてUnityを使っているんじゃなくて、Unityを使いたいからC#を学んだような感じだからなぁ。
そのためC#そのものには疎いんですよね。
yield
について少し勉強してみましょう。
こちらのサイトにこんなことが書かれていました。
yield return でオブジェクトを返すメソッドが呼び出されると、メソッドは何も実行せずにオブジェクトだけを返します。 このとき、オブジェクトの MoveNext() メソッドに対して yield return の内容が割り当てられます。 そして、MoveNext() が実行されるたびに、前回の yield return 文からブロックが再開されるという仕組みになっています。
なるほど、MoveNext()
を呼ぶと、次のyield return
まで処理を進めるのですね。
今回作成したクラスではkeepWaiting
プロパティにて、MoveNext()
を呼んでいます。
そのため、このクラスを呼び出し先のコルーチンにてkeepWaiting
にアクセスするたびに先に進んでしまいます。
yield return null
ならよいのですが、WaitForSeconds()
など一定の処理を待とうとする場合は、正しく動きません。
困ったな。どうしましょう。
これじゃ使えない……。
でも頑張れば何とかなるんじゃ……
でもでも、これならやり方を変えれば何とかなるんじゃないかな。
例えばさっきのWaitForSeconds()
の場合は、while()
とyield return null
を組み合わせれば使えるようになるはず。
WaitProcessクラス
private IEnumerator Routine() { float startTime = Time.time; while( startTime + 3.0f > Time.time ) { yield return null; } // 何かこの処理に対して"結果"というものがあるとして…. Result = Random.Range( 0, 2 ) == 0 ? true : false; }
ほらほら、何とかなったよ!
WWW
クラスの処理を待ったりする場合も、こういう風にwhile()
とyield return null
組み合わせれば何とかなるはずだ。
じゃあ、これからはこのCustomYieldInstruction
を使って処理を書いていこう!
……という風にはなりませんよね。
そもそも、引数にコールバックを仕込むのが面倒くさい、ということから今回CustomYieldInstruction
の使用を検討しているのに、より面倒くさいことになっているではないか!
yield return
の使い方には気を使わなくてはいけませんし、一つの非同期処理を作るのに一つのCustomYieldInstruction
を継承したクラスを作らなくてはいけない。
め、めんどうくせー……。
というわけで今回のCustomYieldInstruction
の検討結果としては、使わない、という結論に達するしかありません。
もっと上手に使う方法があるのかもしれませんが、僕の頭ではアイディアが出てきそうにありません。
うーん、残念。
そもそも普通のクラスで解決出来たじゃないか……
そもそも今回の問題、普通のクラスを使うことで解決できないだろうか。
一つずつ確かめていこう。
まず、MonoBehaviour
を継承していないクラスの非同期処理を、MonoBehaviour
を継承したクラスで実行することは可能です。
using UnityEngine; using System.Collections; public class TestRoutine : MonoBehaviour { private IEnumerator Start() { Debug.LogFormat( "Start Time={0} Frame={1}", Time.time, Time.frameCount ); var externalProcess = new ExternalProcess(); yield return externalProcess.Update(); Debug.LogFormat( "Finish Result={0} Time={1} Frame={2}", externalProcess.Result, Time.time, Time.frameCount ); } } // class TestRoutine public class ExternalProcess { public bool Result { get; private set; } public IEnumerator Update() { yield return new WaitForSeconds( 3.0f ); // 何かこの処理に対して"結果"というものがあるとして…. Result = Random.Range( 0, 2 ) == 0 ? true : false; } }
この書き方なら正しく動作します。
今回は非同期処理内のIEnumerator Start()
で処理を記述しているので直接呼んでいますが、別にStartCoroutine()
で呼ぶことだって出来ますよね。
using UnityEngine; using System.Collections; public class TestRoutine : MonoBehaviour { private void Start() { var externalProcess = new ExternalProcess(); StartCoroutine( externalProcess.Update() ); } } // class TestRoutine public class ExternalProcess { public bool Result { get; private set; } public IEnumerator Update() { Debug.LogFormat( "Start Time={0} Frame={1}", Time.time, Time.frameCount ); yield return new WaitForSeconds( 3.0f ); // 何かこの処理に対して"結果"というものがあるとして…. Result = Random.Range( 0, 2 ) == 0 ? true : false; Debug.LogFormat( "Finish Result={0} Time={1} Frame={2}", Result, Time.time, Time.frameCount ); } }
そして別のクラスから渡した非同期処理をこのクラスに渡して、同じように非同期処理を実行することも出来ちゃいます。
using UnityEngine; using System.Collections; public class TestRoutine : MonoBehaviour { private IEnumerator Start() { Debug.LogFormat( "Start Time={0} Frame={1}", Time.time, Time.frameCount ); var externalProcess = new ExternalProcess( WaitProcess() ); yield return externalProcess.Update(); Debug.LogFormat( "Finish Result={0} Time={1} Frame={2}", externalProcess.Result, Time.time, Time.frameCount ); } private IEnumerator WaitProcess() { yield return new WaitForSeconds( 3.0f ); } } // class TestRoutine public class ExternalProcess { public ExternalProcess( IEnumerator i_process ) { m_process = i_process; } private IEnumerator m_process = null; public bool Result { get; private set; } public IEnumerator Update() { yield return m_process; // 何かこの処理に対して"結果"というものがあるとして…. Result = Random.Range( 0, 2 ) == 0 ? true : false; } }
あれっ、これなら記述の仕方を工夫すれば問題解決できないかな。
using UnityEngine; using System.Collections; public class TestRoutine : MonoBehaviour { private IEnumerator Start() { Debug.LogFormat( "Start Time={0} Frame={1}", Time.time, Time.frameCount ); var process = new CoroutineHelper( WaitProcess ); yield return process.Update(); Debug.LogFormat( "Finish Result={0} Time={1} Frame={2}", process.Result, Time.time, Time.frameCount ); } private IEnumerator WaitProcess( CoroutineHelper.OnFinishAction i_onFinished ) { yield return new WaitForSeconds( 3.0f ); // 何かこの処理に対して"結果"というものがあるとして…. bool result = Random.Range( 0, 2 ) == 0 ? true : false; i_onFinished( result ); } } // class TestRoutine public class CoroutineHelper { public CoroutineHelper( ProcessFunc i_function ) { m_function = i_function; } public delegate void OnFinishAction( bool i_result ); public delegate IEnumerator ProcessFunc( OnFinishAction i_onFinished ); private ProcessFunc m_function; public bool Result { get; private set; } public IEnumerator Update() { if( m_function == null ) { yield break; } OnFinishAction onFinished = i_result => Result = i_result; yield return m_function( onFinished ); } }
この書き方ならいけるはずだ。
最初に話したコールバック方式を使って結果を判断する処理を、CoroutineHelper
内の関数で記述しているので、呼出し元の関数はすごくスッキリします。
今回は、結果の型をbool
型で固定させていますが、ジェネリック型にすることで、どんな型でも対応できるようにできます。
using UnityEngine; using System.Collections; public class TestRoutine : MonoBehaviour { private IEnumerator Start() { Debug.LogFormat( "StartA Time={0} Frame={1}", Time.time, Time.frameCount ); var process = new CoroutineHelper<bool>( WaitProcess ); yield return process.Update(); Debug.LogFormat( "FinishA Result={0} Time={1} Frame={2}", process.Result, Time.time, Time.frameCount ); Debug.LogFormat( "StartB Time={0} Frame={1}", Time.time, Time.frameCount ); var processB = new CoroutineHelper<int>( WaitProcessB ); yield return processB.Update(); Debug.LogFormat( "FinishB Result={0} Time={1} Frame={2}", processB.Result, Time.time, Time.frameCount ); } private IEnumerator WaitProcess( CoroutineHelper<bool>.OnFinishAction i_onFinished ) { yield return new WaitForSeconds( 3.0f ); // 何かこの処理に対して"結果"というものがあるとして…. bool result = Random.Range( 0, 2 ) == 0 ? true : false; i_onFinished( result ); } private IEnumerator WaitProcessB( CoroutineHelper<int>.OnFinishAction i_onFinished ) { yield return new WaitForSeconds( 3.0f ); // 何かこの処理に対して"結果"というものがあるとして…. int result = Random.Range( 0, 10 ); i_onFinished( result ); } } // class TestRoutine public class CoroutineHelper<TResult> { public CoroutineHelper( ProcessFunc i_function ) { m_function = i_function; } public delegate void OnFinishAction( TResult i_result ); public delegate IEnumerator ProcessFunc( OnFinishAction i_onFinished ); private ProcessFunc m_function; public TResult Result { get; private set; } public IEnumerator Update() { if( m_function == null ) { yield break; } OnFinishAction onFinished = i_result => Result = i_result; yield return m_function( onFinished ); } }
StartA Time=0 Frame=1
FinishA Result=False Time=3.001926 Frame=172
StartB Time=3.001926 Frame=172
FinishB Result=6 Time=6.016861 Frame=354
あらあら、いいじゃありませんか。
このやり方なら、返り値数を増やすのだって簡単にできちゃいます。
using UnityEngine; using System.Collections; public class TestRoutine : MonoBehaviour { private IEnumerator Start() { Debug.LogFormat( "StartA Time={0} Frame={1}", Time.time, Time.frameCount ); var process = new CoroutineHelper<bool, string>( WaitProcess ); yield return process.Update(); Debug.LogFormat( "FinishA Result={0} Detail={1} Time={2} Frame={3}", process.Result, process.Detail, Time.time, Time.frameCount ); } private IEnumerator WaitProcess( CoroutineHelper<bool,string>.OnFinishAction i_onFinished ) { yield return new WaitForSeconds( 3.0f ); // 何かこの処理に対して"結果"というものがあるとして…. bool result = Random.Range( 0, 2 ) == 0 ? true : false; string detail = "I have a bad feeling about this."; i_onFinished( result, detail ); } } // class TestRoutine public class CoroutineHelper<TResult, TDetail> { public CoroutineHelper( ProcessFunc i_function ) { m_function = i_function; } public delegate void OnFinishAction( TResult i_result, TDetail i_detail ); public delegate IEnumerator ProcessFunc( OnFinishAction i_onFinished ); private ProcessFunc m_function; public TResult Result { get; private set; } public TDetail Detail { get; private set; } public IEnumerator Update() { if( m_function == null ) { yield break; } OnFinishAction onFinished = ( i_result, i_detail ) => { Result = i_result; Detail = i_detail; }; yield return m_function( onFinished ); } }
StartA Time=0 Frame=1
FinishA Result=True Detail=I have a bad feeling about this. Time=3.010901 Frame=172
やった! 問題解決だ!
引数の型の数は同名のクラス名でオーバーロード出来るため、引数一つ版と引数二つ版のクラスは共存できる点も〇です。
そうか……、既に知っている技術で何とかできる問題だったのか。
新しい技術を調べる前に、今ある技術で何とかする方法を模索することも大事ですね。
参考リンク
スクリプトリファレンス
Unity - Scripting API: CustomYieldInstruction
Unity公式ブログ
カスタムコルーチン | Unity Blog
さけのさかなのブログ
【Unity】 カスタムコルーチンでこのコールバックからの卒業 - さけのさかなのブログ
G-MODE Engineers' Blog
G-MODE Engineers' Blog — StartCoroutineは何をしているのか?作って学ぶコルーチンの仕組み(前編)
C#入門
yield キーワード