【Unity】コルーチンってなんなのなの? って時に読む記事【解説】

【Unity】コルーチンってなんなのなの? って時に読む記事【解説】

Unityを触っていると、コルーチン(Coroutine)を使う場面が多々あります。

「ウェイト処理を入れたいなぁ」

「徐々にImageのアルファの値を増やしたいなぁ」

「1フレーム目のStart()の処理、別のフレームに分散させたいなぁ」

こんな時には、コルーチンを使って処理を実装すると超便利! ……なんだけど、初心者の頃はコルーチンの正体を追求しないでおまじない的に使っていたため、改めて情報をまとめてみることに。初心者向けを謳いながらマニアックな部分にも突っ込んでます。

 

環境

macOS 10.13 High Sierra

Unity2018.2.2f1

コルーチンとは

コルーチンとは処理を中断した後、同じ部分から続けて再開できる処理のまとまりです。なので、指定時間待った後に後続の処理を続けたり、1フレームごとにImageのアルファの値を変化させて、徐々にフェードインさせることが可能なんです。

Update()では別の処理を続けつつ、コルーチンで3秒後に何かのフラグを変更したり、はたまたリソースをダウンロードしたり、なんて非同期っぽい処理も実装可能です。

英語のままならCoroutineで、Co(協調する)-Routine(処理のまとまり)となっている通り、一緒に動いてくれる処理のまとまりなんです。

公式のリファレンスもあわせてご覧くださいな。

処理の中断

処理の中断は『yield return <処理を待つ時間>』のように記述します。<処理を待つ時間>はフレーム単位であったり、秒だったりと、割と柔軟に指定できるので便利です。

ここで指定した時間が経過すると、続きの処理を行うようになります。これがあるのでウェイト処理などが実装できるんです。

yieldはいろんな意味を持つ言葉でちょっとイメージを掴みにくいですが、ここでは「明け渡す」とか「譲る」といった用法が近いかもしれません。指定した時間だけ処理を明け渡しますよ、なんて記述になっています。

サブルーチン

コルーチンと似たような言葉にサブルーチンがあります。こちらも処理のまとまりですが、こちらはデフォルトだと中断した後に同じ場所からの再開はできません。リセットしたら最初からやり直しになるファミコンの古いゲームみたいなイメージでしょうか。

フラグとか作って呼び出されるたびに実行ポイントを変える、みたいなことは実装できるでしょうが、手続き型全盛期時代のプログラミングになりそうです。Fortranとか。

 

話が逸れましたが、大事な点は時間に関係する処理非同期的な処理を盛り込むのに便利だということ。

 

マルチスレッドとの違い

メインの処理をしつつ裏では別の処理をする、という点ではコルーチンはマルチスレッドと似ています。

何が違うかと言えば、マルチスレッドの場合はマルチコアCPUの場合に別のCPUを使って並列処理をさせることができますが、コルーチンはあくまでシングルスレッド内で動いている処理。処理のタイミングを変えているだけなんです。

UnityのAPI、例えばUnityEngineの下にあるGameObjectを生成する処理や、コンポーネントを操作する処理などはメインスレッドで動くことが前提となっています。

これを別スレッドで動かそうとすると「メインスレッドで動かせよオラァン!」なんて怒られます。

例えば、以下のように別スレッドでgameObjectを触ってみます。

適当なGameObjectにスクリプトをアタッチしてゲームを実行すると、以下のメッセージが出力されます。

get_gameObject can only be called from the main thread.
Constructors and field initializers will be executed from the loading thread when loading a scene.
Don’t use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.
UnityEngine.Component:get_gameObject()
ThreadTester:AnotherThreadProcess() (at Assets/Scripts/ThreadTester.cs:16)

めっちゃ怒られました。長々と説教されている気分。

gameObjectはMonoBehaviourの継承元のBehaviourクラス、さらにその継承元であるComponentクラスから継承された変数なので、がっつりUnity APIの範囲内です。

Unityの場合、各プラットフォームに対応した形でコンパイルされるため、マルチスレッドに対応していないプラットフォーム(WebGLとか、WebGLとか、WebGLとか)なのにマルチスレッドの処理を書けちゃったら大変なことになりますもんね。

コルーチンの目的

ゲームの実行においては、可能な限り全ての処理を最短の時間で終わらせることが必ずしも正しいとは限りません。

というのも、画面に表示される要素をフェードイン/フェードアウトさせる演出が必要ですし、ウェイトを導入して間を取ることも必要なためです。あえて時間を使って処理させることも大事なんです。

例えば、以下の例は画面のフェードインを実装してみたもの。左はコルーチンを使わずに一瞬で画面を表示し、右はコルーチンを使って2秒かけて画面のフェードインを行っている例です。ゲーム開始後から少しの間だけ画面が消えているのを入れておきたかったので、Update()の中で10フレーム経過をトリガーとして処理を開始しています。

徐々に画面が明るくなる方がゲームっぽいでしょ? いきなりパッと画面が切り替わるのは、意図した演出でない限りは避けた方が無難です。

GIFなので徐々に変化する右側が汚くなってるのには目を瞑ってください。Unityでは綺麗に見えてるから! 本当です! 信じてください!

フェードなし(左)とフェードあり(右)
フェードなし(左)とフェードあり(右)

 

こういった場合にコルーチンを使うのが最適です。ゲームのプレイヤーが視覚的に知覚できるよう画像のアルファ値をフレームごとに段階的に変化させてレンダリング(画面の描画)をしたり、指定時間待って処理を続けることで間を演出したりできます。

なのでコルーチンを使うケースというのは、マルチスレッドのように複数CPUを使った並列処理でCPU時間を減らす、という目的とはまたちょっと違うんですよね。

目的に応じた使い分けが大事です。

なお、並列処理として空いてるCPUに仕事をさせたい場合は、2018.1から使えるようになったC# Job Systemが便利です。新規にスレッドを立てる代わりにジョブを発行して、Unity側で起動済みの暇してるワーカースレッドに「お、君暇そうだね。この仕事やっといてくれない? もちろんちゃんと定時(フレーム)内で終わらせてね」なんて仕事を割り振る上司のような機能です。

値型しか渡せない制約はあるものの、メインスレッドの負担を減らせるのでCPU時間の削減にはもってこいです。C# Job Systemについてはまた別途。

 

コルーチンを使おう!

肝心の使い方ですが、戻り値がIEnumeratorのメソッドを用意し、それを任意の場所からStartCoroutine(<コルーチンの名前>)で呼ぶ、という流れ。

IEnumeratorはアイ・イニュメレイターとかアイ・エニュメレイターなんて発音します。カタカナ表記は難しい……。

先頭に『I』が付いていることから、インタフェースであることが分かります。何のためのインタフェースかというと、C#でコレクション(要素のまとまり・グループ)の中身を見る時に、現在の位置を知るためのプロパティである『Current』や、次の要素に移動させるためのメソッドである『MoveNext()』などを実装するインタフェースなんです。

コルーチンの場合だと、一度中断して指定した時間が経過して処理を再開する時に、どこまで処理を行ったのか知っておく必要があります。なので、このインタフェースを使っているんじゃないかな(想像)

StartCoroutineはMonoBehaviourから継承されるメソッドで、UnityEngineの内部にあるソースなので中身が見れず、想像するしかないのよね。

 

……とまぁ説明したものの、初心者の頃は文章で読んでも意味が分かりませんでした。なので使用例を見ちゃいましょうか。

実装したのは、コルーチンを開始して1秒待ってから待ち時間を出力する機能です。

戻り値がIEnumeratorのメソッドであるDelayProcessを定義し、それをStart()の中からStartCoroutineを使って起動しています。

IEnumeratorを使う場合にはSystem.Collectionの名前空間を記述しておく必要があります。スクリプトファイルを作成した時に自動的に出力されているはずですが、ない場合は自分で追加しましょ。

コルーチンの中身では開始された時刻をローカル変数のstartTimeに保存し、WaitForSecondsによって引数で指定された秒数だけ処理を中断します。再開後にその時刻をendTimeとして取得し、だいたい指定秒数だけ経過していることを確認します。

yield returnを跨いでもスコープの外に出ないので、最初に宣言したローカル変数startTimeも使えています。

 

適当なGameObjectにスクリプトをアタッチしてゲームを実行すると、以下のメッセージが出力されます。

出力順も興味深い
出力順も興味深い

 

ちゃんと『yield return *』でウェイト処理が入ってから後続のデバッグ文が出力されています。これを使えば、例えばキャラクター同士の会話で間を簡単に作ることができますね。

待ち時間は1.0秒を指定していますが、正確に1.0秒待つのではなく、このフレームの開始時刻が『yield returnで中断したフレームの開始時刻 + 指定秒数』を超えていたら続きの処理を行う、といった条件になっているみたい。なので、実行するたびに微量だけど終了時刻が前後します。

スクリプトリファレンスにも同様の記述があるのでそちらもご覧あれ。

興味深いのはデバッグ文の出力順。

通常のメソッドであれば、Start()の中に書かれた処理は上から順番に実行されるので、そのメソッド内の処理が終わってから後続の処理が実行されます。今回の例でいえば、DelayProcessが終わってからDebug.Log(“コルーチンを起動しました”);が実行されるイメージ。

でもコルーチンの場合は、書かれた処理のうちyield returnで処理を中断するまでは同じフレームで実行され、中断後はStart()に戻ってDebug.Log(“コルーチンを起動しました”);が実行されています。

順番に何らかの値を操作したい場合には、実行順に気をつける必要があるかもしれません。とはいえコルーチンを使ってそこまで厳密にしなきゃいけないケースもそうそうないかな。

まとめ

コルーチンとは処理を中断した後、同じ部分から続けて再開できる処理のまとまり。

ウェイト処理や、数フレームかけて値を変える処理など、時間操作系の処理を行いたい時に便利です。

また、メイン処理の裏で非同期的にリソースをロードしたりする処理も実装可能なので、これが使えると表現の幅がぐっと広がります。

みんなで使おうコルーチン。

     

ゲーム開発の攻略チャートを作りました!

CTA-IMAGE

「ゲームを作ってみたいけど、何から手を付けていいか分からない!」


そんなお悩みをお持ちの方向けに、todoがアプリをリリースした経験を中心に、ゲーム作りの手順や考慮すべき点をまとめたe-bookを作成しました。ゲーム作りはそれ自体がゲームのように楽しいプロセスなので、「攻略チャート」と名付けています。


ゲームを作り始めた時にぶつかる壁である「何をしたら良いのか分からない」という悩みを吹き飛ばしましょう!