Unityにおけるコルーチンの使い方【サンプルあり】

ゲームを作ってる上で結構面倒くさいのが、非同期処理です。例えば、ロード画面でバックグラウンドで情報をロードしつつ
ゲーム画面ではアニメーションをしていたり、特定の処理が完了したら挙動を変化させたりなど、色々な要素が絡み合っていて
頭を抱える人も多いでしょう。
なので、今回はUnityの非同期処理である。コルーチンについて解説していきます。



ゲームは非同期でつらいよ

さて、そもそもゲームというコンテンツを考えてみると、非同期処理のオンパレードです。
例えば、ポーズ画面でゲームの動きをすべて止めたり、タイマーがゼロになったら自動的にゲームオーバーになったりなど、
メインのゲームロジックとは別に何らかの要素によって常に影響を与えるものが多いのです。
そのため、条件分岐が複雑になりすぎて、どんどんと各オブジェクトが密に結合していってしまいます。
例えば↓のように、タイマーがゼロになったら以下の処理をやってほしいというのがあった場合

    Update() {
        --timer; 
        // timerがゼロになったら実行してほしいロジック
        if (timer > 0) {
            // いろいろな処理
        } 
    }

このifの条件が増える(30秒以下で音楽を変えるエフェクトを変えるなどなど)した場合は、加速度的にif文が増えます。
Unityの性質上、オブジェクトはUpdate関数に処理を記述しないといけないので、↑のようにガッツリUpdateの関数に依存したものは
後々の処理を分割するというのが難しくなります。

ましてや、ゲーム開発の性質上、実際に作ってみる-> 遊ぶ -> 仕様を追加 or 変更 というサイクルを行って開発していくので、
後々のためにオブジェクトの構成を考えておかないと真面目に絶望します。

ちなみに、私の経験だと、むりやりオブザーバーパターンと列挙体を組み込んでなんちゃって非道処理システムみたいなものを作ったら、
ゲームの複数のロジックと依存してて、ゲームのロジックの変更が入ったときに、とてつもなく現実逃避したくなったことがあります。

Unityのコルーチン

このゲーム開発の苦行にメスをを入れたのがコルーチンです。
コルーチンとは特別な関数で、特定の地点で処理を打ち切り次のフレームで処理を再開するというものです。

実際にタイマーの実装例をもとにに使い方とをみてみましょう。

普通の使い方

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

public class timer : MonoBehaviour {

	// Use this for initialization
	void Start () {
        // コルーチンを実行
		StartCoroutine("testtimer",10);		
	}
	
	// Update is called once per frame
	void Update () {

	}

	IEnumerator testtimer (int lefttime) {		
		while(lefttime >= 0){
			// 残り時間が0以上の場合はタイマーを更新 
			yield return new WaitForSeconds(1.0f);
			Debug.Log(lefttime);
			--lefttime;
		}
		Debug.Log("end");
	}
}

↑を適当なゲームオブジェクトに貼り付けて実行すれば

といった形でカウントダウンしていきます。

今回のコルーチン関数は以下の部分です。

	IEnumerator testtimer (int inittime) {		
		// 残り時間が0以上の場合はタイマーを更新 
		while(inittime >= 0){
			// タイマーを更新してフレームを中断
            yield return new WaitForSeconds(1.0f);
            // 次のフレームでここから再開
			Debug.Log(inittime);
			--inittime;
		}
		Debug.Log("end");
	}
}

それでは詳しく解説していきましょう。
コルーチン関数は以下の要素で構成されていて、

  • 戻り値が IEnumerator
  • 戻り値の指定は yeild という演算子を使う
  • 処理の再開条件は yiledのあとの戻り地によって決定する
  • これだけです。

    まずひとつ目に関しては

    	IEnumerator testtimer (int inittime) {		
    

    ↑の IEnumeratorといのが返り値についていますよね。これが一つ目の条件となります。
    これを使うことで、これはコルーチンを使う関数ということを定義しているのです。

    そのため、2つめの条件であるyiled演算子、つまり

    			yield return new WaitForSeconds(1.0f); // ここで処理を中断
                // 次のフレームでここから処理を再開する。
    

    の部分になります。この返り値の指定によって、フレームの処理を中断して、次のフレームで
    処理を再開します。
    そして再開する条件を指定するために、この戻り値に特定の値を返すことによって、処理を再開する条件を指定できます。
    今回の場合は1秒後に処理を再開するようにしているので、 WaitForSecondsという特定の時間時間後に処理を再開するオブジェクトに 1.0fというを与えています。

    そして最後にコルーチン関数を実行するために、

    	// Use this for initialization
    	void Start () {
            // コルーチンを実行
    		StartCoroutine("testtimer",10);// 実行		
    	}
    

    StartCoroutineという関数に 関数名と、関数の引数をそれぞれ渡します。

    yieldの戻り値について

    コルーチンのキモは処理を中断して再開するタイミングであるということが、
    先程の説明からわかってきたと思います。
    なので、そのタイミングを左右するコルーチンの戻り値と効果をこれから順々に解説していきます。

    そのフレームの処理を打ち切る

        yield return null;
    

    上記のように戻り値にnullにした場合そのフレームでの処理を打ち切って、次のフレームで処理を再開します。

    コルーチンを強制終了

        yield break;
    

    実行しているコルーチンを特定のタイミングで終了させます。
    この場合は、処理の再開が発生しません。例えば、先程のタイマー例にすると

    	IEnumerator testtimer (int inittime) {		
    		while(inittime >= 0){
    			// 残り時間が0以上の場合はタイマーを更新 
    			yield return new WaitForSeconds(1.0f);
    			Debug.Log(inittime);
    			--inittime;
    			if(inittime == 5) {
    				yield break;// この時点でコルーチンが終了する
    			}
    		}
    		Debug.Log("end");
    	}
    

    タイマーは6秒の表示にして機能を停止します。

    特定の時間待つ

        yiled return new WaitForSeconds(3.0f); // 3秒間待つ
    

    これはUnityが用意しているWaitForSecondsというオブジェクトで、使用します。
    コードにもあるように与える値によって待つ時間を調節できます。

    中断条件のカスタム

    先程の戻り値の解説を見てもらえばわかるとおり、コルーチンは戻り値によって再開するタイミングを調節することができます。
    そのため、Unity側もこの再開するタイミングを自分でカスタマイズする機能を用意してくれています。
    それがCustomYieldInstructionです。

    実際に例を見てましょう。これは公式ホームページのコードを少しいじったもので、キーボードのAが押されたときに処理を再開するようにします。

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class ButtonWait : MonoBehaviour {
    
    	// Use this for initialization
    	void Start () {
    		StartCoroutine("routine");
    	}
    	
    	// Update is called once per frame
    	void Update () {
    		
    	}
    
    	IEnumerator routine(){
    		yield return new PushAButton();
    		Debug.Log("A button has push");
    	}
    }
    
    class PushAButton : CustomYieldInstruction{
    
    	public override bool keepWaiting{
    		get {
    			return !Input.GetKeyDown(KeyCode.A);
    		}
    	}
    
    	public PushAButton(){
    		Debug.Log("waiting for press A button");
    	}
    }
    
    

    カスタムイベントは下記の部分で定義しています。

    class PushAButton : CustomYieldInstruction{
    
    public override bool keepWaiting{
    	get {
    
    		return !Input.GetKeyDown(KeyCode.A); // ここで再開する条件を記述する
    	}
    }
    
    public PushAButton(){
    	Debug.Log("waiting for press A button"); //コルーチンの処理中断直後の処理はここに書く
    }
    }
    
    

    カスタムコルーチンのイベントは以下の条件で構成されています。

  • 新たなクラスでCustomYieldInstructionを継承している
  • class PushAButton : CustomYieldInstruction{
    
  • keepWaitingをoverideしている
  • keepWaitingはbooleanでtrueのときに再開する
  • public override bool keepWaiting{
    	get {
    		return !Input.GetKeyDown(KeyCode.A);
    	}
    }
    
  • コンストラクタで中断時の処理を定義する
  • public PushAButton(){
    	Debug.Log("waiting for press A button");
    }
    

    実際に作ってみる(fade out)

    では実践編で実際に使えるものを作ってみましょう。
    視覚的な効果が一番わかり易いと思うので、トランジションの効果のフェードアウトを作ります

    ロジック

    まずはロジックを理解しましょう。

    1.特定のスプライトのオブジェクトをカメラの前に置く
    2.徐々にスプライトの色のアルファ値を減少させていく

    これだけです。

    今回は2. の部分にコルーチンを使用して実装してみます。

    前準備

    まずは、オブジェクトを用意しましょう。
    まずはカメラの全面に表示する画像を用意します。
    UIのパネルを使用しましょう。

    画像のようにUI -> panelからパネルオブジェクトを生成して、それを渡します。

    そして、コルーチンするスクリプトはそのパネルのオブジェクトに貼り付けましょう。

    実装

    さて実際にソースコードを書きましょう。

    Spriteのカラーをいじるにはこのプロパティを使えばよいので、
    これを使って実装したのが以下になります。

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    
    
    public class Fade : MonoBehaviour {
    
    	public Image fadeTarget;// fadeoutするスプライトの対象
    
    	// Use this for initialization
    	void Start () {
    		StartCoroutine("fadeout",3);
    	}
    	
    	// Update is called once per frame
    	void Update () {
    		
    	}
    
    	IEnumerator fadeout (int fadeValuePerSeconds) {		
    		float alpha = 1f;
    		while(alpha >= 0){
    			// 残り時間が0以上の場合はタイマーを更新 
    			yield return new WaitForSeconds(0.3f);
    			alpha -= 0.03f;
    			Color nextColor = new Color(fadeTarget.color.r,fadeTarget.color.g,fadeTarget.color.b, alpha);
    			this.fadeTarget.color = nextColor;
    		}
    		Debug.Log("end");
    	}
    }
    

    これを適当なオブジェクトに貼り付けて、フェードさせるオブジェクトをpublic Image fadeTargetにアタッチをしてあげれば、
    だんだんと時間経過とともにフェードしていくことが確認できます。

    今回は適当な時間でフェードアウトしてるようにしてますが、
    例えば今回は紹介した、カスタムコルーチンと組み合わせるとボタンを押したことをトリガーに
    フェードアウトしていくということもできますし、
    コルーチンのフェードアウトの部分に「何秒かけてアニメーションするか」という機能を作り込めば、
    簡単なアセットとして使いまわすことができます。
    また、注意点ですが、今回実装を簡単にするために、上記のように書いているので、フェードはなめらかに行われずになんかもっさりしています。
    なので、これらを調整する場合は lerp という関数を使用して滑らかにするようにしてください。

    まとめ

    ゲームはひたすらに 作る->実際に遊ぶ->問題点を洗い出す -> もう一度作るというサイクルで成り立っています。
    そしてこのサイクルの数がゲームがの質に直結してくるので、できるだけ、作る部分に時間をかけないためにも、
    コルーチンなどの技術を使って簡単に変更が行えるコードを書いていきましょう。









    この記事をかいた人

    assa

    京都でエンジニアをやっています。assaまたはえつーと覚えてください。 本業はwebで趣味でいろいろいじっています。よしなに