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

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

【Unity】死んでも生きてるMonoBehaviour、Unityのコワイうわさ

 もう夏本番になってきましたね。
 というわけで、今回はちょっと怖い話をしていこうと思います。
 これは友達の友達から聞いた話なんですが……。


この記事にはUnity2017.1.0f3を使用しています。

冒頭

 あるところにMonoBehaviourを使ったシングルトンクラスがいました。

using UnityEngine;

public class Singleton : MonoBehaviour
{
    [SerializeField]
    private int m_number    = 0;
    public  int Number
    {
        get { return m_number; }
    }

    public static Singleton Instance
    {
        get;
        private set;
    }

    private void Awake()
    {
        if( Instance != null )
        {
            Destroy( this );
            return;
        }

        Instance    = this;
    }

} // class Singleton

 彼の友人には、ゲーム制御用のクラスがいます。

using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;

public class TestController : MonoBehaviour
{
    private IEnumerator Start()
    {
        DontDestroyOnLoad( gameObject );
        
        Debug.LogFormat( "SceneA num={0}", Singleton.Instance.Number );
        
        yield return null;

        SceneManager.LoadScene( "SceneB" );
        yield return null;
        
        Debug.LogFormat( "SceneB num={0}", Singleton.Instance.Number );
    }

} // class TestController

 世界はこのような作りになっていました。
f:id:urahimono:20170725073425p:plain

 ゲーム制御用のクラスであるTestControllerは、シーンの切り替えと友人であるSingletonNumberプロパティの値をログとして表示することしかできません。

 TestControllerDontDestroyOnLoad()を設定しているので、シーンが切り替わっても消えません
 SingletonDontDestroyOnLoad()を設定していないので、シーンが切り替わったら消えてしまいます

 そのためTestControllerはシーン切り替え後にはSingletonに話しかけることはできないはずです。
 シーンが切り替わればSingletonは死んでしまうのですから。
 きっとエラーが発生するに違いありません。

不滅

 さあ、どうなるでしょう。
f:id:urahimono:20170725073542g:plain
f:id:urahimono:20170725073645p:plain

SceneA num=0
SceneB num=0

 なんということでしょう。
 Hierarchy上にいないはずのSingletonTestControllerが話しかけています。
 どういうことなのでしょう。
 Singleton.Instanceはどうなっているのでしょうか。

using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;

public class TestController : MonoBehaviour
{
    private IEnumerator Start()
    {
        DontDestroyOnLoad( gameObject );

        Debug.LogFormat( "SceneA Instance={0}", Singleton.Instance );

        yield return null;

        SceneManager.LoadScene( "SceneB" );

        yield return null;

        Debug.LogFormat( "SceneB Instance={0}", Singleton.Instance );
    }

} // class TestController

f:id:urahimono:20170725073714p:plain

SceneA Instance=Singleton (Singleton)
SceneB Instance=null

 やはり、Singletonnullになっている!
 きゃあああ、お化けだ!

 ……あれっ、nullの時ってちゃんとnullと表示されるんだっけ。
 デバッガでみてみましょう。

f:id:urahimono:20170725073734p:plain

 んー、Singleton.Awake()の時のInstancenullのときと、SceneBのnullのときとではデバッガの値が違うなぁ。
 Singleton.Instance.gameObjectはどうなっているのでしょう。

using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;

public class TestController : MonoBehaviour
{
    private IEnumerator Start()
    {
        DontDestroyOnLoad( gameObject );

        Debug.LogFormat( "SceneA gameObject={0}", Singleton.Instance.gameObject );

        yield return null;

        SceneManager.LoadScene( "SceneB" );

        yield return null;

        Debug.LogFormat( "SceneB gameObject={0}", Singleton.Instance.gameObject );
    }

} // class TestController

f:id:urahimono:20170725073805p:plain

SceneA gameObject=Singleton (UnityEngine.GameObject)
MissingReferenceException: The object of type 'Singleton' has been destroyed but you are still trying to access it.

 Singleton.Instance.gameObjectには話しかけることはできないみたいです。
 Singleton.Update()はどうでしょうか。

using UnityEngine;

public class Singleton : MonoBehaviour
{
    [SerializeField]
    private int m_number    = 0;
    public  int Number
    {
        get { return m_number; }
    }

    public static Singleton Instance
    {
        get;
        private set;
    }

    private void Awake()
    {
        if( Instance != null )
        {
            Destroy( this );
            return;
        }

        Instance    = this;
    }

    private void Update()
    {
        Debug.Log( "Update()" );
    }

} // class Singleton

f:id:urahimono:20170725073847p:plain

 SceneBではSingleton.Update()は呼ばれていません。
 gameObjectUpdate()のようなHierarchy上になければ使えないものはアクセスできませんが、クラスとしてはまだ存在しているようです。
 やはりお化けなのか……。
 
 こんなお化けみたいな状態で存在されていると、後に気づかぬバグへと発展してしまいそうだ。

成仏

 この原因はstatic変数としてSingletonクラスを保持し続けていることが問題なのです。
 OnDestroy()でちゃんとnullを入れるようにしましょう。

using UnityEngine;

public class Singleton : MonoBehaviour
{
    [SerializeField]
    private int m_number    = 0;
    public  int Number
    {
        get { return m_number; }
    }

    public static Singleton Instance
    {
        get;
        private set;
    }

    private void Awake()
    {
        if( Instance != null )
        {
            Destroy( this );
            return;
        }

        Instance    = this;
    }

    private void OnDestroy()
    {
        if( Instance == this )
        {
            Instance    = null;
        }
    }

} // class Singleton

f:id:urahimono:20170725073919p:plain

 これでSingletonは迷わず成仏したと思われます。
 もしシーンをまたいで使用する場合には、TestController同様DontDestroyOnLoad()が必要です。
 そもそもUpdate()などのMonoBehaviourの機能を使わずに、変数などの値のみを保持し続けたいのなら、MonoBehaviourを継承する必要すらないはずだ。

public class Singleton
{
    public int Number
    {
        get;
        set;
    }

    private Singleton()
    {
    }

    public static Singleton Instance
    {
        get;
        private set;
    }

    public static void CreateInstance()
    {
        if( Instance == null )
        {
            Instance = new Singleton();
        }
    }

    public static void DestroyInstance()
    {
        Instance    = null;
    }

} // class Singleton

 この場合はシーンを切り替えても生死に影響はありません。
 まあ、保持する変数タイプによっては、Dispose()などの設計も考えなくてはいけませんが。

 生成、破棄のタイミングすらいらないのなら、staticクラスを使う選択肢もあるかもしれませんよ。

public static class Singleton
{
    public static int Number
    {
        get;
        set;
    }
} // class Singleton

感想

 gameObjectUpdate()などにはアクセスできなくても、クラス独自の変数の値や関数は呼び出せる。
 こんなお化けみたいな状態で開発を続けることは、極めて怖いことです。
 お気をつけください。

 この世のすべてのバグが成仏しますように。