徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【Unity】Invoke()をコルーチンに置き換えて問題が発生した件

 とあるコードを見ているとInvoke()をコード内で見つけました。
 んー、Invoke()かぁ。あまり好ましくないなぁ。
 コルーチンを使う形に変更しましょう。
 と、軽い気持ちでリファクタリングしました。
 その後、この処理の部分が動かなくなりました。
 い、一体何が起きたというんだ!
 そんな思い出話です。

この記事にはUnity5.5.1f1を使用しています。

Invoke()ってなんだっけ?

 えーっと、前に一度調べたような気がしますね。
 指定した関数を時間差で呼ぶことが出来るMonoBehaviourの関数の一つです。
 詳しくは以前まとめた資料をどうぞ。

www.urablog.xyz

どうしてInvoke()は嫌だったの?

 一番の理由はInvoke()で時間差で呼びたい関数の指定するする方法が文字列で指定するというところです。
 文字列のために、VisualStudioなどの関数検索機能に引っかからない。
 文字列のために、関数名を間違えてもコンパイルエラーは起こらない。そのため、実際に処理されるまで間違いに気づかない!

f:id:urahimono:20170128192052p:plain

 関数名を変える可能性などを考えると、あまり使用したくはないんですよねー。

どうやってコルーチンに置き換えたの?

 以下のInvoke()を使った処理があるとしましょう。

Invoke()を使用時

public class TestComponent : MonoBehaviour
{
    public void CallMethod()
    {
        Invoke( "Method", 3.0f );
    }

    private void Method()
    {
        Debug.LogFormat( "Call Method" );
    }

} // class TestComponent

 これを以下のようにコルーチンを使う形に変更。

コルーチンに変更後

public class TestComponent : MonoBehaviour
{
    public void CallMethod()
    {
        StartCoroutine( Method() );
    }

    private IEnumerator Method()
    {
        yield return new WaitForSeconds( 3.0f );
        Debug.LogFormat( "Call Method" );
    }

} // class TestComponent

 まあ、簡単に書き換えれますよ。
 気を付けたことは、using System.Collections;を指定し忘れないようにすることぐらいです。

 実際にUI上のボタンから関数を呼んで動くことを確認しました。
 うん、普通の状態ならね。

f:id:urahimono:20170128192110p:plain

GameObjectが非アクティブ状態だと面倒なことに……

 さて、問題なのはここからです。
 先ほど作ったTestComponentがアタッチしているGameObjectが非アクティブ状態でCallMethod()を呼ぶとどうなるでしょう。

f:id:urahimono:20170128192126p:plain
f:id:urahimono:20170128192139p:plain

 なんか出ましたよ、見たくない赤いマークと共に。

Coroutine couldn't be started because the the game object 'Test' is inactive!

 そうなんです、GameObjectが非アクティブ状態ではStartCoroutine()は呼べないみたいなんです。

 ではInvoke()を使った形に戻して再度実行してみましょう。

f:id:urahimono:20170128192153p:plain

 動きよったー!

 以前Invoke()の機能をまとめた際の資料にも記述したように、Invoke()はアタッチしているGameObjectが非アクティブでも元気に動くのです!
 この違いがあるため、Invoke()を使っている箇所を何も考えずにコルーチンに置き換えることは危険です!

 まさか万能と思っていたコルーチンにこんな弱点があるとは……。

(おまけ)こんな解決法もありますよ

 今回の問題点はアタッチしている非アクティブになる可能性があるGameObjectに対してStartCoroutine()を呼ぶから問題になるのです。
 では常にアクティブ状態になっているGameObjectならば、この問題は起こらなくなるはずだ。

 でも、今は常にアクティブ状態であったとしても、多人数で作業しているときや、仕様が変更になったりすればその条件が守れなくなる可能性も出てきます。
 ではStartCoroutine()を呼ぶ専用のGameObjectを作るというのはどうでしょうか。

public class TestComponent : MonoBehaviour
{
    [SerializeField]
    private MonoBehaviour   m_coroutineObject   = null;


    public void CallMethod()
    {
        // m_coroutineObject常にアクティブになっている前提だよ!
        m_coroutineObject.StartCoroutine( Method() );
    }

    private IEnumerator Method()
    {
        yield return new WaitForSeconds( 3.0f );
        Debug.LogFormat( "Call Method" );
    }

} // class TestComponent

 関数を呼ぶGameObjectが非アクティブでも、StartCoroutine()を呼ぶGameObjectがアクティブなら問題なく動きます。
 今回は手を抜いてInspector上で指定する形にしていますが、シングルトンなどを利用すればもう少し使いやすくなると思います。

 皆さんもInvoke()を見つけてコルーチンにリファクタリングしたいなぁーと思ったときに僕のようにならないようにしてくださいねー。