シーンを管理するシステムを作りたいのですが、それにはまずUnityのSceneManager
について詳しく知っておきたいものです。
ドキュメントを見る限り、LoadScene()
ぐらいしか主に使ってないですね。
せっかくなのでこの機会に、SceneManager
で遊びつくしましょう。
この記事にはUnity5.6.0f3を使用しています。
- Get関数と取得するSceneデータについて。
- GetSceneByName()
- GetSceneByPath()
- GetSceneAt()
- GetSceneByBuildIndex()
- GetActiveScene()
- SetActiveScene
- LoadScene()
- LoadSceneAsync()
- UnloadSceneAsync()
- CreateScene()
- MoveGameObjectToScene()
- MergeScenes()
- Events
- SceneUtility
- 調査結果その後
Get関数と取得するSceneデータについて。
ドキュメントを見ると、Get○○関数が多いので、まずそれらから調べていきましょう。
ちなみにこの、Get○○関数で取得するUnityEngine.SceneManagement.Scene
は構造体。クラスではなく構造体。
値型のためにnull
にはならなかったりします。
じゃあ、Get○○関数でScene
データの取得に成否の判断はどうすればいいんだ。
「私、失敗しないので」とでも言う気なのだろうか。
その点は無問題。
Scene
にはIsValid()
という関数があります。これで有効なScene
情報か判断していきましょう。
GetSceneByName()
SceneManager に追加されているシーンの中から、指定した名前のシーンを検索します。
指定した名前のシーン情報を取ってくる関数のようですね。
早速以下の条件で使ってみます。
using UnityEngine; using System.Collections; using UnityEngine.SceneManagement; public class TestController : MonoBehaviour { private void Awake() { // シーンを切り替えていくから、俺は死ぬわけにはいかない! DontDestroyOnLoad( gameObject ); } private IEnumerator Start() { Scene sceneA = SceneManager.GetSceneByName( "SceneA" ); Debug.LogFormat( "sceneA ={0}", sceneA.IsValid() ); Scene sceneB = SceneManager.GetSceneByName( "SceneB" ); Debug.LogFormat( "sceneB ={0}", sceneB.IsValid() ); Scene sceneX = SceneManager.GetSceneByName( "SceneX" ); Debug.LogFormat( "sceneX ={0}", sceneX.IsValid() ); yield return null; } } // class TestController
取得に成功したのは、sceneAだけですねー。
SceneBはAssetとしては存在するが、Hierarchy上に存在しないので、情報の取得は出来ない。
SceneXはAssetとしてもHierarchy上にも存在しないので、情報の取得は出来ない。
そしてBuildSettgingsにシーンが登録されているかどうかは、取得に影響しない。
GetScene○○シリーズ中でも一番使いそう。
GetSceneByPath()
SceneManager に追加されていて、指定したアセットパスを持つすべてのシーンを検索します。
今度はパスを指定して取得するタイプのようですね。
private IEnumerator Start() { Scene sceneA1 = SceneManager.GetSceneByPath( "Scenes/SceneA" ); Debug.LogFormat( "sceneA1 ={0}", sceneA1.IsValid() ); Scene sceneA2 = SceneManager.GetSceneByPath( "Assets/Scenes/SceneA" ); Debug.LogFormat( "sceneA2 ={0}", sceneA2.IsValid() ); Scene sceneA3 = SceneManager.GetSceneByPath( "Assets/Scenes/SceneA.unity" ); Debug.LogFormat( "sceneA3 ={0}", sceneA3.IsValid() ); Scene sceneB = SceneManager.GetSceneByPath( "Assets/Scenes/SceneB.unity" ); Debug.LogFormat( "sceneB ={0}", sceneB.IsValid() ); Scene sceneX = SceneManager.GetSceneByPath( "Assets/Scenes/SceneX.unity" ); Debug.LogFormat( "sceneX ={0}", sceneX.IsValid() ); yield return null; }
取得に成功したのは、sceneA3のみですね。
GetSceneByPath()
に渡す引数は、プロジェクトフォルダーの相対パスである必要があるみたいですね。
そのため、Assetsから記述しなくてはならないようです。めんどくさ……。
SceneA1はパスがAssetsから開始していないので、情報の取得は出来ない。
SceneA2はパスは正しいが、拡張子(.unity)がないため、情報の取得は出来ない。
SceneBはパスは正しいが、Hierarchy上に存在しないので、情報の取得は出来ない。
SceneXはAssetとしてもHierarchy上にも存在しないので、情報の取得は出来ない。
そして今回も、BuildSettgingsにシーンが登録されているかどうかは、取得に影響しない。
実際に使う際もAssets/と拡張子の書き忘れは多発しそう……。
GetSceneAt()
SceneManager の追加されたシーンのリストから指定したインデックスのシーンを取得します。
今度はインデックスを指定して取得するタイプですね。
private IEnumerator Start() { for( int i = 0; i < 5; ++i ) { Scene scene = SceneManager.GetSceneAt( i ); Debug.LogFormat( "scene{0} = {1}, name = {2}", i, scene.IsValid(), scene.name ); } yield return null; }
当たり前かもしれませんが、GetSceneAt()
に現在あるシーン数以上の数値を入れると例外が発生しました。
IndexOutOfRangeException: Scene index "2" is out of range.
private IEnumerator Start() { Scene scene = SceneManager.GetSceneAt( -1 ); Debug.LogFormat( "scene{0} = {1}, name = {2}", -1, scene.IsValid(), scene.name ); yield return null; }
そして、GetSceneAt()
に負の数値を入れても例外が発生しました。
そんなことする人いないと思いますけど……。
IndexOutOfRangeException: Scene index "-1" is out of range.
一応確認のため、エディタ上でシーンの並び方を変えてみましょう。
SceneAをアクティブなシーンにしたままの状態で、SceneBをHierarchy上で上の方に配置します。
結果、0がSceneBで、1がSceneAになる。
エディタ上の並びがインデックスの順になっているようですね。
読み込んだり、破棄したりを繰り返していると、そのシーンが何番目にあるかなんてスクリプト上で把握するのは難しそうですね。
ピンポイントでこのインデックスのシーンが欲しい、というよりも、今存在する全てのシーンの情報が欲しい、というときに使えそうですね。
GetSceneByBuildIndex()
えーと、今度はBuildSettgingsのインデックスから取得するタイプのようですね。
現在、日本語ドキュメントは5.4までしかなく、そこにはまだないようなので、新しい関数なのかな。
private IEnumerator Start() { for( int i = 0; i < 5; ++i ) { Scene scene = SceneManager.GetSceneByBuildIndex( i ); Debug.LogFormat( "scene{0} = {1}, name = {2}", i, scene.IsValid(), scene.name ); } yield return null; }
わざわざ調べる必要はないかとは思いましたが、一応GetSceneAt()
と同様に、BuildSettgingsに現在あるシーン数以上の数値を入れると例外が発生。負数も同様。
ArgumentException: GetSceneByBuildIndex: Invalid build index: 1
ふーむ、ただ妙だな。
そもそもBuildSettgingsには何のシーンも登録していないんだけどな。
だから0もダメだと思うんだけど。
一度SceneBをBuildSettgingsに登録してみましょう。
この状態で同じ検証をもう一度行います。
うーむ、なんだこの結果は。
BuildSettgingsの0番に登録したSceneBは、Hierarchy上に存在しないので、情報の取得は出来ない。
BuildSettgingsに登録していないが、Hierarchy上に存在するSceneAは、1番に登録されている。
なんでさ!
さらにもう少し検証が必要なようだ。
private IEnumerator Start() { Debug.LogFormat( "sceneCountInBuildSettings = {0}", SceneManager.sceneCountInBuildSettings ); for( int i = 0; i < SceneManager.sceneCountInBuildSettings; ++i ) { Scene scene = SceneManager.GetSceneByBuildIndex( i ); Debug.LogFormat( "scene{0} = {1}, name = {2}", i, scene.IsValid(), scene.name ); } yield return null; }
Build Settingsのシーン数を取得するsceneCountInBuildSettings
は2を返しよった。
どうやら、BuildSettgingsに登録していないが、Hierarchy上に存在するシーンはBuild Settingsに連番で登録されるみたいだね。
エディタで実行した際のシーンによって、Build Settingsに影響があるのだね。
意図的に使うことはないだろうけど、この挙動は覚えておこう。
GetActiveScene()
現在アクティブなシーンを取得します。
以下の画像の状態なら、SceneAの情報が取得できました。
アクティブなシーンってなんぞ
ただ、なんなのさ。アクティブなシーンって!
new GameObject()
やGameObject.Instantiate()
などでオブジェクトを作った際に配置されるのが、アクティブなシーンだ、ぐらいしか知らないなぁ。
テラシュールブログさんの記事では、ライトマップやナビメッシュなどにも関係があるそうみたいですな。
アクティブなシーンが無い状態ってあり得るの?
あ、あるのかな。アクティブなシーンが無い状態なんて。
試してみよう!
private IEnumerator Start() { yield return new WaitForSeconds( 2.0f ); while( SceneManager.sceneCount > 0 ) { Scene scene = SceneManager.GetActiveScene(); Debug.LogFormat( "ActiveScene = {0}", scene.name ); yield return SceneManager.UnloadSceneAsync( scene ); yield return new WaitForSeconds( 0.5f ); } yield return null; }
SceneBが消えない……。
ログを見てみましょう。
Unloading the last loaded scene Assets/Scenes/SceneB.unity(build index: 0), is not supported. Please use.
なるほど、UnloadSceneAsync()
が失敗しておる。
どうやらシーンが無い状態にすることは出来ないようですね。
そのため、最低でも1つのシーンがあり、そのシーンはアクティブなシーンになるため、アクティブなシーンが無い状態はありえないみたい。
SetActiveScene
シーンをアクティブにします。
関数名そのまんまの挙動。
渡す引数が有効なScene情報でないと、返り値falseが返って失敗するようだね。
LoadScene()
Build Settings の名前かインデックスでシーンを読み込みます。
この関数は結構使ってきているから、ある程度挙動はわかっているつもりですが一応調べておきます。
private IEnumerator Start() { SceneManager.LoadScene( "SceneB" ); yield return null; }
BuildSettgingsに登録されていないシーン名を指定すると例外が発生します。
Scene 'SceneB' couldn't be loaded because it has not been added to the build settings or the AssetBundle has not been loaded.
To add a scene to the build settings use the menu File->Build Settings...
BuildSettgingsに無い番号や負数を指定しても同様です。
Scene with build index: 1 couldn't be loaded because it has not been added to the build settings.
To add a scene to the build settings use the menu File->Build Settings...
まあ、でしょうね。
そして、第二引数にシーンの読み込み方式を決めることが出来ます。
docs.unity3d.com
private IEnumerator Start() { yield return new WaitForSeconds( 2.0f ); SceneManager.LoadScene( "SceneB", LoadSceneMode.Additive ); yield return new WaitForSeconds( 2.0f ); SceneManager.LoadScene( "SceneC", LoadSceneMode.Single ); yield return null; }
さて、ここからが、くえすちょんだ!
既にHierarchy上にあるシーンは読み込めるのか
出来るのかな。出来ていいのかもわかんないけど。
private IEnumerator Start() { while( true ) { yield return new WaitForSeconds( 2.0f ); SceneManager.LoadScene( "SceneB", LoadSceneMode.Single ); } }
再読み込みが走ってますね。gifだとわかりにくいけど。
private IEnumerator Start() { while( true ) { yield return new WaitForSeconds( 1.0f ); SceneManager.LoadScene( "SceneB", LoadSceneMode.Additive ); } }
Additive
だと動きがわかりやすい。
気持ち悪いぐらいシーンが増えていってる。
ただ、出来るとはいえ実際やっていいのかと言えば、正直ちょっとお勧めできそうにないですな。
なぜなら、同名のシーンが複数存在する場合、GetSceneByName()
などでシーン情報を取得するのが極めて難しくなってしまう。
同じ名前だから、どれがどれなのかわからない。大混乱だ!
結論、既にHierarchy上にあるシーンも読み込める。
ただし、GetScene系でシーン情報取得が難しくなる。
最初にHierarchy上にあるシーンとBuildSettgings
GetSceneByBuildIndex()
を調べた際に、実行時にHierarchy上にあるシーンは自動でBuildSettgingsに登録されてました。
そのシーンはLoadScene()
出来るのだろうか。
private IEnumerator Start() { while( true ) { yield return new WaitForSeconds( 1.0f ); SceneManager.LoadScene( "SceneA", LoadSceneMode.Additive ); } }
出来た! 出来よった!
ただ、ビルドを作成した際は、BuildSettgingsの0番からロードされ開始されるので、エディタ上でのみ有効な手段。
使うタイミングはあるのだろうか。
結論、実行時にHierarchy上にあるシーンはBuildSettgingsに登録されていなくても読み込める。
ただし、エディタ時のみしか使えないため、あんまり意味がない。
LoadSceneAsync()
バックグラウンドで非同期的にシーンを読み込みます。
LoadScene()
の非同期版。
返り値のAsyncOperation
はそのままコルーチンでyield return
で使うことが出来ますぜ。
private IEnumerator Start() { yield return SceneManager.LoadSceneAsync( "SceneB", LoadSceneMode.Additive ); }
AsyncOperation
で気を付けたいのは、allowSceneActivation
の存在だ。
allowSceneActivation
をfalse
にすることで、シーンを読み込み終わった後、すぐにシーンが有効にならなくなります。
上手く使うことで、シーンが有効になるタイミングを任意で制御できるようになるのだけれど、allowSceneActivation
がfalse
の場合、AsyncOperation
のisDone
はfalse
のままで、progress
は0.9で止まるので注意。
これは仕様。
もっかい言っときます。これは仕様。
ドキュメントにも書かれますからねー。
どうやら、読み込んだシーンが有効にされるまでがAsyncOperation
の完了を意味するようですな。
UnloadSceneAsync()
非同期によるシーンの破棄。
Unity5.4ぐらいまではUnloadScene()
だったけど、今ではシーンの破棄は非同期のみとなっているみたい。
すでにUnloadScene()
はObsolete
扱いになっており、使用が推奨されるものではなくなっています。
使っている人は切り替えよう!
GetActiveScene()
の時に調べたけど、最低でもシーンは一つ以上存在する必要があるため、最後のシーンを破棄しようとすると以下の警告文が出て失敗します。
Unloading the last loaded scene Assets/Scenes/SceneB.unity(build index: 0), is not supported. Please use.
ちなみに、現在読み込まれていないシーンをアンロードすると以下の例外が発生しました。
ArgumentException: Scene to unload is invalid.
CreateScene()
ランタイムに指定した名前で新しい空のシーンを作成します。
ほー、ランタイム中にシーンが作れるとは面妖な。
ちなみに空の文字列を渡すと以下の例外が発生。
ArgumentException: The input scene name cannot be empty.
既にHierarchy上にあるシーン名と同じ文字列を渡しても例外。
ArgumentException: Scene with name "SceneA" already exists.
まあ、ドキュメントにもそんな文字列は入れんなって言ってますから入れんでください。
ではこの場合はどうだろう。
BuildSettgingsに登録されているシーンの名前をCreateScene()
に渡す場合。
private IEnumerator Start() { SceneManager.CreateScene( "SceneB" ); SceneManager.LoadSceneAsync( "SceneB", LoadSceneMode.Additive ); Debug.LogFormat( "sceneCountInBuildSettings = {0}", SceneManager.sceneCountInBuildSettings ); yield return null; }
どうやら、Hierarchy上にさえ無ければCreateScene()
にBuildSettgingsに登録されている同名のシーン名を渡すことは可能であるみたいです。
この後に、SceneManager.CreateScene( "SceneB" );
を呼ぶと、上記の例外が発生してしまいました。
ちなみにこの後にSceneManager.sceneCountInBuildSettings
の数を調べてみたけど、増えてなかった。
CreateScene()
を使っても、SceneManager.sceneCountInBuildSettings
は増えないみたい。
MoveGameObjectToScene()
現在のシーンから新しいシーンにゲームオブジェクトを移動します。 ルートオブジェクトだけをあるシーンから他のシーンへ移動できます。つまり、移動するゲームオブジェクトは、シーンのゲームオブジェクトの子であってはなりません。
オブジェクトをシーンをまたいで移動する関数のようですね。
注釈に「移動するゲームオブジェクトは、シーンのゲームオブジェクトの子であってはなりません。」と書かれてありますね。
もちろんやってみます。
private IEnumerator Start() { yield return SceneManager.LoadSceneAsync( "SceneB", LoadSceneMode.Additive ); Scene sceneB = SceneManager.GetSceneByName( "SceneB" ); SceneManager.MoveGameObjectToScene( m_objectB, sceneB ); yield return null; }
ArgumentException: Gameobject is not a root in a scene.
「やらないでください」と注釈に書かれている以上、やったら当然例外が出ます。
ルートにあるObjectAをを引数に渡した場合は、ちゃんと移動しました。
上記でも紹介したテラシュールブログさんの記事で書かれていますが、シーンのアクティブを切り替えを頻繁に行うのはよくないようなので、オブジェクトの生成するシーンを変更する場合には、MoveGameObjectToScene()
を使ったほうがよさそうですね。
MergeScenes()
sourceScene を destinationScene にマージします。
注釈に「危険」と書かれているとは楽しそうな関数。
この関数は sourceScene のコンテンツを destinationScene にマージし、 sourceScene を削除します。 sourceScene のルートにあるすべてのゲームオブジェクトは destinationScene のルートに移動します。 注意: souceScene はマージが完了すると破棄されます。そのため、この関数は使い方を間違えると危険です
さっそく遊んでみる。
using UnityEngine; using System.Collections; using UnityEngine.SceneManagement; public class TestController : MonoBehaviour { [SerializeField] private GameObject m_objectA = null; [SerializeField] private GameObject m_objectB = null; private void Awake() { // シーンを切り替えていくから、俺は死ぬわけにはいかない! DontDestroyOnLoad( gameObject ); } private IEnumerator Start() { Scene sceneA = SceneManager.GetSceneByName( "SceneA" ); yield return new WaitForSeconds( 1.0f ); yield return SceneManager.LoadSceneAsync( "SceneB", LoadSceneMode.Additive ); Scene sceneB = SceneManager.GetSceneByName( "SceneB" ); yield return new WaitForSeconds( 1.0f ); SceneManager.MergeScenes( sceneB, sceneA ); yield return new WaitForSeconds( 1.0f ); yield return SceneManager.LoadSceneAsync( "SceneC", LoadSceneMode.Additive ); Scene sceneC = SceneManager.GetSceneByName( "SceneC" ); yield return new WaitForSeconds( 1.0f ); SceneManager.MergeScenes( sceneC, sceneA ); yield return new WaitForSeconds( 1.0f ); SceneManager.CreateScene( "SceneX" ); Scene sceneX = SceneManager.GetSceneByName( "SceneX" ); yield return new WaitForSeconds( 1.0f ); SceneManager.MergeScenes( sceneA, sceneX ); yield return null; } } // class TestController
マージされるシーンが次々に無くなっていっていますね。
見てる分には面白いですが、ちゃんと管理しないと面倒なことになりそうですね。
Events
- activeSceneChanged:アクティブなシーンが変更時のイベント
- sceneLoaded:シーンが読み込まれた際のイベント
- sceneUnloaded:シーンが解放された際のイベント
この三種のイベント関連については、以前調べましたな。そういえば。
一応、バージョンも更新しましたし確認しておきましょうか。
using UnityEngine; using System.Collections; using UnityEngine.SceneManagement; public class TestController : MonoBehaviour { private void Awake() { // シーンを切り替えていくから、俺は死ぬわけにはいかない! DontDestroyOnLoad( gameObject ); SceneManager.activeSceneChanged += OnActiveSceneChanged; SceneManager.sceneLoaded += OnSceneLoaded; SceneManager.sceneUnloaded += OnSceneUnloaded; } private IEnumerator Start() { yield return new WaitForSeconds( 1.0f ); yield return SceneManager.LoadSceneAsync( "SceneB", LoadSceneMode.Additive ); yield return new WaitForSeconds( 1.0f ); yield return SceneManager.LoadSceneAsync( "SceneC", LoadSceneMode.Single ); yield return new WaitForSeconds( 1.0f ); SceneManager.CreateScene( "SceneX" ); yield return new WaitForSeconds( 1.0f ); Scene sceneC = SceneManager.GetSceneByName( "SceneC" ); Scene sceneX = SceneManager.GetSceneByName( "SceneX" ); SceneManager.MergeScenes( sceneC, sceneX ); yield return null; } private void OnActiveSceneChanged( Scene i_preChangedScene, Scene i_postChangedScene ) { Debug.LogFormat( "OnActiveSceneChanged() preChangedScene:{0} postChangedScene:{1}", i_preChangedScene.name, i_postChangedScene.name ); } private 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 ); } private void OnSceneUnloaded( Scene i_unloadedScene ) { Debug.LogFormat( "OnSceneUnloaded() current:{0} unloaded:{1}", SceneManager.GetActiveScene().name, i_unloadedScene.name ); } } // class TestController
ログの結果として、
activeSceneChanged
:SceneAsceneLoaded
:SceneASceneManager.LoadSceneAsync( "SceneB", LoadSceneMode.Additive );
sceneLoaded
:SceneBSceneManager.LoadSceneAsync( "SceneC", LoadSceneMode.Single );
sceneUnloaded
:SceneAsceneUnloaded
:SceneBactiveSceneChanged
:SceneCsceneLoaded
:SceneCSceneManager.MergeScenes( sceneC, sceneX );
activeSceneChanged
:SceneXsceneUnloaded
:SceneC
まとめると、
- 最初にHierarchy上にあるものは、
activeSceneChanged
,sceneLoaded
の順で呼ばれる。 SceneManager.LoadScene()
をLoadSceneMode.Single
で読み込む場合は、現在あるシーンのsceneUnloaded
,新しいシーンのactiveSceneChanged
,sceneLoaded
の順で呼ばれる。SceneManager.CreateScene()
はシーンを作成したのであって、シーンを読み込んではいないため、sceneUnloaded
は呼ばれない。- アクティブのシーンがマージされる場合は、マージシーンの
activeSceneChanged
,マージされたシーンのsceneUnloaded
の順で呼ばれる。
SceneUtility
なんか新しいクラスが出来てますね。
Sceneを扱う際の便利関数ですかね。
二つしかないようですが……。
- GetBuildIndexByScenePath()
- GetScenePathByBuildIndex()
GetBuildIndexByScenePath()
はシーンのパスからそれがBuildSettgingsで何番目か調べる関数みたいですね。
パスの指定はGetSceneByPath()
と同じ。
private IEnumerator Start() { int sceneAIndex = SceneUtility.GetBuildIndexByScenePath( "Assets/Scenes/SceneA.unity" ); Debug.LogFormat( "sceneAIndex = {0}", sceneAIndex ); int sceneBIndex = SceneUtility.GetBuildIndexByScenePath( "Assets/Scenes/SceneB.unity" ); Debug.LogFormat( "sceneBIndex = {0}", sceneBIndex ); int sceneCBIndex = SceneUtility.GetBuildIndexByScenePath( "Assets/Scenes/SceneC.unity" ); Debug.LogFormat( "sceneCIndex = {0}", sceneCBIndex ); yield return null; }
sceneAIndex = 1
sceneBIndex = 0
sceneCIndex = -1
パスが誤っていたり、BuildSettgingsにないものだと、-1が返るようです。
GetScenePathByBuildIndex()
はその逆。
private IEnumerator Start() { string scene0 = SceneUtility.GetScenePathByBuildIndex( 0 ); Debug.LogFormat( "scene0 = {0}", scene0 ); string scene2 = SceneUtility.GetScenePathByBuildIndex( 2 ); Debug.LogFormat( "scene2 = {0}", scene2 ); string sceneN1 = SceneUtility.GetScenePathByBuildIndex( -1 ); Debug.LogFormat( "sceneN1 = {0}", sceneN1 ); yield return null; }
scene0 = Assets/Scenes/SceneB.unity
scene2 =
sceneN1 =
BuildSettgingsにない数値を指定すると空文字が返るようです。
調査結果その後
えらく長くなってしまった……。
長くなった理由がの大半が、明らかに間違った使い方をすると例外が発生するということに関してだ……。
ただこれでSceneManager
に関して、知識が深まった気がします。
いまならSceneManagerマネージャーを名乗れそうだ!