今回は、シーンによるプロジェクトの分割の3回目で、第31回から作ってきたサービス(シーンの切り替えを仲介する機能)を使って、シーンを切り替えるメッセージを送ります。
「シーンによるプロジェクトの分割」と呼んでいるのは、ゲーム自体を複数のシーンに分けて作業性を高め、分割・結合を簡単にできるようにすることです。ゲーム全体を一つのマネージャー(ゲーム全体のデータを管理する構造体)に管理させるだけでなく、全てのシーンが揃っていなくても、最低限のシーンだけでテストができるようにします。
前回までの流れ
#31でシーン同士の関係を記述する「サービス」を定義し、#32でシーン同士が親子関係、つまりプロジェクト内の全シーンがツリー状に繋がるようにしました。
ツリーにしたかったのは、以下の理由からでした。
(1)全てのシーンがマネージャーにアクセスできるようにする
(2)もしマネージャーが不在でも、担当者レベル、シーンレベルでテストをする時に、上位シーンがマネージャーの代理応答をできるようにする
(参考)
#31:シーン間の関わりを記述するコンポーネント「サービス」
tomo-mana.hatenablog.com
#32:サービスに親子関係を持たせる
tomo-mana.hatenablog.com
今回やりたいこと
今回は、このツリーのどこかにいるシーンに、切り替えメッセージを送る処理を実装します。
この時、シーンは切り替え先のシーンだけを意識し、実際はマネージャーを経由して目的のシーンに切り替えます。
シーンの切り替えを仲介する機能(サービス)は、親子関係を持っています。サービスを持つシーン同士は、以下のように一人の親と複数の子を持ちます。ちょうどGameObjectとTransformの関係に似ています。
各サービスが自分が受信したいメッセージを定義します。このツリー状のプロジェクトでは、メッセージはプロジェクト全体で一意(ユニーク)である必要があります。メッセージが一意であることで、送り先を指定しなくても送り先を特定することができます。詳しくは以下にまとめてあります。
tomo-mana.hatenablog.com
サービスはメッセージをブロードキャストで飛ばします。サービスはメッセージを受信できるサービスを前方一致で見つけ、そのサービスをアクティブにして、メッセージの伝達を終わります。
この手続きは、以下の4つのステップに分かれます。
(1) サービスが作られたか(存在するか)
(2) 親(マネージャー)の探索
(3) 親(マネージャー)から子にメッセージを届ける
(4) シーン間メッセージ
サービスが作られたか(存在するか)
前回は、シーンがロードされたかどうかを、rootObjectが取得できるかで判断しました。サービスを取得する時はこの方が都合が良かったのですが、途中でシーンをアンロードしなければ、サービスがいるものとしてアクセスできる方が楽です。
そのため、前回追加したinitializedフラグを、boolから状態変数(列挙体)に変えます。初期化されたか、だけでなく、初期化の結果作ったか(Exist)、作らなかったか(Not Exist)。この判断のため、初期化処理を少し変更します。
親(マネージャー)の探索
次に、親を探索します。サービスの親は一人しかいないので、親が居れば自分はマネージャーでなく、居なければ自分がマネージャーです。
親(マネージャー)から子にメッセージを届ける
親(マネージャー)から子にメッセージを送るには、木構造を再帰的に探索します。
親から子に届けるメッセージは、システム内で一意になるようにするので、今回は前方一致にします。(見つけたら終わり。後方一致は最後まで検索する)
※前回の図に番号を振った木構造の図
ここまでの機能で、システムで最初にアクティブにするシーンを決定できます。システムで親(=マネージャー)になったシーンが、システム内で最初のシーンになりたい人(FirstScene)をアクティブにします。最初のシーンになりたい人の受信メッセージ一覧に"FirstScene"を追加します。なお、この実装が、(3)の単体テストも兼ねます。
シーン間メッセージ
親の探索と、親から子どもにメッセージを送る機能を実現したら、シーン間メッセージはこの組み合わせで実現できます。
今回は、(4)のテストも兼ねて、マネージャーまたはフィールドから起動して、フィールドでエンカウントしたらバトルにメッセージを送ってみます。
例えば、フィールドシーンのエンカウント処理(第28回)に以下を追加します。
// シーンは、ゲームオブジェクトのどの階層からでも以下の1文で取得できます。 Scene scene = gameObject.scene; // サービスの取得用に、以下のインターフェースを用意します。 SceneMessageService service = SceneMessageService.GetService( scene ); // サービス間メッセージ(メッセージは文字列で指定) if( service != null ){ SceneMessageService.SendMessage( service, "Encount" ); }
上の4点の修正をまとめると、以下になります。
コード
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; // (1) 存在するか? public enum RegistState { NOT_INITIALIZED, EXIST, NOT_EXIST, }; // シーン登録 [System.Serializable] public class SceneRegistration { // シーン名 public string name; // ロードされていなければロードする public bool forceLoad; // シーン(保持) [System.NonSerialized] public Scene scene; // サービス(保持) [System.NonSerialized] public SceneMessageService service; // 初期化完了 public RegistState initialized = RegistState.NOT_INITIALIZED; public bool Initialized() { if( initialized != RegistState.NOT_INITIALIZED ){ return true; } return false; } public bool Exist() { if( initialized == RegistState.EXIST ){ return true; } return false; } } // 受信可能なメッセージ一覧 [System.Serializable] public class SceneMessage { public string message; } public class SceneMessageService : MonoBehaviour { [SerializeField] public SceneRegistration sceneParent; [SerializeField] public SceneRegistration[] sceneChildren; public enum ServiceState { NOT_INITIALIZED, SCENE_INITIALIZED, MANAGER_FOUND, FIRST_SCENE_SELECTED, }; ServiceState initialized = ServiceState.NOT_INITIALIZED; [SerializeField] public SceneMessage[] messages; public bool isParent = false; void Start() { // 強制起動 if( NeedToLoad(sceneParent) ){ SceneManager.LoadSceneAsync( sceneParent.name, LoadSceneMode.Additive ); } foreach( SceneRegistration sceneChild in sceneChildren ){ if( NeedToLoad(sceneChild) ){ SceneManager.LoadSceneAsync( sceneChild.name, LoadSceneMode.Additive ); } } } bool InitializeService() { bool r = true; if( !sceneParent.Initialized() ){ r = false; GetService( sceneParent ); } foreach( SceneRegistration sceneChild in sceneChildren ){ if( !sceneChild.Initialized() ){ r = false; GetService( sceneChild ); } } return r; } void Update() { switch( this.initialized ){ case ServiceState.NOT_INITIALIZED: if( InitializeService() ){ ServiceLog(); this.initialized = ServiceState.SCENE_INITIALIZED; } break; case ServiceState.SCENE_INITIALIZED: if( FindParent() ){ this.initialized = ServiceState.MANAGER_FOUND; } break; case ServiceState.MANAGER_FOUND: if( isParent ){ if( !FindChild(this, "FirstScene") ){ SceneManager.SetActiveScene(this.gameObject.scene); } } this.initialized = ServiceState.FIRST_SCENE_SELECTED; break; default: break; } } bool NeedToLoad(SceneRegistration r) { if( !string.IsNullOrEmpty( r.name ) ){ if( !(r.name == SceneManager.GetSceneByName( r.name ).name) ){ if( r.forceLoad ){ return true; } } } return false; } List<GameObject> rootObjects = new List<GameObject>(); void GetService(SceneRegistration r) { if( !string.IsNullOrEmpty( r.name ) ){ r.scene = SceneManager.GetSceneByName( r.name ); if( r.scene.name == r.name ){ r.scene.GetRootGameObjects( rootObjects ); if( rootObjects != null ){ foreach( GameObject obj in rootObjects ){ SceneMessageService[] s = obj.GetComponents<SceneMessageService>(); if( s.Length > 0 ){ r.service = s[0]; r.initialized = RegistState.EXIST; break; } } } } else { if(!r.forceLoad){ r.initialized = RegistState.NOT_EXIST; } } } else { r.initialized = RegistState.NOT_EXIST; } } void ServiceLog() { string[] s = new string[3]{ "--", "--", "--" }; if( Loaded(sceneParent) ){ s[0] = sceneParent.service.gameObject.scene.name; } for(int i = 0; i < sceneChildren.Length; i++){ if( Loaded(sceneChildren[i]) ){ s[i+1] = sceneChildren[i].service.gameObject.scene.name; } } Debug.Log($"me:{gameObject.scene.name}, p:{s[0]}, c0:{s[1]}, c1:{s[2]}"); } bool Loaded(SceneRegistration r) { if( !string.IsNullOrEmpty( r.name ) ){ if( (r.name == SceneManager.GetSceneByName( r.name ).name) ){ return true; } } return false; } // (2) 親の選出 bool FindParent() { bool find = false; SceneMessageService sv = this; while( find == false ){ if( sv.sceneParent.Initialized() ){ if( sv.sceneParent.Exist() ){ sv = sv.sceneParent.service; } else { sv.isParent = true; find = true; } } else { // また今度 break; } } return find; } // (3) 子にメッセージを送る static bool FindChild(SceneMessageService sv, string message) { if( sv != null ){ if( Dispatch(sv, message) ){ return true; } else { foreach( SceneRegistration child in sv.sceneChildren ){ if( FindChild( child.service, message ) ){ return true; } } } } return false; } static bool Dispatch(SceneMessageService sv, string message) { if( sv == null ){ return false; } if( sv.messages == null ){ return false; } foreach( SceneMessage myMes in sv.messages ){ if( message == myMes.message ){ SceneManager.SetActiveScene( sv.gameObject.scene ); return true; } } return false; } // (4) シーン間メッセージ public static SceneMessageService GetService( Scene scene ) { List<GameObject> rootObjects = new List<GameObject>(); scene.GetRootGameObjects( rootObjects ); foreach( GameObject obj in rootObjects ){ SceneMessageService[] s = obj.GetComponents<SceneMessageService>(); if( s.Length > 0 ){ return s[0]; } } return null; } public static bool SendMessage( SceneMessageService sv, string message ) { if( sv = FindParent( sv ) ){ if( FindChild( sv, message ) ){ return true; } } return false; } static SceneMessageService FindParent( SceneMessageService sv ) { if( sv != null ){ if( sv.isParent || !sv.sceneParent.Exist() ){ return sv; } else { return FindParent(sv.sceneParent.service); } } return null; } }
動作テスト
(1) 初期化が完了するとEXIST、NOT EXISTになっているのが確認できます。
(2) 親(マネージャー)になったシーンにはisParentがチェックされている事が分かります(システム内で一つのシーンだけ)。
(3) FirstSceneメッセージを受け取ることにしたシーンが最初にアクティブになります。(A)
いなければ、親が最初にアクティブになります(B)
(4) フィールドからバトルに切り替わります。
次回
メッセージ送信処理はここまでにして、次回から戦闘画面の設計に入ります。