tetu式

ゲームと音楽・作曲の自己満足と悩みどころの多いプログラムのブログ。

Unityで使うIEnumerator

最近気付いたこと。
知っている人にとっては当然のことと思うかもしれないですが自分にとっては新しい発見だったので一応記事に。
ただし内容はチラ裏並。


Unityでは非同期で処理したい時に使ったりするIEnumeratorさん。
普通のC#だとロードに時間がかかりそうな処理を任せる時に使うかもしれないですが、
Unityでは処理の間に

yield return new WaitForSeconds(指定秒数);

を入れることで指定秒数分だけ処理を止めることも出来るので、Updateとは別の方法でのアニメーション処理とかにも使えたりします。
そこそこ大きい指定秒数にするとゲーム&ウォッチみたいな動きもできるかもですね。

アニメーション以外にも徐々に○○、という処理にも向いてるのでBGMのフェードイン、フェードアウトなんかにも一役買ってくれるでしょう。




さて、IEnumeratorは非同期で処理、という風に書きましたが自分の中では

これ俺が一緒に処理すると途中で止まっちゃってヤバいから任せるわ、ぽいっ。

みたいなイメージでした。
AさんがBさんに仕事を与え、あとはAさんが関わらなくてもBさんが一人でやってくれるイメージです。

が、これは間違いでした。もうちょっと言うと書き方の問題でした。

間違いと気付いた後のイメージとしてはAさんはBさんに命令を出し続け、Bさんは命令されてないと動けないというもの。

たとえばBさんのクラスは

using UnityEngine;
using System.Collections;

public class B : MonoBehaviour {

    bool flg = false;

    public IEnumerator StartRot()
    {
        yield return new WaitForSeconds(0.01f);
        Invoke ("flgOn",3.0f);
        while(!flg)
        {
            this.transform.Rotate(Vector3.up,5.0f);
            yield return new WaitForSeconds(0.01f);
        }
        yield return new WaitForSeconds(0.01f);
    }

    private void flgOn()
    {
        flg = true;
    }
}

こんなクラスだったとします。
処理内容はY軸回転を一定時間繰り返すもの。
これは自身で IEnumerator を呼び出すことができず、他のクラスから呼び出す必要が出てきます。

このクラスBを持ったプレハブをOBJ_Bとします。

AさんはこのOBJ_Bのプレハブを読み込み、クリックされたら生成して命令実行後、自身を削除、という流れにします。

using UnityEngine;
using System.Collections;

public class A : MonoBehaviour {

    GameObject instOBJ_B;

    void Awake ()
    {
        instOBJ_B = Resources.Load ( "Prefab/OBJ_B" ) as GameObject;
    }
    
    void OnMouseDown()
    {
        GameObject tmp = ( GameObject ) Instantiate( instOBJ_B );
        StartCoroutine( tmp.GetComponent<B>().StartRot() );
        Destroy(this.gameObject);
    }
}

クラスAを持っているオブジェクトは大きさに合わせたColliderコンポーネントがあると思ってください。

AさんはBさんを作成し、Bさんに回転する命令を送って自害してるわけですが、 IEnumerator StartRot を命令しているのはどちら?という問題になります。

この場合、IEnumerator StartRot はAさんから命令しています。
Aさんの命令を受けてBさんが一定時間くるくるする訳ですが、Aさんが死ぬとBさんは命令が途絶えてしまうので停止してしまいます。

Destroyをコメントアウトすればもちろん一定時間Bさんが回りますが、Aさん自体はこの命令が済んだらあとは何もしなくても良いため、出来ればちゃんと退場願いたいところ。
メモリ管理が気になってくると特にそんな感じですね。

ではどうするかというと IEnumerator StartRot を実行する命令をBさん自身ですればいいのです。
他のクラスから直接ではなく、間接的に送るようにします。

using UnityEngine;
using System.Collections;

public class A : MonoBehaviour {

    GameObject instOBJ_B;

    void Awake ()
    {
        instOBJ_B = Resources.Load ( "Prefab/OBJ_B" ) as GameObject;
    }
    
    void OnMouseDown()
    {
        GameObject tmp = ( GameObject ) Instantiate( instOBJ_B );
        tmp.GetComponent<B>().StartReady();
        Destroy(this.gameObject);
    }
}



public class B : MonoBehaviour {

    bool flg = false;

    public void StartReady()
    {
        StartCoroutine(StartRot());
    }

    private IEnumerator StartRot()
    {
        yield return new WaitForSeconds(0.01f);
        Invoke ("flgOn",3.0f);
        while(!flg)
        {
            this.transform.Rotate(Vector3.up,5.0f);
            yield return new WaitForSeconds(0.01f);
        }
        yield return new WaitForSeconds(0.01f);
    }

    void flgOn()
    {
        flg = true;
    }
}

こんな風に書くとAさんがBさんの IEnumerator StartRot を「俺が実行する」ではなく「Bで実行しとけ」って命令する訳です。
こうするとBさんが自分の意思で IEnumerator StartRot を呼び出し、他の誰かに束縛されることなくくるくるできるようになります。


とまぁ、こんな感じ。
呼び出す側を消す理由がないならこの限りではないですが、たとえばゲームのタイトルで「PRESS START」っていうボタンがあったら最初に押されて必要なメニューを表示したら、基本もういらなくなりますよね?
そういう時には直接 IEnumerator を呼ばずに呼び出し元のクラスから別の void とかで呼び出すようにしましょう。

別の大本になるクラス(マネージャーとか)があってそちらでフラグ管理とかしてるならそれはそれで良いと思います。