📚

Unityの最強シーン遷移管理フレームワーク『Navigathena』と画面管理の設計思想について解説

に公開
5
GitHubで編集を提案

Discussion

KomoriGameDevKomoriGameDev

貴重な知見の共有ありがとうございます。
Navigathenaの設計思想に共感して使わせてもらってます。

シーン遷移演出の機能を使っていて疑問点があり質問させて頂きます。ITransitionDirectorの使用例でコンストラクタにCanvasGroupの参照を渡している部分を参考に、自分でも同じ用に画面遷移元にあるCanvasGroupの参照を渡して試してみましたが、画面が切り替わる瞬間に以下の警告がでました。

DOTWEEN ► Tween startup failed (NULL target/property - ): the tween will now be killed ► The object of type 'CanvasGroup' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
以下略…

おそらく、CanvasGroupがあるシーンがアンロードされてしまいアクセスできなくなってしまったこと原因だと思うのですが、シーンをまたいでCanvasGroupにアクセスし続けられることにするとなるとDontDestroyにするか、FAQにある「アプリケーションの寿命を通して、常に存在するシーン」に置くことなどが考えられますが、作者さん的にはどのように使用されることを想定されていますでしょうか?
ご教授いただけましたら幸いです。

MakihiroMakihiro

使っていただき、ありがとうございますm (_ _)m
確かにサンプルが動かないのはうっかりしていました…!

Canvas系の要素(およびGameObject)を使用したITransitionDirectorの実装法としては、遷移元にはインスタンスを置かない実装になります。(ご指摘の通り、遷移元はアンロードされるので)

なので方法としては、以下のような感じになります。

  • 遷移演出用のシーンをロード・作成する
  • シングルトンな遷移演出用のCanvasを用意し、DontDestroyOnLoadなどで別のシーンに逃がす

今作っているゲームから「 遷移演出用のシーンをロード・作成する」の例を出すと以下のような実装になります。

SimpleLoadingTransitionDirectorが事前に作成されたシーンを読み込み、そのシーンからSimpleLoadingTransitionDirectorBehaviourコンポーネントを見つけ、演出を実行しています。(シーンの扱いにはISceneIdentifierを使用していますが、SceneManagerなど一般的なものでも構いません)

namespace MackySoft.UntitledNewGame.Transitions
{

	public sealed class SimpleLoadingTransitionDirectorBehaviour : MonoBehaviour
	{

		[SerializeField]
		float m_FadeDuration = 0.5f;

		[SerializeField]
		Ease m_FadeEase = Ease.OutCubic;

		[SerializeField]
		CanvasGroup m_CanvasGroup;

		public UniTask StartTransition (CancellationToken cancellationToken = default)
		{
			return m_CanvasGroup.DOFade(1f, m_FadeDuration)
				.SetEase(m_FadeEase)
				.ToUniTask(cancellationToken: cancellationToken);
		}

		public UniTask EndTransition (CancellationToken cancellationToken = default)
		{
			return m_CanvasGroup.DOFade(0f, m_FadeDuration)
				.SetEase(m_FadeEase)
				.ToUniTask(cancellationToken: cancellationToken);
		}
	}

	public sealed class SimpleLoadingTransitionDirector : ITransitionDirector
	{

		readonly ISceneIdentifier m_SceneInfo;

		public SimpleLoadingTransitionDirector (ISceneIdentifier sceneInfo)
		{
			m_SceneInfo = sceneInfo;
		}

		public ITransitionHandle CreateHandle ()
		{
			return new SimpleLoadingTransitionDirectorHandle(m_SceneInfo);
		}

		class SimpleLoadingTransitionDirectorHandle : ITransitionHandle
		{

			readonly ISceneIdentifier m_SceneInfo;
			ISceneHandle m_SceneHandle;
			SimpleLoadingTransitionDirectorBehaviour m_Director;

			public SimpleLoadingTransitionDirectorHandle (ISceneIdentifier sceneInfo)
			{
				m_SceneInfo = sceneInfo;
			}

			public async UniTask Start (CancellationToken cancellationToken = default)
			{
				var handle = m_SceneInfo.CreateHandle();
				Scene scene = await handle.Load(cancellationToken: cancellationToken);
				if (!scene.TryGetComponentInScene(out m_Director, true))
				{
					throw new InvalidOperationException($"Scene '{scene.name}' does not have a {nameof(SimpleLoadingTransitionDirectorBehaviour)} component.");
				}

				m_SceneHandle = handle;

				await m_Director.StartTransition(cancellationToken);
			}

			public async UniTask End (CancellationToken cancellationToken = default)
			{
				await m_Director.EndTransition(cancellationToken);
				m_Director = null;

				await m_SceneHandle.Unload(cancellationToken: cancellationToken);
				m_SceneHandle = null;
			}
		}
	}
}

もう一つ例を出すと、「シーンを新たに作って、そこにオブジェクトを逃がす方法」があります。この例はスプラッシュ画面に表示されるアートワークをフェードさせるITransitionDirectorです。ゲーム起動時、スプラッシュ画面のアートワークは最初から表示されているので、それをシーン遷移演出として扱い、フェードアウトさせる機能ですね。

この例ではスプラッシュシーンに最初から存在しているCanvasGroupを渡してSplashTransitionDirectorを生成し、SplashTransitionDirectorHandleではCanvasGroupを新たなシーンに逃がすようにしています。これは特殊な処理をしていて、遷移演出用のシーンを破棄するときにアートワークのインスタンスも破棄されるので、Popされない前提で作っています。(Popは履歴に残っているITransitionDirectorを利用するので、原則としてITransitionDirectorは再利用可能になっている必要があります)

namespace MackySoft.UntitledNewGame.Transitions
{
	public class SplashTransitionDirector : ITransitionDirector
	{

		readonly CanvasGroup m_RootCanvasGroup;

		public SplashTransitionDirector (CanvasGroup rootCanvasGroup)
		{
			if (rootCanvasGroup == null)
			{
				throw new ArgumentNullException(nameof(rootCanvasGroup));
			}
			m_RootCanvasGroup = rootCanvasGroup;
		}

		public ITransitionHandle CreateHandle ()
		{
			return new SplashTransitionDirectorHandle(m_RootCanvasGroup);
		}

		class SplashTransitionDirectorHandle : ITransitionHandle
		{

			const string kSceneName = "SplashTransition";

			readonly CanvasGroup m_RootCanvasGroup;

			public SplashTransitionDirectorHandle (CanvasGroup rootCanvasGroup)
			{
				m_RootCanvasGroup = rootCanvasGroup;
			}

			public UniTask Start (CancellationToken cancellation = default)
			{
				Scene scene = SceneManager.CreateScene(kSceneName);
				SceneManager.MoveGameObjectToScene(m_RootCanvasGroup.gameObject, scene);

				return UniTask.CompletedTask;
			}

			public async UniTask End (CancellationToken cancellation = default)
			{
				await m_RootCanvasGroup.DOFade(0f,1f).ToUniTask(cancellationToken: cancellation);
				await SceneManager.UnloadSceneAsync(kSceneName).ToUniTask(cancellationToken: cancellation);
			}
		}
	}
}

まとめると、「遷移演出に使用されるものは遷移元シーンには残さないようにする」といった感じですね。

サンプルは時間があるときに直すようにしておきます…!

KomoriGameDevKomoriGameDev

ご返信いただきありがとうございます!
例示していただいたコードを参考に自分でも作ってみたいと思います。
このような有用なフレームワークが世に広まるように願っておりますm (_ _)m

真夜中真夜中

Unity6でスタメン起用させて頂いてます!
大きすぎず小さすぎない絶妙なサイズ感が気に入っています
特にVContainerとの併用が凄く凄く良いです
そのまま使うとエントリポイントが一個になりますので、IoCも含めてDIコンテナのある暮らしの良さが分かりやすく伝わってきます
さらに面倒なマルチシーン開発が一気に楽しくなったりして
素晴らしいものなのでもっと流行れと念じています
開発頑張って下さい!

MakihiroMakihiro

ご使用ありがとうございます!
業務等でもいい感じに運用できているので、VContainerとの統合はかなり会心の出来ですね~