SceneManager
も研究しましたし、今回のプロジェクトのシーンを管理するシステムを作っていきましょう。
急いで作りましょう。
Photonから離れて半年近く経っていますから、そろそろPhotonでゲームを作る自信がなくなってきましたよ。
この記事にはUnity5.6.0f3を使用しています。
こんな感じにシーンを管理したいな
今回のプロジェクトで使うシーン構成はこんな感じなものを考えています。
SceneManager
のAdditive
を全力で使った形を目指します。
実際使いやすいかはわかりませんが、とりあえず作っていきます。
シーン管理委員会のメンバー紹介
会計:シーン情報クラスSceneData
各シーン情報を管理するクラス、それがSceneData
だ。
エディター上でパラメータを記述したりしちゃうぞ。
とりあえず最初はID(シーン名)だけのパラメータから始めます。
[System.Serializable] public class SceneData { [SerializeField] private string m_id = null; public string ID { get { return m_id; } } }
書記:シーン情報テーブルSceneTable
噂のScriptableObject
を使って、SceneData
をリストで持っちゃうのがSceneTable
だ。
Assetとして存在しちゃうぞ。
SceneData
のリストと一緒に、指定したシーンの名前からSceneData
を取得する関数も追加しよう。
SceneTable.cs
using UnityEngine; using System.Linq; [CreateAssetMenu( menuName = "Game/Create SceneTable", fileName = "SceneTable" )] public class SceneTable : ScriptableObject { [SerializeField] private SceneData[] m_sceneList = null; /// <summary> /// シーン情報を取得するよ! /// </summary> /// <param name="i_id">取得したいシーンのID(名前)</param> /// <returns>シーン情報</returns> public SceneData GetSceneData( string i_id ) { if( string.IsNullOrEmpty( i_id ) ) { // 引数に変な文字列渡してんじゃないのさ! throw new System.ArgumentNullException( "i_id" ); } if( m_sceneList == null || m_sceneList.Length == 0 ) { // シーン情報が一つも登録されておらんのだが……。 Debug.AssertFormat( false, "m_sceneList is empty!" ); return null; } return m_sceneList.FirstOrDefault( value => value.ID == i_id ); } } // class SceneTable
会長:シーン管理クラスSceneController
そして、シーンの読み込みなどありとあらゆるシーンの管理を司るのがSceneController
だ。
SceneManager
という名前にすると、UnityのSceneManager
と被るので絶対に止めようぜ!
シーンを管理するクラスを作成。
SceneController.cs
using UnityEngine; using UnityEngine.SceneManagement; using System.Collections; public class SceneController : MonoBehaviour { /// <summary> /// シーンを読み込むぜ! /// </summary> /// <param name="i_sceneId">シーンのID(名前)</param> public void LoadScene( string i_sceneId ) { StartCoroutine( LoadSceneProcess( i_sceneId ) ); } private IEnumerator LoadSceneProcess( string i_sceneId ) { // シーンを読み込む処理を書くよ。 yield return null; } } // class SceneController
とりあえず、何もしないSceneController
が出来ました。
SceneController
は仕事をするにはシーンの情報が必要なので、SceneTable
にアクセスする必要がありますね。
以前はScriptableObject
をどのクラスからでも取得できるようにしましたが、シーン情報はSceneController
クラスが主にアクセスするためSceneTable
はSceneController
が所持する形にしたほうがいいでしょう。
アクセスできる人間(クラス)は限られていたほうが、管理しやすいです。
という訳で、SceneTable
を読み込む処理を追加しましょう。
SceneTable.cs
private static readonly string RESOURCE_PATH = "SceneTable"; /// <summary> /// シーンテーブルのアセットを読み込むよ! /// </summary> /// <returns>SceneTableのアセット</returns> public static SceneTable LoadAsset() { var asset = Resources.Load( RESOURCE_PATH ) as SceneTable; if( asset == null ) { // どうやらシーンテーブルのアセットが用意されていないようだ。 // 早急に用意してくれたまえ。 throw new System.IO.FileNotFoundException( RESOURCE_PATH ); } return asset; }
SceneController.cs
private SceneTable m_sceneTableAsset = null; private void Awake() { m_sceneTableAsset = SceneTable.LoadAsset(); } /// <summary> /// シーンを読み込むぜ! /// </summary> /// <param name="i_sceneId">シーンのID(名前)</param> public void LoadScene( string i_sceneId ) { StartCoroutine( LoadSceneProcess( i_sceneId ) ); } private IEnumerator LoadSceneProcess( string i_sceneId ) { var sceneData = m_sceneTableAsset.GetSceneData( i_sceneId ); if( sceneData == null ) { // そんなシーン、俺は知らん! Debug.AssertFormat( false, "Scene is missing! id={0}", i_sceneId, gameObject ); yield break; } yield return SceneManager.LoadSceneAsync( sceneData.ID, LoadSceneMode.Additive ); }
シーン達の用意
今回のプロジェクトで使用するシーンを空の状態で用意しましょう。
そして、先ほど作ったSceneTable
にガンガン登録していきます。
文字列コピー面倒くさい……。
エディタ拡張も必要かもしれない。
神:ゲーム制御者GameController
今回のゲームを制御する絶対者、GameController
を作っていきましょう。
神とは名乗ってますが、神クラスみたいな作りは僕は嫌いです。
では早速、GameController
様にSceneController
を使ってシーンを読みこんでもらいましょう。
GameController.cs
using UnityEngine; using System.Collections; public class GameController : MonoBehaviour { private SceneController m_sceneController = null; private void Awake() { m_sceneController = gameObject.AddComponent<SceneController>(); } private IEnumerator Start() { // 仮機能でバンバンシーンを読み込む。 string[] sceneList = new string[] { "DebugMenu", "Title", "MainMenu", "Status", "Explain", "Option", "Gameplay", "Stage00", "Stage01", }; foreach( var sceneID in sceneList ) { m_sceneController.LoadScene( sceneID ); yield return new WaitForSeconds( 1.0f ); } } } // class GameController
シーンが読み込めました。よかったです。
ただこのままでは、理想には程遠い。
改良していきますよ。
同じシーンがHerarchy上に存在することを阻止する
先ほどのコード。少し問題があることに気づきました。
GameController
にて読み込むシーンを以下のようにして試してみます。
GameController.cs
string[] sceneList = new string[] { "DebugMenu", "DebugMenu", "DebugMenu", "DebugMenu", "Gameplay", "Stage00", "Stage01", };
同じシーンが読み込まれてますね。
まあこれはSceneManager
を調べた際にわかっていたことではあるんですけどね。
ただ、同じシーンがあるのは管理上厄介そうなので、コード上でこんなことにはならないようにしましょう。
SceneController.cs
private IEnumerator LoadSceneProcess( string i_sceneId ) { var sceneData = m_sceneTableAsset.GetSceneData( i_sceneId ); if( sceneData == null ) { // そんなシーン、俺は知らん! Debug.AssertFormat( false, "Scene is missing! id={0}", i_sceneId, gameObject ); yield break; } Scene currentScene = SceneManager.GetSceneByName( sceneData.ID ); if( currentScene.IsValid() ) { // そのシーンは既に読み込まれているようだ。 // かぶるから読み込みは差し控えさせていただきます。 Debug.LogWarningFormat( "The scene has already been loaded.! id={0}", i_sceneId, gameObject ); yield break; } yield return SceneManager.LoadSceneAsync( sceneData.ID, LoadSceneMode.Additive ); }
同じシーンを読み込もうとすると、警告文が出て読み込めなくなりましたね。
この状態になった場合は、ゲームのロジック部分で何かおかしなことが起きている証拠なのです。
シーンを型(カテゴリ)にはめよう
さて、僕の目指すシーン管理の理想図をもう一度みてみましょう。
TitleやGameplayなど共存できないシーンがあります。
これを表現する機能が必要です。
カテゴリという情報を追加してみましょう。
[System.Serializable] public class SceneData { [SerializeField] private string m_id = null; public string ID { get { return m_id; } } [SerializeField] private string m_category = null; public string Category { get { return m_category; } } }
そしてSceneController
に同じカテゴリのシーンを取得する関数を追加しますね。
SceneController.cs
/// <summary> /// 指定したシーンと同じカテゴリのHerarchy上のシーンを取得するぞ。 /// </summary> private Scene GetSceneOfSameCategoryInHerarchy( SceneData i_sceneData ) { if( i_sceneData == null ) { throw new System.ArgumentNullException( "i_sceneData" ); } // カテゴリが無いシーンはルートシーン扱い。 if( string.IsNullOrEmpty( i_sceneData.Category ) ) { return new Scene(); } for( int i = 0; i < SceneManager.sceneCount; ++i ) { Scene scene = SceneManager.GetSceneAt( i ); if( scene.name == i_sceneData.ID ) { continue; } SceneData sceneData = m_sceneTableAsset.GetSceneData( scene.name ); if( sceneData == null ) { continue; } if( string.IsNullOrEmpty( sceneData.Category ) ) { continue; } if( sceneData.Category == i_sceneData.Category ) { return scene; } } return new Scene(); }
この関数を利用して、シーンを読み込む際に同じカテゴリのシーンがHerarchy上にある場合は、読み込まれているシーンを破棄する機能を追加してみます。
SceneController.cs
private IEnumerator LoadSceneProcess( string i_sceneId ) { var sceneData = m_sceneTableAsset.GetSceneData( i_sceneId ); if( sceneData == null ) { // そんなシーン、俺は知らん! Debug.AssertFormat( false, "The scene is missing! id={0}", i_sceneId, gameObject ); yield break; } { Scene currentScene = SceneManager.GetSceneByName( sceneData.ID ); if( currentScene.IsValid() ) { // そのシーンは既に読み込まれているようだ。 // かぶるから読み込みは差し控えさせていただきます。 Debug.LogWarningFormat( "The scene has already been loaded.! id={0}", i_sceneId, gameObject ); yield break; } } // 同一のカテゴリのシーンを複数存在させない。 Scene sceneOfSameCategory = GetSceneOfSameCategoryInHerarchy( sceneData ); if( sceneOfSameCategory.IsValid() ) { yield return SceneManager.UnloadSceneAsync( sceneOfSameCategory ); } yield return SceneManager.LoadSceneAsync( sceneData.ID, LoadSceneMode.Additive ); }
さて、うまくいっているか試してみましょう。
GameController.cs
private IEnumerator Start() { // 仮機能でバンバンシーンを読み込む。 string[] sceneList = new string[] { "DebugMenu", "Title", "MainMenu", "Status", "Explain", "Option", "Gameplay", "Stage00", "Stage01", }; foreach( var sceneID in sceneList ) { m_sceneController.LoadScene( sceneID ); yield return new WaitForSeconds( 1.0f ); } }
同じカテゴリのシーンを読む場合は、シーンを破棄してから読み込むようになっています。
成功です。
徐々に理想に近づいてきました。
親子の絆
僕の理想を実現するためには、シーンに親子関係を持たせる必要がありそうです。
親シーンという情報を追加しましょう。
[System.Serializable] public class SceneData { [SerializeField] private string m_id = null; public string ID { get { return m_id; } } [SerializeField] private string m_category = null; public string Category { get { return m_category; } } [SerializeField] private string m_parentId = null; public string ParentId { get { return m_parentId; } } }
親シーンが破棄された場合は、子シーンも破棄する
親シーンが破棄された場合は、子シーンも破棄される仕組みを作りましょう。
まずは、子シーンデータを取得する関数を追加します。
SceneTable.cs
/// <summary> /// 指定したシーンを親に持つシーンを取得するよ。 /// </summary> public SceneData[] GetChildScenes( string i_sceneId ) { if( string.IsNullOrEmpty( i_sceneId ) ) { // 引数に変な文字列渡してんじゃないのさ! throw new System.ArgumentNullException( "i_id" ); } if( m_sceneList == null || m_sceneList.Length == 0 ) { // シーン情報が一つも登録されておらんのだが……。 Debug.AssertFormat( false, "m_sceneList is empty!" ); return null; } return m_sceneList.Where( value => value.ParentId == i_sceneId ).ToArray(); }
親子シーン破棄用の関数を作り、SceneManager.UnloadSceneAsync()
を呼んでいた箇所と差し替えます。
SceneController.cs
/// <summary> /// シーンを破棄するよ。 /// 子シーンも破棄するよ。 /// </summary> private IEnumerator UnloadScene( Scene i_scene ) { if( !i_scene.IsValid() ) { throw new System.ArgumentException( "i_scene" ); } SceneData[] childList = m_sceneTableAsset.GetChildScenes( i_scene.name ); if( childList != null ) { foreach( var child in childList ) { Scene childScene = SceneManager.GetSceneByName( child.ID ); if( childScene.IsValid() ) { // 子シーンの子シーンがいる可能性があるため、再帰する。 yield return UnloadScene( childScene ); } } } yield return SceneManager.UnloadSceneAsync( i_scene ); }
これでシーンを破棄する際に、子シーンがある場合は一緒に破棄してくれるようになります。
親がいない状態で子シーンを読む場合は、親シーンも読む
先ほどとは逆に子シーンのみ作成された場合には、親シーンを読んでから子シーンを読み込むようにしましょう。
実際にこのような状況があるかはわかりませんけど……。
SceneController.cs
/// <summary> /// シーンを読み込むよ。 /// 親シーンがいるけど、まだ読み込まれていない場合は親シーンも読み込むよ。 /// </summary> private IEnumerator LoadScene( SceneData i_sceneData ) { if( i_sceneData == null ) { throw new System.ArgumentNullException( "i_sceneData" ); } string parentId = i_sceneData.ParentId; if( !string.IsNullOrEmpty( parentId ) ) { Scene parentScene = SceneManager.GetSceneByName( parentId ); if( !parentScene.IsValid() ) { SceneData parentSceneData = m_sceneTableAsset.GetSceneData( parentId ); if( parentSceneData == null ) { Debug.AssertFormat( false, "Parent scene is missing! id={0} parentId={1}", i_sceneData.ID, parentId, gameObject ); } // 親シーンの親シーンがいる可能性があるため、再帰する。 yield return LoadScene( parentSceneData ); } } yield return SceneManager.LoadSceneAsync( i_sceneData.ID, LoadSceneMode.Additive ); }
この新しく作ったLoadScene()
をSceneManager.LoadSceneAsync()
と差し替えます。
動作を確認してみましょう。
GameController.cs
private IEnumerator Start() { // 仮機能でバンバンシーンを読み込む。 string[] sceneList = new string[] { "DebugMenu", "Title", "MainMenu", "Status", "Explain", "MainMenu", "Option", "Stage00", "Stage01", }; foreach( var sceneID in sceneList ) { m_sceneController.LoadScene( sceneID ); yield return new WaitForSeconds( 1.0f ); } yield return null; }
いい感じに動いています。
僕の思い描いていたシーン管理を実現できたような気がしますよ!
完成!
これで僕が望んでいたシーン管理システムが出来ました。
実際使っていくと気になる点が出てくるとは思いますので、それは都度修正していきましょう。