Skip to content

kids.kapish.scenes

unity

Technical reference for the kids.kapish.scenes Unity package -- async scene management with groups, transitions, and addressable support.

Package Identity

  • Package name: kids.kapish.scenes
  • Display name: Carrot.Scenes
  • Version: 0.1.0
  • Minimum Unity: 6000.0
  • License: MIT
  • Dependencies: kids.kapish 0.1.0, kids.kapish.addressables 0.1.0

Assembly Structure

Carrot.Scenes (Runtime)

  • Path: Runtime/Carrot.Scenes.asmdef
  • Root namespace: Carrot.Scenes
  • References: Carrot, Carrot.Precompiled, Carrot.Addressables, Unity.Addressables, Unity.ResourceManager
  • Engine references: Yes
  • Platforms: All

Contains all runtime types: SceneDirector, SceneGroup, SceneRef, SceneTransition, SceneLoadProgress, and ILoadingScreen.

Carrot.Scenes.Editor (Editor)

  • Path: Editor/Carrot.Scenes.Editor.asmdef
  • Root namespace: Carrot.Scenes.Editor
  • References: Carrot, Carrot.Scenes, Carrot.Editor
  • Platforms: Editor only

Editor support assembly. Currently a placeholder for future inspectors and tooling.

Architecture

Bootstrap Pattern

The system assumes a persistent bootstrap scene that is never unloaded. This scene contains DontDestroyOnLoad singletons (including SceneDirector itself, which inherits from MonoSingleton<T>). All gameplay content loads additively on top of the bootstrap scene.

Bootstrap (persistent, never unloaded)
  +-- SceneDirector (DontDestroyOnLoad)
  +-- AudioManager, etc.

Gameplay Group (additive, managed by SceneDirector)
  +-- Level_Terrain
  +-- Level_Lighting
  +-- Level_Gameplay

This pattern ensures singletons survive scene transitions and the bootstrap scene provides a clean initialization point.

Scene Groups

SceneGroup is a ScriptableObject (extending CarrotObject) that bundles multiple SceneRef entries into a logical unit. Each group can:

  • Contain a mix of build scenes and addressable scenes
  • Specify which scene becomes the active scene for lighting (ActiveSceneIndex)
  • Override the default transition with a per-group SceneTransition

Groups are the primary unit of scene management. A "level" might be a group of three scenes: terrain, lighting, and gameplay logic.

Transition Flow

LoadGroupAsync executes a fixed sequence:

  1. Guard -- reject if a transition is already in progress
  2. Resolve transition -- use the explicit parameter, fall back to the group's transition, then to the director's default
  3. Fade out -- if enabled, fade the overlay from transparent to black
  4. Show loading screen -- if the transition has a loading scene, load it additively and find the ILoadingScreen component
  5. Unload previous group -- unload all build scenes and addressable scenes from the previous group, in reverse order
  6. Load new group -- load each scene in the group sequentially, reporting progress after each
  7. Set active scene -- set the designated scene as active for lighting/baking
  8. Enforce minimum duration -- wait if the transition completed faster than MinimumDurationMs
  9. Hide loading screen -- call Hide() and unload the loading scene
  10. Fade in -- if enabled, fade the overlay from black to transparent

The entire method is wrapped in a try/finally that clears IsTransitioning, ensuring the flag is always reset even if an operation is cancelled.

Fade System

SceneDirector creates a full-screen fade overlay on demand: a Canvas at sort order 9999 with a black Image and a CanvasGroup for alpha control. The overlay is only created once (lazy) and toggled via SetActive. Fading uses Time.unscaledDeltaTime so it works correctly during Time.timeScale = 0.

Loading Screens

Loading screens are scenes, not prefabs. The loading scene is specified as a SceneRef on the SceneTransition. When loaded, SceneDirector finds the ILoadingScreen implementation dynamically via FindAnyObjectByType<MonoBehaviour>() and casts it. This means:

  • Loading screens can have their own cameras, canvases, and complex hierarchies
  • Multiple loading screen designs are just different scenes
  • The loading screen scene is loaded/unloaded like any other scene

The ILoadingScreen interface is intentionally minimal: Show(), Hide(), and UpdateProgress(SceneLoadProgress). Implementations handle their own animation, progress bars, tips, etc.

Scene References

SceneRef is a serializable struct that abstracts over two scene location strategies:

  • BuildScene -- scene is in Build Settings, referenced by name. Loaded via SceneManager.LoadSceneAsync.
  • Addressable -- scene is an addressable asset, referenced by key. Loaded via Addressables.LoadSceneAsync.

The SceneRefKind enum determines which path is taken. SceneRef.IsValid checks that the relevant field is non-empty. Factory methods FromBuild and FromAddressable provide clean construction.

Progress Tracking

SceneLoadProgress provides a normalised Progress value (0--1) across all operations in a transition (unloads + loads). It tracks:

  • TotalScenes -- set once at the start of a transition (previous group count + new group count)
  • LoadedScenes -- incremented after each completed operation
  • currentOperationProgress -- sub-progress of the current scene load (from AsyncOperation.progress or AsyncOperationHandle.PercentComplete)

The formula is (completedOps + currentOpProgress) / totalOps. The ProgressChanged signal dispatches on every progress update, which SceneDirector re-broadcasts via its own ProgressChanged signal.

Addressable Scene Management

Addressable scenes are tracked separately from build scenes in loadedAddressableScenes (a list of AsyncOperationHandle<SceneInstance>). On unload, the handle is checked with IsValid() before calling Addressables.UnloadSceneAsync. This dual-list approach keeps the cleanup straightforward: build scenes unload by name, addressable scenes unload by handle.

CancellationToken Support

All async methods accept a CancellationToken. Cancellation is checked:

  • After each frame wait in build scene loading loops
  • After addressable operation completion
  • During fade animation frames

This allows callers to cancel mid-transition (e.g., if the player exits to desktop). The try/finally on LoadGroupAsync ensures IsTransitioning is always cleared.

Key Design Decisions

  1. Groups, not individual scenes. The primary API is LoadGroupAsync(SceneGroup), not individual scene loading. Single-scene methods exist for edge cases but the group is the standard unit.

  2. Loading screens as scenes. Loading screens are full scenes, not prefabs. This gives maximum flexibility (own cameras, post-processing, UI) without any coupling to the scene director.

  3. Sequential loading within groups. Scenes within a group load one at a time, not in parallel. This is deliberate -- parallel scene loading in Unity can cause race conditions with SetActiveScene and lighting, and sequential loading gives clean progress reporting.

  4. Minimum duration enforcement. SceneTransition.MinimumDurationMs ensures loading screens are visible long enough to be read. Without this, fast loads produce a disorienting flash.

  5. Unscaled time for fades. Fading uses Time.unscaledDeltaTime and Time.realtimeSinceStartup so transitions work correctly when the game is paused (Time.timeScale = 0).

  6. Lazy fade overlay. The Canvas/Image/CanvasGroup overlay is created only when the first fade is requested, avoiding unnecessary objects in scenes that don't use fading.


Usage Guide

Practical guide to scene management with the kids.kapish.scenes package.

Installation

Add kids.kapish.scenes as a dependency in your package's package.json or install it via the Unity Package Manager from the Carrot registry.

json
{
  "dependencies": {
    "kids.kapish.scenes": "0.1.0"
  }
}

Reference the Carrot.Scenes assembly in your .asmdef:

json
{
  "references": ["Carrot.Scenes"]
}

Bootstrap Setup

The scene system assumes a persistent bootstrap scene that is never unloaded.

  1. Create a scene called Bootstrap (or similar)
  2. Add an empty GameObject with a SceneDirector component
  3. Optionally configure the Default Transition on the SceneDirector (loading scene, fade settings)
  4. Set Bootstrap as scene 0 in Build Settings
  5. All other scenes load additively on top of this
csharp
using Carrot.Scenes;
using UnityEngine;

public class GameBootstrap : MonoBehaviour
{
    [SerializeField] private SceneGroup startingGroup;

    async void Start()
    {
        // Bootstrap scene is already loaded.
        // Load the first gameplay group.
        await SceneDirector.Instance.LoadGroupAsync(startingGroup);
    }
}

Loading a Scene Group

Scene groups are the primary way to manage scenes. Create a SceneGroup asset via Create > Carrot > Scenes > Scene Group, add your scenes, and load it:

csharp
using Carrot.Scenes;
using UnityEngine;

public class LevelLoader : MonoBehaviour
{
    [SerializeField] private SceneGroup forestLevel;
    [SerializeField] private SceneGroup desertLevel;

    public async void LoadForest()
    {
        await SceneDirector.Instance.LoadGroupAsync(forestLevel);
    }

    public async void LoadDesert()
    {
        await SceneDirector.Instance.LoadGroupAsync(desertLevel);
    }
}

LoadGroupAsync handles everything: fading out, showing a loading screen, unloading the current group, loading the new group, and fading back in.


Transition Configuration

Each transition can be customised with a SceneTransition. You can set one on the SceneGroup asset (per-group override), pass one explicitly, or rely on the SceneDirector's default.

Priority order: explicit parameter > group's transition > director's default.

Fade Only (No Loading Screen)

Leave the loading scene field empty on the SceneTransition. The director will fade to black, swap scenes, and fade back in:

  • Fade Out: enabled
  • Fade In: enabled
  • Fade Duration: 300 ms (default)
  • Loading Scene: (none)

With Loading Screen

Set the loading scene field to a scene containing an ILoadingScreen MonoBehaviour:

  • Loading Scene: LoadingScreen (build scene name)
  • Minimum Duration: 1500 ms (ensures the loading screen is readable)

No Transition

Pass a SceneTransition with fading disabled and no loading scene for an instant cut:

csharp
SceneTransition instant = new SceneTransition();
// All defaults are off except fades, which default true --
// configure via inspector or create a shared asset
await SceneDirector.Instance.LoadGroupAsync(group, instant);

Custom Loading Screens

Loading screens are scenes, not prefabs. Create a scene with a Canvas and UI, then implement ILoadingScreen on a MonoBehaviour in that scene:

csharp
using Carrot.Scenes;
using UnityEngine;
using UnityEngine.UI;

public class MyLoadingScreen : MonoBehaviour, ILoadingScreen
{
    [SerializeField] private CanvasGroup canvasGroup;
    [SerializeField] private Slider progressBar;
    [SerializeField] private Text sceneNameText;

    public void Show()
    {
        canvasGroup.alpha = 1f;
    }

    public void Hide()
    {
        canvasGroup.alpha = 0f;
    }

    public void UpdateProgress(SceneLoadProgress progress)
    {
        progressBar.value = progress.Progress;
        sceneNameText.text = progress.CurrentSceneName;
    }
}

SceneDirector finds the implementation automatically when the loading scene activates. The loading scene is loaded and unloaded just like any other scene.


Progress Tracking

Subscribe to progress events for UI updates outside of a loading screen:

csharp
using System;
using Carrot.Scenes;
using UnityEngine;

public class TransitionHUD : MonoBehaviour
{
    private Action unsubProgress;
    private Action unsubStart;
    private Action unsubComplete;

    void OnEnable()
    {
        unsubProgress = SceneDirector.Instance.ProgressChanged.Add(HandleProgress);
        unsubStart    = SceneDirector.Instance.GroupLoadStarted.Add(HandleStart);
        unsubComplete = SceneDirector.Instance.GroupLoadCompleted.Add(HandleComplete);
    }

    void OnDisable()
    {
        unsubProgress?.Invoke();
        unsubStart?.Invoke();
        unsubComplete?.Invoke();
    }

    private void HandleStart(SceneGroup group)
    {
        Debug.Log($"Loading {group.GroupName}...");
    }

    private void HandleProgress(SceneLoadProgress progress)
    {
        Debug.Log($"[{progress.Progress:P0}] {progress.CurrentSceneName} ({progress.LoadedScenes}/{progress.TotalScenes})");
    }

    private void HandleComplete(SceneGroup group)
    {
        Debug.Log($"{group.GroupName} loaded.");
    }
}

Add kids.kapish.signals to your asmdef references if using these signals from your own code.


Single Scene Loading

For scenes outside the group system (e.g., debug overlays, temporary UI):

csharp
using Carrot.Scenes;
using UnityEngine.SceneManagement;

// Load a build scene additively
await SceneDirector.Instance.LoadSceneAsync(
    SceneRef.FromBuild("DebugOverlay"),
    LoadSceneMode.Additive);

// Unload it later
await SceneDirector.Instance.UnloadSceneAsync("DebugOverlay");

// Check if a scene is loaded
if (SceneDirector.Instance.IsSceneLoaded("DebugOverlay"))
{
    // already up
}

Addressable Scenes

Scenes can be loaded via the Addressables system instead of Build Settings. Use SceneRef.FromAddressable when constructing references in code, or set the Kind to Addressable in the inspector on a SceneGroup entry:

csharp
using Carrot.Scenes;
using Carrot.Addressables;
using UnityEngine.SceneManagement;

// Load an addressable scene directly
await SceneDirector.Instance.LoadAddressableSceneAsync(
    new AssetAddress("DLC_Level_01"),
    LoadSceneMode.Additive);

Addressable scenes in a SceneGroup work transparently -- the director handles the different load/unload paths internally.


Cancellation

All async methods accept a CancellationToken. Use this to cancel mid-transition if the player quits or if you need to abort:

csharp
using System.Threading;
using Carrot.Scenes;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    private CancellationTokenSource transitionCts;

    public async void LoadLevel(SceneGroup group)
    {
        transitionCts?.Cancel();
        transitionCts = new CancellationTokenSource();

        try
        {
            await SceneDirector.Instance.LoadGroupAsync(group, ct: transitionCts.Token);
        }
        catch (OperationCanceledException)
        {
            Debug.Log("Transition cancelled.");
        }
    }

    void OnDestroy()
    {
        transitionCts?.Cancel();
        transitionCts?.Dispose();
    }
}

Fade Transitions

Fading is built into the group transition flow, but here is what happens under the hood:

  • A full-screen black Canvas overlay is created lazily on first use (sort order 9999)
  • Fade uses Time.unscaledDeltaTime, so it works during pause (Time.timeScale = 0)
  • The overlay is hidden (SetActive(false)) when fully transparent

The fade behaviour is controlled entirely through SceneTransition:

FieldDefaultEffect
FadeOuttrueFade to black before loading
FadeIntrueFade from black after loading
FadeDurationMs300Duration of each fade (0--2000 ms)

Typical Project Layout

Assets/
  Scenes/
    Bootstrap.unity          <-- Scene 0, never unloaded
    Loading.unity            <-- Loading screen scene
    MainMenu.unity
    Forest_Terrain.unity
    Forest_Lighting.unity
    Forest_Gameplay.unity
  ScriptableObjects/
    Scenes/
      MainMenu.asset         <-- SceneGroup (1 scene)
      ForestLevel.asset      <-- SceneGroup (3 scenes)
      DefaultTransition.asset

The bootstrap scene contains the SceneDirector (and other persistent singletons). Everything else loads and unloads through groups.

Carrot