Appearance
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.kapish0.1.0,kids.kapish.addressables0.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_GameplayThis 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:
- Guard -- reject if a transition is already in progress
- Resolve transition -- use the explicit parameter, fall back to the group's transition, then to the director's default
- Fade out -- if enabled, fade the overlay from transparent to black
- Show loading screen -- if the transition has a loading scene, load it additively and find the
ILoadingScreencomponent - Unload previous group -- unload all build scenes and addressable scenes from the previous group, in reverse order
- Load new group -- load each scene in the group sequentially, reporting progress after each
- Set active scene -- set the designated scene as active for lighting/baking
- Enforce minimum duration -- wait if the transition completed faster than
MinimumDurationMs - Hide loading screen -- call
Hide()and unload the loading scene - 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 operationcurrentOperationProgress-- sub-progress of the current scene load (fromAsyncOperation.progressorAsyncOperationHandle.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
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.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.
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
SetActiveSceneand lighting, and sequential loading gives clean progress reporting.Minimum duration enforcement.
SceneTransition.MinimumDurationMsensures loading screens are visible long enough to be read. Without this, fast loads produce a disorienting flash.Unscaled time for fades. Fading uses
Time.unscaledDeltaTimeandTime.realtimeSinceStartupso transitions work correctly when the game is paused (Time.timeScale = 0).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.
- Create a scene called
Bootstrap(or similar) - Add an empty GameObject with a
SceneDirectorcomponent - Optionally configure the Default Transition on the SceneDirector (loading scene, fade settings)
- Set
Bootstrapas scene 0 in Build Settings - 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.signalsto 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
Canvasoverlay 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:
| Field | Default | Effect |
|---|---|---|
FadeOut | true | Fade to black before loading |
FadeIn | true | Fade from black after loading |
FadeDurationMs | 300 | Duration 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.assetThe bootstrap scene contains the SceneDirector (and other persistent singletons). Everything else loads and unloads through groups.