unity どのシーンでも共通でサブシーンを常駐させる

unity ゲームが完成に近づいていくと、どのシーンでも共通で欲しいデータ・処理をどうするか、という問題にあたります。

  • 必要なゲームデータ
  • シーン共通のフェードイン・アウトといった演出
  • Editor でどのシーンから始めても、これらが常にいてほしい

どう実装していいのかわからないので、色々と試してみました。

Monobehaviour 消して static 変数にアクセス

Main.cs を任意の GameObject につけて使います。

(Main.cs)

public class Main
{
	public static int DataA;
	public static int DataB;
	public static int DataC;
}

(Caller.cs)

public class Caller
{
	public void Call()
	{
		Main.DataA = 1;
		Debug.Log(Main.DataA);
	}
}

簡単。データのやりとりだけならこれでもいいかも?
シーン跨いだらデータ消える
間違って2つの GameObject につけたらおかしくなりそう…?
全シーンの GameObject にくっつける必要があって大変

(結論)全然要件を満たせない

Singleton Monobehaviour

こちらより拝借しました。これも任意の GameObject につけて使います。

https://qiita.com/okuhiiro/items/3d69c602b8538c04a479

(SingletonMonoBehaviour.cs)

using System;
using UnityEngine;

public abstract class SingletonMonoBehaviour<T> : MonoBehaviour where T : MonoBehaviour
{
	static T instance;
	public static T Instance
	{
		get
		{
			if (instance == null)
			{
				Type t = typeof(T);

				instance = (T)FindObjectOfType(t);
				if (instance == null)
				{
					Debug.LogError($"No GameObject with attached {t}";
				}
			}

			return instance;
		}
	}

	virtual protected void Awake()
	{
		if (this != Instance)
		{
			Destroy(this);
			Debug.LogError($"Multiple {typeof(T)} cannot exist. I deleted it from this. (Original = {Instance.gameObject.name})";
			return;
		}

		DontDestroyOnLoad(this.gameObject);
	}
}

(NewBehaviourScript .cs)

using UnityEngine;
using System.Collections;

public class NewBehaviourScript : SingletonMonoBehaviour<NewBehaviourScript>
{
	public int DataA;

	override protected void Awake()
	{
		// Be sure to call
		base.Awake();
	}
}

(Caller.cs)

public class Caller
{
	public void Call()
	{
		NewBehaviourScript.Instance.DataA = 1;
		Debug.Log(NewBehaviourScript.Instance.DataA);
	}
}

まぁまぁ簡単。
シーン跨いだらデータ消える ← DontDestroyOnLoad になるため、消えない
間違って2つの GameObject につけたらおかしくなりそう…? ← エラーで確認できる
NewBehaviourScript のあるシーンからしか開始できなくなる

ゲーム作成中は、色々なシーンからスタートしたいので、ちょっと使いづらいです。
一応、シーン起動時、例えばメインカメラに自動的に NewBehaviourScript をスクリプトでアタッチすれば大丈夫…? こんな感じで(実行はしていません)

(Initialize.cs)

public class Initialize : MonoBehaviour
{
	[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
	static void afterSceneLoad()
	{
		if (NewBehaviourScript.Instance == null)
		{
			Camera.main.gameObject.AddComponent<>();
		}
	}
}

こうなってくると、実行までヒエラルキーに存在しないので、ヒエラルキーで構造を把握しにくくなりました。
また、データ管理ならともかく、シーン共通のフェードインアウトなんかはちょっと入れづらそう。

おすすめ:マスターシーンを作成し、どのシーンから起動しても呼び出されるようにする

この方法を愛用しています。1番応用が利くと思うので。
元ネタはテラシュールブログさんだったのですが、探しても見つからなかった…(いつもお世話になっております)。
手順はこんな感じです。

  1. 最初に起動したシーンマスターシーンでなければ、一旦オブジェクトを全て Disabled
  2. マスターシーンを起動
  3. マスターシーンのオブジェクトを全て DontDestroyOnLoad に
    ⇒ これでシーン移動しても消えなくなります
  4. (3) が完了したら、最初に起動したシーンを元に戻す
  5. マスターシーンの残骸(.unity)を消去

[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]

この属性がついたメソッドは特殊で、以下のような便利な特性を持っています。

  • GameObjectについてなくてもコールされる
  • 起動シーンのAwakeよりも早く実行される

これを使って、最初のシーンが起動するよりも先にマスターシーン(常駐)を起動、実行させる方法です。

(Main.cs)

/// <summary>
/// Master Scene
/// </summary>
public class Main : MonoBehaviour
{
	static GameObject[]	childSceneObjects = null;
	
	/// <summary>
	/// Program entry point
	/// c の main() に相当するメソッド
	/// </summary>
	[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
	static void main()
	{
		// launch check
		string sceneName = SceneManager.GetActiveScene().name;
		
		// if: launch different from 'Main'
		if (sceneName != "Main")
		{
			// Disable child scene
			setChildSceneActive(false);
			
			// load 'Main' scene
			SceneManager.LoadSceneAsync("Main", LoadSceneMode.Additive);
		}
	}
	
	/// <summary>
	/// Make the Main scene 'Don't destroy'.
	/// Main シーンを常駐化する
	/// </summary>
	void Awake()
	{
		StartCoroutine(awaker());
	}
	
	/// <summary>
	/// Create 'Don't destroy' scene (Main).
	/// </summary>
	IEnumerator awaker()
	{
		// 'Main' moved 'DontDestroyOnLoad'
		UnityEngine.Object.DontDestroyOnLoad(this.gameObject);
		
		// Enable child scene
		if (childSceneObjects != null)
		{
			setChildSceneActive(true);
			childSceneObjects = null;
		}
		
		Scene scene = SceneManager.GetSceneByName(eScene.Main.ToString());
		
		// loaded になるまで unload できないので、ここで待つ
		while (scene.isLoaded == false)
		{
			yield return null;
		}
		
		// delete source scene
		yield return SceneManager.UnloadSceneAsync(scene);
	}
	
	/// <summary>
	/// Activate child scene.
	/// </summary>
	static void setChildSceneActive(bool value)
	{
		if (childSceneObjects == null)
		{
			childSceneObjects = Object.FindObjectsOfType<Transform>()
				.Select(t => t.root.gameObject)
				.Distinct()
				.Where(go => go.activeInHierarchy)
				.ToArray();
		}
		
		foreach (GameObject go in childSceneObjects)
		{
			go.SetActive(value);
		}
	}
}
			// Disable child scene
			setChildSceneActive(false);
			
			// load 'Main' scene
			SceneManager.LoadSceneAsync("Main", LoadSceneMode.Additive);
  1. 最初に起動したシーンがマスターシーンでなければ、一旦オブジェクトを全てDisabled
  2. マスターシーンを起動
		// 'Main' moved 'DontDestroyOnLoad'
		UnityEngine.Object.DontDestroyOnLoad(this.gameObject);
  1. マスターシーンのオブジェクトを全て DontDestroyOnLoad に
    ⇒ これでシーン移動しても消えなくなります
		// Enable child scene
		if (childSceneObjects != null)
		{
			setChildSceneActive(true);
			childSceneObjects = null;
		}
  1. 4(3) が完了したら、最初に起動したシーンを元に戻す
		// loaded になるまで unload できないので、ここで待つ
		while (scene.isLoaded == false)
		{
			yield return null;
		}
		
		// delete source scene
		yield return SceneManager.UnloadSceneAsync(scene);
  1. マスターシーンの残骸(.unity)を消去

マスターシーンには全てのシーンで使うデータを色々と入れておきます。
DontDestroyOnLoad に移動した時、バラバラにならないように、Main というルート GameObject の中に全てを入れておきます。

Camera .. BgRoot を表示するため入れました
EventSystem .. 全てのシーンに入れてたら、必要なし
BgRoot .. アプリケーションで表示する背景です
VoiceClip .. 音声データ登録
DebugDisp .. デバッグ用の情報表示

Main に Main.cs をアタッチ

この状態になったら新しいシーンを作成し、実行します。
以下のように、どのシーンから開始しても DontDestroyOnLoad に Main シーンが作成&実行されます。