ゲーム化!tomo_manaのブログ

ゲーム化!tomo-manaのブログ

Unityでゲームを作る方法について紹介しています

Unity学習#32 シーンによるプロジェクトの分割#2 (Unity 2019.4.4f1)

今回は、前回の続きで、サービス同士を連結させて、任意のシーンにアクセスできる環境を作ります。

前回までの流れ

tomo-mana.hatenablog.com
プロジェクトをシーンで分割して、作業、デバッグをしやすくする方法について検討してきました。
シーン切り替えのメッセージを仮想化して、シーンを担当者や場面別に分けて開発しても、プロジェクトを結合した時に、実装を変えずに済むようにしたい。また、マネージャーなど一部のシーンが無い状態でも、シーン単体や担当者が受け持つシーン内でデバッグできるようにしたい。

そこで、シーンを以下のように連結して、シーン切り替えを指示するとアクティブなシーンを切り替えながら目的のシーンまで切り替える方法を検討しました。また、目的のシーンが無い場合に、マネージャーの代わりをしてくれるシーンが代理応答できるようにしたいと思います。

(本当はシーンにこの機能があれば、と思うのですが、シーンに機能を追加するわけにもいかないので、サービスに親子認識を持たせます。「サービス」とは、シーンに親子関係を持たせるために前回定義したクラス(SceneMessageService)を指します)

f:id:tomo_mana:20210328014102p:plain
シーン切り替え(仮想化)

今回は、サービス同士が親子関係を持てるように、サービス同士を連結する処理を追加します。(具体的には、互いへの参照を持たせます)

考え方

前回の記事で、シーン同士の関係を持つサービスを、以下のように定義しました。
f:id:tomo_mana:20210415214613p:plain

お互いに関係のないシーン同士を、サービスで関連付けます(図中のSがサービス)。

f:id:tomo_mana:20210415220526p:plain
サービス(S)とシーンの関係


サービスを取得するには、シーンの RootGameObjects にアクセスできる必要があります。

ロードが開始されたかどうかは、LoadSceneAsync()の直後でも認識できましたが、ロード完了となると話は違います。ロード完了を知る方法はいくつかありそうですが、今回は目的のサービスが相手先のシーンから取得できるようになるまで、シーンからサービスの取得をリトライすることにします。

コード

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

// シーン登録
[System.Serializable]
public class SceneRegistration {
	// シーン名
	public string name;
	// ロードされていなければロードする
	public bool forceLoad;
	// シーン(保持)
	[System.NonSerialized]
	public Scene scene;
	// サービス(保持)
	[System.NonSerialized]
	public SceneMessageService service;
	// 初期化完了
	public bool initialized = false;
}

public class SceneMessageService : MonoBehaviour
{
	[SerializeField]
	public SceneRegistration sceneParent;
	[SerializeField]
	public SceneRegistration[] sceneChildren;
	
	bool initialized = false;
	
	void Start()
	{
            // 略(#31)
	}
	
	void Update()
	{
		if( !this.initialized ){
			bool r = true;
			if( !sceneParent.initialized ){
				r = false;
				GetService( sceneParent );
			}
			foreach( SceneRegistration sceneChild in sceneChildren ){
				if( !sceneChild.initialized ){
					r = false;
					GetService( sceneChild );
				}
			}
			this.initialized = r;
		}
	}
	
	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 = true;
							break;
						}
					}
				}
			} else {
				if(!r.forceLoad){
					r.initialized = true;
				}
			}
		} else {
			r.initialized = true;
		}
	}
}


テスト時には、以下のログを追加しています。s[0]には親のシーン名、s[1]s[2]には子のシーン名が入ります。サービスからシーンを参照できるかで進捗を確認します。(Component→GameObject→Sceneの関係:以前の記事

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;
}

動作テスト

シーンのロード完了までそれなりに時間はかかるものの、数回のポーリングでサービスが全て取得できました。

前回の設定に従って出力すると、以下のようになります(今回はフィールドシーン経由で起動テストしました)。ツリーの通りに起動しています。

ログ
f:id:tomo_mana:20210415004116p:plain

(前回の設定)
f:id:tomo_mana:20210306001700p:plain


サービスの関連付けができたかは、initializedフラグでも確認できます。

フィールドシーンの例
f:id:tomo_mana:20210415003312p:plain

次回

いよいよ、仮想化してメッセージをターゲットとなるシーンに、他のシーン経由で伝達する方法を実装します。