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番応用が利くと思うので。
元ネタはテラシュールブログさんだったのですが、探しても見つからなかった…(いつもお世話になっております)。
手順はこんな感じです。
- 最初に起動したシーンがマスターシーンでなければ、一旦オブジェクトを全て Disabled
- マスターシーンを起動
- マスターシーンのオブジェクトを全て DontDestroyOnLoad に
⇒ これでシーン移動しても消えなくなります - (3) が完了したら、最初に起動したシーンを元に戻す
- マスターシーンの残骸(.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);
- 最初に起動したシーンがマスターシーンでなければ、一旦オブジェクトを全てDisabled
- マスターシーンを起動
// 'Main' moved 'DontDestroyOnLoad' UnityEngine.Object.DontDestroyOnLoad(this.gameObject);
- マスターシーンのオブジェクトを全て DontDestroyOnLoad に
⇒ これでシーン移動しても消えなくなります
// Enable child scene if (childSceneObjects != null) { setChildSceneActive(true); childSceneObjects = null; }
- 4(3) が完了したら、最初に起動したシーンを元に戻す
// loaded になるまで unload できないので、ここで待つ while (scene.isLoaded == false) { yield return null; } // delete source scene yield return SceneManager.UnloadSceneAsync(scene);
- マスターシーンの残骸(.unity)を消去
マスターシーンには全てのシーンで使うデータを色々と入れておきます。
DontDestroyOnLoad に移動した時、バラバラにならないように、Main というルート GameObject の中に全てを入れておきます。
Camera .. BgRoot を表示するため入れました
EventSystem .. 全てのシーンに入れてたら、必要なし
BgRoot .. アプリケーションで表示する背景です
VoiceClip .. 音声データ登録
DebugDisp .. デバッグ用の情報表示
Main に Main.cs をアタッチ
この状態になったら新しいシーンを作成し、実行します。
以下のように、どのシーンから開始しても DontDestroyOnLoad に Main シーンが作成&実行されます。