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

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

【Unity】OnLevelWasLoadedとSceneManager

 新しいものが生まれれば、古いものは捨てられていくものです。
 Unityの機能も同様です。
 新しい機能が追加される傍ら、古い機能が使えなくなったり、存在そのものがなくなったりします。

 今回はUnity5.4で新しく追加されたSceneManager.sceneLoadedと旧機能であるOnLevelWasLoadedについてのお話です。


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

さよなら、OnLevelWasLoaded

 シーンが切り替わった際をコンポーネントが検知するにはどうすればいいでしょうか。
 そんなときはOnLevelWasLoaded()の関数をコンポーネントに記述すればいいのです。

http://docs.unity3d.com/ja/current/ScriptReference/MonoBehaviour.OnLevelWasLoaded.htmldocs.unity3d.com

 Awake()Update()などと同様にMonoBehaviourクラスを継承したクラスならば、関数があるだけでシーンが切り替えた際に通知されます。
 簡単に組み込むことが出来てとっても便利です。

 さてそれをUnity5.4で使用しようとするとこんなメッセージがConsoleウィンドウに表示されます。

f:id:urahimono:20160901195856p:plain

OnLevelWasLoaded was found on DemoScene
This message has been deprecated and will be removed in a later version of Unity.
Add a delegate to SceneManager.sceneLoaded instead to get notifications after scene loading has completed

 いずれこの関数を削除するからもう使うなってさ!
 事実、Unity5.4のドキュメントのMonoBehaviourの項目からOnLevelWasLoadedはなくなっていました。
 
f:id:urahimono:20160901195910p:plain  
 今までお疲れ!
 
 ちなみに現時点では警告文こそ出ますが、OnLevelWasLoadedのメッセージは飛んできますので使用することは出来ます。

新たな希望、SceneManager.sceneLoaded

 ではこれからはシーンの切り替えはどのように検知すればよいのでしょうか。
 それはUnity5.3からシーン周りの処理を担当することになったSceneManagerが解決してくれました。

docs.unity3d.com

 Unity5.4からSceneManagerには以下のデリゲートが追加されました。これらに登録することでシーンが切り替わったら読み込み終わったりが判断できますよ。

 ちなみに現時点ではドキュメントに記述されている情報が少なかったため、ちょっと補足です。

  • activeSceneChanged
    宣言は、public static event UnityAction<Scene, Scene> activeSceneChanged;
    アクティブになっているシーンが切り替わったら呼ばれます。
    第1引数に切り替わる前にアクティブだったシーンが代入されています。
    第2引数に切り替えた後にアクティブになったシーンが代入されています。
  • sceneLoaded
    宣言は、public static event UnityAction<Scene, LoadSceneMode> sceneLoaded;
    シーンのロードが終わったら呼ばれます。
    第1引数にロードされたシーンが代入されています。
    第2引数にシーンの読み込みタイプ(Single or Additive)が代入されています。
  • sceneUnloaded
    宣言は、public static event UnityAction<Scene> sceneUnloaded;
    シーンのアンロードが終わったら呼ばれます。
    第1引数にアンロードされたシーンが代入されています。

実際に使ってみよう

 では早速以下のスクリプトを使って挙動を確認して見ましょう。

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

public class DemoScene : MonoBehaviour
{
    void Awake()
    {
        SceneManager.activeSceneChanged += OnActiveSceneChanged;
        SceneManager.sceneLoaded        += OnSceneLoaded;
        SceneManager.sceneUnloaded      += OnSceneUnloaded;
        DontDestroyOnLoad( gameObject );
    }

    void Start()
    {
        StartCoroutine( LoadSceneProcess() );
    }

//         void OnLevelWasLoaded( int i_level )
//         {
//             Debug.LogFormat( "OnLevelWasLoaded() {0}", i_level );
//         }

    void OnActiveSceneChanged( Scene i_preChangedScene, Scene i_postChangedScene )
    {
        Debug.LogFormat( "OnActiveSceneChanged() preChangedScene:{0} postChangedScene:{1}", i_preChangedScene.name, i_postChangedScene.name );
    }

    void OnSceneLoaded( Scene i_loadedScene, LoadSceneMode i_mode )
    {
        Debug.LogFormat( "OnSceneLoaded() current:{0} loadedScene:{1} mode:{2}", SceneManager.GetActiveScene().name, i_loadedScene.name, i_mode );
    }

    void OnSceneUnloaded( Scene i_unloaded )
    {
        Debug.LogFormat( "OnSceneUnloaded() current:{0} unloaded:{1}", SceneManager.GetActiveScene().name, i_unloaded.name );
    }


    IEnumerator LoadSceneProcess()
    {
        while( true )
        {
            yield return new WaitForSeconds( 3.0f );

            SceneManager.LoadScene( "SceneB" );

            yield return new WaitForSeconds( 3.0f );

            SceneManager.LoadScene( "SceneC" );
        }
    }

} // class DemoScene

f:id:urahimono:20160901195944p:plain

 SceneAのGameObjectに上記のDemoSceneをアタッチしてSceneBSceneCに遷移させています。

 ログを見て面白いのは、ゲーム開始はSceneAから始まるためOnSceneLoaded()が呼ばれるのは分かるのですが、第2引数に4が代入されていることです。
 sceneLoadedの第2引数の型であるLoadSceneModeにはSingleAdditiveの2つしかないため4という数字が何を表しているのかが不明です。

docs.unity3d.com

 そして、OnSceneUnloadedが呼ばれるタイミングはアンロードされるシーン中であるところも興味深いですね。

 DemoSceneを改良してもう少し見て見ましょう。
 今度はSceneManager.LoadScene()LoadSceneMode.Additiveで呼んでみましょう。

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

public class DemoScene : MonoBehaviour
{
    void Awake()
    {
        SceneManager.activeSceneChanged += OnActiveSceneChanged;
        SceneManager.sceneLoaded        += OnSceneLoaded;
        SceneManager.sceneUnloaded      += OnSceneUnloaded;
        DontDestroyOnLoad( gameObject );
    }

    void Start()
    {
        StartCoroutine( LoadSceneProcess() );
    }

//         void OnLevelWasLoaded( int i_level )
//         {
//             Debug.LogFormat( "OnLevelWasLoaded() {0}", i_level );
//         }

    void OnActiveSceneChanged( Scene i_preChangedScene, Scene i_postChangedScene )
    {
        Debug.LogFormat( "OnActiveSceneChanged() preChangedScene:{0} postChangedScene:{1}", i_preChangedScene.name, i_postChangedScene.name );
    }

    void OnSceneLoaded( Scene i_loadedScene, LoadSceneMode i_mode )
    {
        Debug.LogFormat( "OnSceneLoaded() current:{0} loadedScene:{1} mode:{2}", SceneManager.GetActiveScene().name, i_loadedScene.name, i_mode );
    }

    void OnSceneUnloaded( Scene i_unloaded )
    {
        Debug.LogFormat( "OnSceneUnloaded() current:{0} unloaded:{1}", SceneManager.GetActiveScene().name, i_unloaded.name );
    }


    IEnumerator LoadSceneProcess()
    {
        while( true )
        {
            yield return new WaitForSeconds( 3.0f );

            SceneManager.LoadScene( "SceneB", LoadSceneMode.Additive );

            yield return new WaitForSeconds( 3.0f );

            SceneManager.LoadScene( "SceneC", LoadSceneMode.Additive );

            yield return new WaitForSeconds( 3.0f );

            SceneManager.SetActiveScene( SceneManager.GetSceneByName( "SceneC" ) );

            yield return new WaitForSeconds( 3.0f );

            SceneManager.LoadScene( "SceneD", LoadSceneMode.Single );
        }
    }

} // class DemoScene

f:id:urahimono:20160901195955p:plain

 ログから複数のシーンがある状態で、別のシーンへLoadSceneMode.Singleで読み込むと、今まであるシーンの全てでOnSceneUnloaded()が呼ばれていることが分かります。

 では次に非同期読み込みならどうでしょうか。
 SceneManager.LoadSceneAsync()で見てみましょう。

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

public class DemoScene : MonoBehaviour
{
    void Awake()
    {
        SceneManager.activeSceneChanged += OnActiveSceneChanged;
        SceneManager.sceneLoaded        += OnSceneLoaded;
        SceneManager.sceneUnloaded      += OnSceneUnloaded;
        DontDestroyOnLoad( gameObject );
    }

    void Start()
    {
        StartCoroutine( LoadSceneProcess() );
    }

//         void OnLevelWasLoaded( int i_level )
//         {
//             Debug.LogFormat( "OnLevelWasLoaded() {0}", i_level );
//         }

    void OnActiveSceneChanged( Scene i_preChangedScene, Scene i_postChangedScene )
    {
        Debug.LogFormat( "OnActiveSceneChanged() preChangedScene:{0} postChangedScene:{1}", i_preChangedScene.name, i_postChangedScene.name );
    }

    void OnSceneLoaded( Scene i_loadedScene, LoadSceneMode i_mode )
    {
        Debug.LogFormat( "OnSceneLoaded() current:{0} loadedScene:{1} mode:{2}", SceneManager.GetActiveScene().name, i_loadedScene.name, i_mode );
    }

    void OnSceneUnloaded( Scene i_unloaded )
    {
        Debug.LogFormat( "OnSceneUnloaded() current:{0} unloaded:{1}", SceneManager.GetActiveScene().name, i_unloaded.name );
    }

    IEnumerator LoadSceneProcess()
    {
        while( true )
        {
            yield return new WaitForSeconds( 3.0f );

            yield return SceneManager.LoadSceneAsync( "SceneB" );

            yield return new WaitForSeconds( 3.0f );

            yield return SceneManager.LoadSceneAsync( "SceneC" );
        }
    }

} // class DemoScene

f:id:urahimono:20160901200006p:plain

 SceneManager.LoadSceneSceneManager.LoadSceneAsyncOnSceneLoadedの呼ばれ方が同じことが分かります。

 更にallowSceneActivation = falseにして非同期の読み込みを見てみます。

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

public class DemoScene : MonoBehaviour
{
    void Awake()
    {
        SceneManager.activeSceneChanged += OnActiveSceneChanged;
        SceneManager.sceneLoaded        += OnSceneLoaded;
        SceneManager.sceneUnloaded      += OnSceneUnloaded;
        DontDestroyOnLoad( gameObject );
    }

    void Start()
    {
        StartCoroutine( LoadSceneProcess() );
    }

//         void OnLevelWasLoaded( int i_level )
//         {
//             Debug.LogFormat( "OnLevelWasLoaded() {0}", i_level );
//         }

    void OnActiveSceneChanged( Scene i_preChangedScene, Scene i_postChangedScene )
    {
        Debug.LogFormat( "OnActiveSceneChanged() preChangedScene:{0} postChangedScene:{1}", i_preChangedScene.name, i_postChangedScene.name );
    }

    void OnSceneLoaded( Scene i_loadedScene, LoadSceneMode i_mode )
    {
        Debug.LogFormat( "OnSceneLoaded() current:{0} loadedScene:{1} mode:{2}", SceneManager.GetActiveScene().name, i_loadedScene.name, i_mode );
    }

    void OnSceneUnloaded( Scene i_unloaded )
    {
        Debug.LogFormat( "OnSceneUnloaded() current:{0} unloaded:{1}", SceneManager.GetActiveScene().name, i_unloaded.name );
    }


    IEnumerator LoadSceneProcess()
    {
        while( true )
        {
            yield return new WaitForSeconds( 3.0f );

            {
                AsyncOperation async       = SceneManager.LoadSceneAsync( "SceneB" );
                async.allowSceneActivation = false;

                // isDoneでは判断できませんでした。
                // yield return new WaitWhile( () => !async.isDone );

                // 1.0f にはなりませんでした
                // yield return new WaitWhile( () => async.progress < 1.0f );

                yield return new WaitWhile( () => async.progress < 0.9f );

                async.allowSceneActivation  = true;
            }

            yield return new WaitForSeconds( 3.0f );

            {
                AsyncOperation async       = SceneManager.LoadSceneAsync( "SceneC" );
                async.allowSceneActivation = false;

                // isDoneでは判断できませんでした。
                // yield return new WaitWhile( () => !async.isDone );

                // 1.0f にはなりませんでした
                // yield return new WaitWhile( () => async.progress < 1.0f );

                yield return new WaitWhile( () => async.progress < 0.9f );

                async.allowSceneActivation  = true;
            }
        }
    }

} // class DemoScene

f:id:urahimono:20160901200120p:plain

 isDoneがtrueにならないことやprogressが1.0にならないなど既存の不具合?はありますが、ログを見る限り挙動は同じのようです。

 SceneManagerのデリゲートになったことにより、今までMonoBehaviourを継承したクラスでなければならなかったところが、全てのクラスで使用できるようになったり、OnLevelWasLoaded()ではシーン番号しか取得できなかったところが、Scene構造体になったためより多くの情報が取得できるようになったりと利点は多いです。
 ただ、各デリゲートに登録した関数は、削除時に必ずデリゲートから解除するのを忘れないようにしなくてはいけませんね。

 この機能を使って、たくさんのシーンを切り替えまくるゲームを作りたいものです。