Navigathena Scene Management Framework

repository·main·Indexed 41 days ago

https://github.com/mackysoft/navigathena

A robust scene management framework for Unity (v1.1.0) providing controlled scene transitions, history management, and lifecycle observation via SceneEntryPoints. It features support for custom transitions via ITransitionDirector, data transfer between scenes using ISceneData, Unity Addressables integration, and dependency injection support via VContainer.

Tokens
6.4K
Snippets
15
Records
18
Agent score
2%

What's inside Navigathena

  1. Manage sub-scenes in Navigathena

    main

    Navigathena does not have explicit support for "sub-scenes" (loading multiple scenes simultaneously like a HUD and a Stage).

    Recommended Pattern: Do not attempt to manage sub-scenes from the originating scene. Instead, transfer necessary data (like a stageId) to the destination scene. The destination scene's SceneEntryPoint (or SceneLifecycle) should then take responsibility for loading and unloading its own sub-scenes using UnityEngine.SceneManagement or similar tools. This keeps scene responsibilities encapsulated.

  2. How SceneNavigator and SceneEntryPoint work together

    main

    Navigathena's scene management is built on two core abstractions:

    1. ISceneNavigator: The engine that drives transitions. It manages the scene history and provides methods to move between scenes (Push, Pop, Change, etc.).
    2. ISceneEntryPoint: The observer within a scene. You place exactly one SceneEntryPoint component in each scene to handle that scene's lifecycle events (initialization, entering, exiting, and finalizing).

    When you call a transition method on a navigator, it triggers a sequence of lifecycle events across the origin and destination SceneEntryPoint instances, ensuring data is passed and transitions (like fades) are executed in the correct order.

  3. Implement custom scene transitions with ITransitionDirector

    main

    Navigathena uses the ITransitionDirector interface to define visual effects (like fades or animations) that occur during scene transitions.

    An ITransitionDirector acts as a factory that produces an ITransitionHandle. The ITransitionHandle is responsible for the actual execution of the transition effect via its Start and End methods.

    When performing a transition, the system executes the Start method of the handle first, then performs the scene loading/unloading, and finally executes the End method.

    public interface ITransitionDirector
    {
      ITransitionHandle CreateHandle ();
    }
    
    public interface ITransitionHandle
    {
      UniTask Start (CancellationToken cancellationToken = default);
      UniTask End (CancellationToken cancellationToken = default);
    }
  4. Interrupt a scene transition operation

    main

    If you need to override a transition while it is in progress (e.g., an event scene detects it is no longer valid during OnInitialize), you can call a navigation method like Pop or Push from the GlobalSceneNavigator.Instance.

    When a new transition is initiated, Navigathena cancels the CancellationTokenSource managed by the SceneNavigator. This causes the CancellationToken passed into the current lifecycle events (like OnInitialize) to enter a canceled state. Subsequent lifecycle methods, such as OnEnter, will not be called if the transition is interrupted.

    protected override async UniTask OnInitialize (ISceneDataReader reader, IProgress<IProgressDataStore> progress, CancellationToken cancellationToken)
    {
      // Assume the event has expired.
      bool isExpired = true;
    
      if (isExpired) {
        // NOTE: The cancellationToken is not passed to the transition operation because the cancellation state is canceled when the transition operation is performed.
        await GlobalSceneNavigator.Instance.Pop(CancellationToken.None);
    
        // cancellationToken.IsCancellationRequested will be true
        Debug.Log(cancellationToken.IsCancellationRequested);
      }
    }
    
    protected override UniTask OnEnter (ISceneDataReader reader, CancellationToken cancellationToken)
    {
      // This will not be called if Pop was called in OnInitialize
      Debug.Log("EventSceneEntryPoint.OnEnter");
      return UniTask.CompletedTask;
    }
  5. The Scene Transition Lifecycle sequence

    main

    When a transition operation is triggered, Navigathena executes these steps in order:

    1. OnExit of the origin ISceneEntryPoint.
    2. Start of the ITransitionHandle (from the ITransitionDirector).
    3. OnFinalize of the origin ISceneEntryPoint.
    4. Unloading of the origin scene.
    5. ExecuteAsync of the IAsyncOperation (if provided).
    6. Loading of the destination scene.
    7. OnInitialize of the destination ISceneEntryPoint.
    8. End of the ITransitionHandle.
    9. OnEnter of the destination ISceneEntryPoint.
  6. Transfer data between scenes using ISceneData

    main

    To pass information from one scene to another (e.g., a selected character ID or the state of a UI menu), use the ISceneData interface.

    • Sending Data: Pass an object implementing ISceneData as an argument to ISceneNavigator.Push. This data is available in the destination scene's ISceneEntryPoint.OnInitialize or OnEnter via the ISceneDataReader.
    • Saving/Restoring State: Use ISceneDataWriter in ISceneEntryPoint.OnExit or OnFinalize to write data. This allows you to store the state of a scene so it can be restored when the user returns to it.

    Best Practice: Define one ISceneData type per scene to maintain type safety and clarity.

    // Define the data contract
    public sealed class HomeSceneData : ISceneData
    {
      public HomeScreenType LastDisplayedScreenType { get; init; }
    }
    
    public sealed class HomeSceneEntryPoint : SceneEntryPoint
    {
      protected override UniTask OnInitialize (ISceneDataReader reader, CancellationToken cancellationToken)
      {
        // Retrieve saved state from the reader
        if (reader.TryRead(out HomeSceneData sceneData))
        {
          m_View.SetScreen(sceneData.LastDisplayedScreenType);
        }
      }
    
      protected override UniTask OnFinalize (ISceneWriter writer, CancellationToken cancellationToken)
      {
        // Store current state to be passed to the next scene or restored later
        writer.Write(new HomeSceneData
        {
          LastDisplayedScreenType = m_View.ScreenType
        });
      }
    }
  7. Implement a persistent Root scene

    main

    To have a scene that persists throughout the entire application lifespan (a "Root" scene), extend ScopedSceneEntryPoint and override the EnsureParentScope method. You can use this phase to load the root scene additively and build its LifetimeScope container.

    public sealed class MyProjectScopedSceneEntryPoint : ScopedSceneEntryPoint
    {
      const string kRootSceneName = "Root";
    
      protected override async UniTask<LifetimeScope> EnsureParentScope (CancellationToken cancellationToken)
      {
        // Load root scene additively if not loaded
        if (!SceneManager.GetSceneByName(kRootSceneName).isLoaded)
        {
          await SceneManager.LoadSceneAsync(kRootSceneName, LoadSceneMode.Additive)
            .ToUniTask(cancellationToken: cancellationToken);
        }
    
        Scene rootScene = SceneManager.GetSceneByName(kRootSceneName);
    
    #if UNITY_EDITOR
        // Reorder root scene in the hierarchy
        EditorSceneManager.MoveSceneBefore(rootScene, gameObject.scene);
    #endif
    
        // Build the root LifetimeScope container
        if (rootScene.TryGetComponentInScene(out LifetimeScope rootLifetimeScope, true) && rootLifetimeScope.Container == null)
        {
          await UniTask.RunOnThreadPool(() => rootLifetimeScope.Build(), cancellationToken);
        }
        return rootLifetimeScope;
      }
    }
  8. Install Navigathena

    main

    Navigathena requires UniTask to support async/await operations. Ensure UniTask is imported into your Unity project before installing Navigathena.

    You can install Navigathena using one of the following methods:

    Via .unitypackage

    Download a version from the official releases.

    Via Unity Package Manager

    Open the Package Manager and select Add package from git URL..., then enter: https://github.com/mackysoft/Navigathena.git?path=Assets/MackySoft/MackySoft.Navigathena

    Via Open UPM

    Use the following command: openupm add com.mackysoft.navigathena

    openupm add com.mackysoft.navigathena
  9. Use Dependency Injection with VContainer

    main

    Navigathena supports VContainer for dependency injection. Instead of inheriting from SceneEntryPoint, you can register a type that implements the ISceneLifecycle interface within your VContainer IContainerBuilder.

    To decouple the scene's lifecycle from a MonoBehaviour, use the ScopedSceneEntryPoint provided by Navigathena.

    Setup Note: Due to VContainer's lifetime scope determination timing, you must set the LifetimeScope GameObject to inactive. You can use the "Create LifetimeScope" button in the ScopedSceneEntryPoint inspector to automate this.

    public override void Configure (IContainerBuilder builder)
    {
      builder.RegisterSceneLifecycle<TitleSceneLifecycle>();
    }
  10. Integrate with Unity Addressables

    main

    Navigathena supports Unity Addressables through the AddressableSceneIdentifier. While BuiltInSceneIdentifier is used for standard scenes, AddressableSceneIdentifier allows you to treat Addressable assets as scenes, handling their loading and unloading seamlessly within the framework.

    When using AddressableSceneIdentifier, the framework wraps Addressables' LoadSceneAsync and UnloadSceneAsync calls. Note that downloading assets is not handled by Navigathena; you should manage pre-download logic separately.

    using MackySoft.Navigathena.SceneManagement;
    using MackySoft.Navigathena.SceneManagement.AddressableAssets;
    
    public static class SceneDefinitions
    {
      // Use BuiltInSceneIdentifier for splash
      public static ISceneIdentifier Splash { get; } = new BuiltInSceneIdentifier("Splash");
    
      // Use AddressableSceneIdentifier for scenes requiring Addressables dependency resolution
      public static ISceneIdentifier Title { get; } = new AddressableSceneIdentifier("Title");
      public static ISceneIdentifier Introduction { get; } = new AddressableSceneIdentifier("Introduction");
      public static ISceneIdentifier Home { get; } = new AddressableSceneIdentifier("Home");
      public static ISceneIdentifier Game { get; } = new AddressableSceneIdentifier("Game");
    }
  11. Display transition progress using IProgress<IProgressDataStore>

    main

    You can display real-time progress (e.g., loading bars or status messages) during a transition by using IProgress<IProgressDataStore>.

    1. Define a data type: Create a struct or class (e.g., MyProgressData) to hold the information you want to display.
    2. Implement IProgress in your Handle: Your ITransitionHandle should implement IProgress<IProgressDataStore>. In the Report method, use progressDataStore.TryGetData<T>(out var data) to extract your custom data and update your UI.
    3. Report progress from SceneEntryPoints: Inside ISceneEntryPoint methods like OnInitialize, use the provided IProgress<IProgressDataStore> to report updates using a ProgressDataStore<T>.
    // 1. Define data
    public readonly struct MyProgressData
    {
      public float Progress { get; }
      public string Message { get; }
      public MyProgressData (float progress, string message) { Progress = progress; Message = message; }
    }
    
    // 2. Implement progress in the handle
    sealed class SimpleTransitionHandle : ITransitionHandle, IProgress<IProgressDataStore>
    {
      void IProgress<IProgressDataStore>.Report (IProgressDataStore progressDataStore)
      {
        if (progressDataStore.TryGetData(out MyProgressData myProgressData))
        {
          m_ProgressText.text = myProgressData.Progress.ToString("P0");
          m_MessageText.text = myProgressData.Message;
          m_ProgressSlider.value = myProgressData.Progress;
        }
      }
    }
    
    // 3. Report from SceneEntryPoint
    protected override UniTask OnInitialize (ISceneDataReader reader, IProgress<IProgressDataStore> progress, CancellationToken cancellationToken)
    {
      ProgressDataStore<MyProgressData> store = new();
      progress.Report(store.SetData(new MyProgressData(0.5f, "Generate Map")));
      await m_MapGenerator.Generate(cancellationToken);
      progress.Report(store.SetData(new MyProgressData(1f, "Complete")));
    }
  12. Create a simple fade transition director

    main

    To implement a custom transition, extend ITransitionDirector and create a nested class (or separate class) that implements ITransitionHandle.

    In the example below, SimpleTransitionDirector uses a CanvasGroup and DOTween to fade the UI in and out during the transition process.

    public sealed class SimpleTransitionDirector : ITransitionDirector
    {
      readonly CanvasGroup m_CanvasGroup;
    
      public SimpleTransitionDirector (CanvasGroup canvasGroup)
      {
        m_CanvasGroup = canvasGroup;
      }
    
      public ITransitionHandle Create ()
      {
        return new SimpleTransitionHandle(m_CanvasGroup);
      }
    
      sealed class SimpleTransitionHandle : ITransitionHandle
      {
        readonly CanvasGroup m_CanvasGroup;
    
        public SimpleTransitionHandle (CanvasGroup canvasGroup)
        {
          m_CanvasGroup = canvasGroup;
        }
    
        public async UniTask Start (CancellationToken cancellationToken = default)
        {
          // Play fade-in with DOTween.
          await m_CanvasGroup.DOFade(1f, 1f).ToUniTask(cancellationToken: cancellationToken);
        }
    
        public async UniTask End (CancellationToken cancellationToken = default)
        {
          await m_CanvasGroup.DOFade(0f, 1f).ToUniTask(cancellationToken: cancellationToken);
        }
      }
    }
    
    // Usage:
    await GlobalSceneNavigator.Instance.Push(new BuiltInSceneIdentifier("MyScene"), new SimpleTransitionDirector(m_CanvasGroup));