もう夏本番になってきましたね。
というわけで、今回はちょっと怖い話をしていこうと思います。
これは友達の友達から聞いた話なんですが……。
この記事には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
世界はこのような作りになっていました。
ゲーム制御用のクラスであるTestController
は、シーンの切り替えと友人であるSingleton
のNumber
プロパティの値をログとして表示することしかできません。
TestController
はDontDestroyOnLoad()
を設定しているので、シーンが切り替わっても消えません。
Singleton
はDontDestroyOnLoad()
を設定していないので、シーンが切り替わったら消えてしまいます。
そのためTestController
はシーン切り替え後にはSingleton
に話しかけることはできないはずです。
シーンが切り替わればSingleton
は死んでしまうのですから。
きっとエラーが発生するに違いありません。
不滅
さあ、どうなるでしょう。
SceneA num=0
SceneB num=0
なんということでしょう。
Hierarchy上にいないはずのSingleton
にTestController
が話しかけています。
どういうことなのでしょう。
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
SceneA Instance=Singleton (Singleton)
SceneB Instance=null
やはり、Singleton
はnull
になっている!
きゃあああ、お化けだ!
……あれっ、null
の時ってちゃんとnullと表示されるんだっけ。
デバッガでみてみましょう。
んー、Singleton.Awake()
の時のInstance
がnull
のときと、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
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
SceneBではSingleton.Update()
は呼ばれていません。
gameObject
やUpdate()
のような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
これで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
感想
gameObject
やUpdate()
などにはアクセスできなくても、クラス独自の変数の値や関数は呼び出せる。
こんなお化けみたいな状態で開発を続けることは、極めて怖いことです。
お気をつけください。
この世のすべてのバグが成仏しますように。