うら干物書き

ゲームを作っています。

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

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