Unity(C#) StartCoroutine (yield 関連) の使い方メモ
0. 今月中に引っ越します
明けましておめでとうございます。
ガンズターンのRyosukeです。
本年もよろしくお願いいたします。m(_ _)m
本当は新年明けてからすぐにブログ書くつもりでしたが、会社人として働き始めたこともあり、なかなか時間が取れずに先送りしてきちゃいました。
おかげさまで無事、転居先も決まりまして(本契約まだですが)、たぶん今月中には東京人になっていることと思います。
今回で、通算8回目の引越し。(子供の頃のやつもカウントすると9回目)
思えば、ずいぶんと引越しの多い人生を過ごしてますね。
とくに、最近3年間は毎年引っ越してます。
次に住むのは今の10倍ぐらい都心に出やすい場所なので、勉強会とか出やすくなるはず……!
というわけですので、なんかあったらお気軽にお声掛けくださいませ。
平日は難しいかもですが、予定が合えば極力参加させていただきますゆえ……! m(_ _)m
てなわけで、すでに引越し屋さんも決めて段ボールももらったので、引越し日に向けて淡々と荷造りしつつ、今このブログを書きこんでます。
もくじ
1. こっから本題
さて。今日書きたかったのは他でもない、タイトルにある通り「Coroutine」(コルーチン)について(とくにUnity C#の)です。
Unityの教則本とかで、けっこうなんの説明もなしに「StartCoroutine()」とか使われてますよね?
自分もたまに使いますが、なんとなく意図した通りの動きにはなっているけど、実際のところどんな理屈で動作している仕組みなのかは全く理解していませんでした。
「仕事でUnityを使う以上、こんな重要な仕組みの理解を曖昧なままにしておくわけにはいかん!」
……てなわけで、今回自分できちんと調べてみようと思ってみたわけであります。ハイ。
例のごとく、素人に毛が生えた程度の人間の書くことですので、なんか間違ったこと書いてたら遠慮せずバンバン叩いてくださいませ。(その方が自分自身の成長にもつながりますので)
では、参ります。
あ、ちなみに、このブログ記事を書くにあたり、以下のサイト様の情報を多分に参考にしております。
私の拙い記事よりも確実に参考になると思いますので、コルーチンについて知りたい人はまずはこちらのページからチェックなされるとよろしいかと……
- 『テラシュールブログ』さん
「UnityのCoroutine(コルーチン)でできる事のメモ」
http://tsubakit1.hateblo.jp/entry/2015/04/06/060608 -
『ほげほげー』さん
「C#におけるyieldの挙動」
http://tyheeeee.hateblo.jp/entry/2013/08/07/C%23%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8Byield_return%E3%81%AE%E6%8C%99%E5%8B%95 -
『kwst』さんの記事(Qiita)
「コルーチンの初歩的な使い方【Unity, 初心者】」
http://qiita.com/kwst/items/ce04abce7c1e2c72e023 -
『Wonderplanet エンジニアBlog』さんの記事
「Unityのコルーチンの使い方をまとめてみた」
http://wonderpla.net/blog/engineer/Unity_Co-routine/ -
『kazz4423』さんの記事(Qiita)
「Unityにおけるコルーチンの性質まとめ」
http://qiita.com/kazz4423/items/73219068684e87adc87d
その他、記事中で触れるサイトもありますが、その時はそのタイミングでリンク貼りますね。
2. そもそもコルーチンってなんぞ?
こういう時は、とにもかくにも wikipedia さんに尋ねてみましょう。
上記リンク先より一部抜粋すると、以下のような記述があります。
〜〜〜〜(略)〜〜〜〜
サブルーチンがエントリーからリターンまでを一つの処理単位とするのに対し、
コルーチンはいったん処理を中断した後、続きから処理を再開できる。
接頭辞 co は協調を意味するが、複数のコルーチンが中断・継続により
協調動作を行うことによる。
〜〜〜〜(略)〜〜〜〜
つまり、任意の場所でいったん処理を抜けて、またその場所に戻ってこれるような仕組みを備えたルーチンってこと……になるんですかね。。。
普通のサブルーチンの考え方だと、一度始まったルーチンは、そのルーチンの出口(通常はreturn文の場所か、あるいはルーチンの末行)まで辿り着かない限り、他の処理に制御を渡すことはできません。(シングルスレッドで動いてる場合)
また、一度処理を抜けてしまうと、再度呼び出した時はまたそのサブルーチンの先頭行から処理が実行されていきます。
けれどコルーチンの場合は、処理の途中で中断もできるし、再度呼び出した時に中断された場所から再開できるわけですね。
便利そう――ですが、いったいどんな時に使う物なんでしょうか?
3. Unityだとこんな時に使えるよ
例えば、複数のオブジェクトをアニメーションさせつつ、すべてのアニメが完了するのを待ってから次の処理に移ったりするのを比較的簡単につくれます。
コルーチンを使うと、複数の処理(でかつ、各処理が複数フレームにまたがるもの)を同時並行的に進行させつつ、お互いに連携させるような処理を、複雑な状態管理なしで実現することができるようになります。
(この辺の具体的な例は『テラシュールブログ』さんの記事に詳しいのでご参照くださいませ)
Unityの場合はUpdate()メソッドを使えるので、処理の数だけコンポーネントを作ればコルーチンを使わずとも同様のことが実現できますが、その場合、処理終了後のコールバックの設計が必要だったりして、けっこう煩雑なロジックになります。(個人的な経験上)
その点コルーチンを用いれば処理終了後に自動的に元の位置に処理が戻って来るため、コールバックもいらないし、もちろん個々の処理が終わったかどうかを判定する特別な処理なども不要になります。
(実際に計測したわけじゃないのですが、コンポーネントに対するUpdate()の呼び出しよりもコルーチンのほうがオーバーヘッドが少ないという話も聞いたことがあります)
4. 実際どう書くの?(Unity C#の場合)
Unityでコルーチンを使うには「StartCoroutine()」というメソッドを使います。
コルーチンをどのように使いたいかによって、いくつか呼び出し方のパターン(具体的には3つ)がありますので、それを簡単に以下にまとめます。
(この先、ほぼ『テラシュールブログ』さんの記事の焼き直しになってしまってます。とはいえ、もうちょっと初心者向けに噛み砕いて書いてるつもりなので、先にリンク先を読んでいただいてから、理解できなかった場合に限り読み進めて頂くと、貴重な時間を有効に使えるかもしれません)
(1) 単純に並列処理を走らせたい時
簡略化した例を示すため、「毎フレーム1から5まで1つずつ数を数えてログを吐き出す処理 CountFrame1To5()」と、「毎フレーム201から205まで1つずつ数を数えてログを吐き出す処理 CountFrame201To205()」という2つのメソッドの例を考えます。
まずは、とくに処理間で協調せず、単純に2つの処理を並列で走らせたいだけの時。
以下のソースコードのように呼び出します。
このコンポーネントがUnity上で実行された時、吐き出されるログは以下のとおり(簡略化して書いてます)。
CountStart !!
1
201
CountEnd !! // 変なとこに出てるけど今は気にしないで!
2
202
3
203
4
204
5
205
StartCoroutine() を介して呼び出している2つのメソッドが、1カウントずつ交互に処理されていることがわかります。
(本来はカウント終了時にログに吐き出そうとした「CountEnd !!」という文字が4行目に出力されてしまっていますが、このログをしかるべき場所に出力する方法については後述します。とりあえず気にしないで読み進めてください)
ここで重要なのは、以下の3点。
- StartCoroutine()を通じて呼び出すメソッドは、必ず「IEnumerator」型を返すメソッドでなければならない。
- コルーチン内で「yield return null;」すると、いったん処理を中断して呼び出し元へ処理を返す。(この書き方は、ひとまずイディオムとして覚えてしまった方がいいです。C# における yield の使用例が理解できるとなんとなくわかってきます)
- 再度コルーチンが呼び出された時は、前回処理が中断された場所(「yield return null;」の次の行)から再開する。
ここでの処理の流れを簡単なフローで表すと、以下のような感じになります。
Start()メソッドは1フレーム目で末尾まで達するので「CountEnd !!」が出力されて処理が終わります。
不思議なのは、2フレーム目以降も「CountFrame1To5()」や「CountFrame201To205()」が毎フレーム呼び出されている点ですね。
呼び出し元の「Start()」メソッドはすでに末尾まで到達してしまっているのに、いったい誰が、毎フレーム律儀にこれらのコルーチンを呼び出してくれてるのでしょう?
――みなさん、もうおわかりですね?
そうです。
……正解は、Unityさんです。
「StartCoroutine()」を使って呼び出したコルーチンについては、Unity内部で(おそらくList<Coroutine>のような形で)保持されていて、毎フレーム、Update()処理が終わるたびにUnityさんが1つずつ呼び出してくれてるんですね。
つまり先ほどの図を2フレーム目以降の処理まで含めて書くとこんな感じ。
なので、単純に並列処理をしたいだけの場合は、「StartCoroutine()」を使って「IEnumerator型のメソッドを呼び出してやる」だけでよいわけです。
自前で同じような仕組みを作るのは大変そうなので、Unityさまさまって感じですね。
ちなみにUnity内部で毎フレームどんな処理が行われているかについては、以下の公式ドキュメントが詳しいです。(コルーチンの実行タイミングについても明記されてる)
(2) コルーチン(単体)の完了を待つ時
例えば「CountFrame1To5()」の処理を先に全て終えてしまってから、「CountFrame201To205()」を実行したい、という場合。
「Start()」メソッドを以下のように修正すれば、OKです。
実行時に吐き出されるログは以下のとおり。
CountStart !!
1
2
3
4
5
201
CountEnd !! // 変なとこに出てるけどやっぱり気にしないで!
202
203
204
205
またしても「CountEnd !!」が中途半端な位置に出力されてますが、今は気にしないでください。
ひとまず、「CountFrame1To5()」によるカウントが全て完了するまで「CountFrame201To205()」が実行されていないことがわかります。
ここで重要なのは、以下の2点。
- 「Start()」メソッド自体の型を「IEnumerator」にすると、Unity内でコルーチンとして実行される。(つまり「yield return ○○;」が使用可能になる。ちなみにUpdate()も同じ仕様)
- コルーチン内で「yield return StartCoroutine();」を実行すると、そこで呼び出したコルーチンが完了するまで処理を進めずに待つ。
ここでの処理の流れをフローにすると以下のとおり。
注目すべきは、「Start()」メソッド内の3行目に処理が戻って来るタイミング。
単純に「StartCoroutine()」を呼び出しただけの時は、コルーチン内の最初の「yield return null;」のタイミングで処理が戻ってきていました。
けれど「yield return StartCoroutine()」とした場合は、呼び出されたコルーチンの処理が末尾にたどり着く(あるいはコルーチン内で「yield break;」される)までは処理が戻ってきていません。
その結果、任意のコルーチンの処理が終わるまで、次の処理を待つという動きが実現できているわけですね。
こうやって一つずつ動きを紐解いていくと、コルーチンだけでもかなり複雑な動きを実装できるようになることがだんだん理解できてくると思います。
おもしろいですね! ^^
(3) コルーチン(複数)の完了を待つ時
これ、見出しの書き方がちょっと難しかったんですが、要は複数の処理を並列に実行していて、それぞれが完了するのを待ってから次の処理に移る、などを実装する時に使う書き方になります。
例えば「CountFrame301To308()」というメソッドを新たに追加して、「CountFrame1To5()」と「CountFrame301To308()」は同時に処理したいけど、「CountFrame201To205()」はそれら2つの処理が完全に終わってから処理を開始したい、というような場合を想定します。
この場合、ソースコードは以下のとおり。
実行時のログは以下のとおり。
CountStart !!
1
301
2
302
3
303
4
304
5
305
306
307
308
201
CountEnd !! // 気にしないで!
202
203
204
205
またしても「CountEnd !!」が中途半端な位置に出力されてますが、いったんそれは忘れてください。
このログを見れば一目瞭然。
「CountFrame1To5()」と「CountFrame301To308()」が並列に実行され、かつ、その両方が終わるまで「CountFrame201To205()」が実行開始されていないことがわかりますね。
ここで重要なのは、以下の3点です。
- 終了を待ちたいコルーチンを呼び出す際、「StartCoroutine()」の戻り値を「Coroutine」型の変数に代入しておく。
- 「yield return Coroutine型の変数;」という形で、「1.」で変数に代入したコルーチンの終了を待つ。
- 終了を待ちたいコルーチンの数だけ「1.」と「2.」を行い、すべてのコルーチンで「yield return」が返ってきた後のタイミングから、次に行う処理を記述する。
「(2) コルーチン(単体)の完了を待つ時」で紹介した書き方と似てますが、「StartCoroutine()」の戻り値をいったん「Coroutine」型の変数に代入しているのがポイントですね。
これをせず、例えば「yield return StartCoroutine();」とやってしまうと、そのコルーチンの処理が終了するまで次の処理にいけないので、複数処理の並列処理ができなくなってしまいます。
ここでの処理の流れをフローにすると以下のとおり。
初心者(というか、この記事を書き始める前の私。苦笑)が勘違いしやすいポイントとしては、「Start()」の中身を以下のように記述してしまう点です。
ものは試しということで、実際にこれで実行してみましょう。
そのログを見てみると……
CountStart !!
1
301
2
302
3
303
201 //あれ、CountFrame201To205()が動き出した!?
CountEnd !! // 気にせんといて!
4
304
202
5
305
203
306
204
307
205
308
むむむ。
途中までは正しく動いているように見えますが、まだ「CountFrame1To5()」も「CountFrame301To308()」も動いている最中なのに、「CountFrame201To205()」が動き出していることがわかります。
このことから、Unityにおいてコルーチンの終了を正しくキャッチするためには「Coroutine」型変数に代入することが必須であることがお分かりいただけるかと思います。
余談ですが、もちろん以下のようにしても正しく動きません。
「StartCoroutine()」を同じメソッドに対して複数回使った場合、使った数だけ新たにコルーチンがUnityに登録されて、それぞれが独立して動作を始めます。
同じ処理を複数個、しかも同時並行で動かしたい時などは重宝する仕組みですが、このことを知らないとどハマりする可能性大です。
いずれにしろ、複数処理の終了を待つような場合は「Coroutine」型変数への代入は必須であり、今回ご紹介したような書き方はコルーチンを使う上で必須のイディオムといったところではないでしょうか?
5. まとめ
いや〜……
いつにも増して長い記事になりましたね。^^;
2日掛かり、かけた時間は半日分ぐらいの大ボリュームとなってしまいました。。。
実はここに書いてある書き方以外にも、「Coroutine」型の変数を使えば「StopCoroutine()」などのメソッドでコルーチンの一時停止なんかも実装できちゃいます。
この辺についても、やっぱり『テラシュールブログ』さんの記事が詳しく記載してくださっているので、ぜひぜひご参考になさるとよろしいかと思います。
「StartCoroutine」の仕組み一つとっても、自分が納得出来るまで調べていくとなかなか一筋縄ではいきません。
(もしUnityがオープンソースだったら、ソースコード解析までしちゃってたかも……? 笑)
ましてや、ブログという形で誰かに伝えるつもりで書こうと思うと、どうしても時間がかかっちゃいますね……。
ひょっとして「Unity コルーチン」とかで検索してこのページへたどり着いた方々からすると、知りたいのはその「書き方」だったり「使い方」だったりするわけで、ここで紹介したような「仕組み」とか「フロー」とかってあんまり興味がなかったりするのかもしれませんが……
個人的には、最後まで書いて満足できました。
ここまでやって、ようやっと「仕事でコルーチン使えるようになったかも……?」て感じです。
今後もこんな感じで「なんとなく今まで使ってきたけど、人に説明できない仕組み」を「人に説明出来る仕組み」に(書いた自分が)なれるような、そんな記事を書いていきたいと思います。
そんなわけで、引き続き当ブログをお引き立てくださいますよう、よろしくお願い申しあげます。m(_ _)m
6. おまけ(簡単なクイズ)
おっと。忘れていた。
ここまで読んでくださった方に、簡単なクイズをお出ししたいと思います。
(といっても、とくに正解者にプレゼントとかは考えてませんが。。。^^; )
[問題]
まずは、解説にも使用した以下のソースコードをみてください。
上記ソースコードでは「CountEnd !!」というログが中途半端な位置に出力されてしまいますが、それを正しい位置(すべてのカウントが終了した後の、ログの最終行)に出力するためには、Start()メソッド内をどのように修正すればよいでしょうか??
なお、修正の際、以下の条件を守るようにして下さい。
- 「Count1To5()」 と 「Count201To205()」 は並列処理で同時実行させること。
とっても簡単なクイズです。
どうしてもわからない場合は、以下のリンクから正解例がご覧になれますので、どうぞポチッとやってみてくださいませ。
それでは本日はこの辺で。
さよおなら〜〜〜!!