Scene Flow
Async scene loading with a loading screen, cancellation, and additive multi-scene support.
One awaitable API for scene transitions · Automatic loading screen, progress events, cooperative cancel · Additive multi-scene layering built in · No-op replacement:
NullSceneService
CGS moves your game between scenes through ISceneService — one awaitable API for every menu-to-gameplay-to-menu transition. You call await _scene.LoadAsync("Gameplay") and the service coordinates the rest: it shows your loading screen automatically, holds it on screen long enough to avoid a one-frame flash, publishes progress events for your progress bar, and supports cooperative cancellation (the player presses Esc, the load stops).
The same service also handles additive scenes — scenes loaded on top of the current one instead of replacing it, which is how you layer a UI scene or stream in a neighboring area. Single-mode and additive loading are two surfaces of one service; there is no extra setup.
What it does
- One async call per transition.
LoadAsyncreturns aTask<SceneLoadResult>you canawait. Success and failure come back as a result object; cancellation and timeout throwOperationCanceledException. - Automatic loading screen. Pass a panel in the options and the service pushes it before the load and pops it after, with a configurable minimum display time.
- Progress events. The service publishes load started / progress / completed / canceled events on the Event Bus — the natural place to fade music out and in.
- Additive layering. Load and unload extra scenes on top of the base scene, inspect the full list of loaded scenes, and choose which one Unity treats as active.
Getting the service
Resolve once and cache the reference (never inside Update):
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)]
public class MenuController : MonoBehaviour
{
private ISceneService _scene;
private void Awake()
{
_scene = ServiceLocator.Resolve<ISceneService>();
}
}
Loading a scene (single mode)
Single-mode loading replaces the current scene — the standard main-menu-to-gameplay transition.
Task<SceneLoadResult> LoadAsync(string sceneName,
SceneLoadOptions opts = default,
CancellationToken ct = default)
Loads a scene by name. The scene must be registered in Build Settings (File > Build Settings > Scenes in Build). Returns a SceneLoadResult on success or error; throws OperationCanceledException on cancel or timeout.
Task<SceneLoadResult> ReloadCurrentAsync(SceneLoadOptions opts = default,
CancellationToken ct = default)
Reloads the currently active scene — a convenience for checkpoint respawns and debug reloads.
Options (SceneLoadOptions)
| Field | What it does |
|---|---|
LoadingScreenPanel | An optional panel (IPanel from the UI Framework) that the service pushes when the load starts and pops when it finishes. |
MinDisplayDuration | Minimum seconds to keep the loading screen visible. Prevents a jarring flash when the load finishes in a few frames. |
LoadTimeoutSeconds | Per-call timeout override. 0 (the default) uses the service-wide setting. On expiry the load throws OperationCanceledException with a TimeoutException inside. |
Result (SceneLoadResult)
| Property | Meaning |
|---|---|
Succeeded | true when the scene activated normally. |
Error | The captured exception when Succeeded is false (for example, the scene is missing from Build Settings). null on success. |
SceneName | The scene that was the load target. |
TotalDuration | End-to-end load time as a TimeSpan. |
Load phases and queries
A load moves through phases you can observe at any time:
string ActiveSceneName { get; } // Active scene name; empty before the first load completes
bool IsLoading { get; } // true while any scene operation is in flight
LoadPhase CurrentPhase { get; } // Idle, Unloading, Loading, MinDisplayHold, Activating, Active
Events published
Subscribe through the Event Bus:
| Event | When | Typical use |
|---|---|---|
SceneLoadStarted(FromSceneName, ToSceneName) | A load began | Fade out the old music |
SceneLoadProgress(SceneName, Progress, Phase) | Progress updates, Progress in 0..1 | Drive a progress bar |
SceneLoadCompleted(FromSceneName, ToSceneName, TotalDuration) | The load finished | Fade in the new music |
SceneLoadCanceled(FromSceneName, AttemptedSceneName, PhaseAtCancel, ElapsedBeforeCancel, Cause) | The load was canceled or timed out | Return to the menu cleanly |
Additive scenes
An additive load adds a scene to the ones already loaded instead of replacing them. Use it for a persistent UI scene over gameplay, a streamed neighboring area, or a shared "managers" scene.
Task<SceneLoadResult> LoadAdditiveAsync(string sceneName,
SceneLoadOptions opts = default,
CancellationToken ct = default)
Loads a scene additively on top of the base scene. The active scene does not change — ActiveSceneName still reports the base scene. Loading a scene that is already additively loaded is a safe no-op: the service logs a warning and returns a completed task (there is no reference counting — each scene is loaded at most once).
Task UnloadAdditiveAsync(string sceneName, CancellationToken ct = default)
Unloads one additively loaded scene. Passing the base scene's name, or a name that is not loaded, is a safe no-op with a warning — the base scene can never be removed this way (use LoadAsync to replace it).
IReadOnlyList<string> ActiveSceneList { get; }
Every loaded scene: the base scene first, then the additive scenes in the order they were loaded. Each read returns a fresh snapshot you can keep or mutate freely.
bool SetActiveScene(string sceneName)
Tells Unity which loaded scene is "active" — the scene that receives newly instantiated objects and drives lighting settings. The scene must already appear in ActiveSceneList; returns false (with a warning, no exception) if it is not loaded.
Behavior worth knowing:
- Additive loads skip the loading screen.
LoadingScreenPanelandMinDisplayDurationare ignored for additive operations (with a log message if you set them) — additive loads are meant to be light and seamless. - The same events fire. Additive loads and unloads publish the same
SceneLoadStarted/SceneLoadProgress/SceneLoadCompletedevents as single-mode loads. You can tell them apart becauseActiveSceneNamedoes not change during an additive operation. - A single-mode load clears the additive list. When
LoadAsynccompletes, Unity has unloaded every previous scene — the service resetsActiveSceneListto just the new base scene. - Still one operation at a time. Additive loads and unloads share the same in-flight gate as single-mode loads: while
IsLoadingistrue, starting another operation is rejected.
Full example
using System;
using System.Threading;
using System.Threading.Tasks;
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)]
public class GameFlow : MonoBehaviour
{
private ISceneService _scene;
private IPanel _loadingScreen;
private CancellationTokenSource _menuCts;
private void Awake()
{
_scene = ServiceLocator.Resolve<ISceneService>();
_loadingScreen = GetComponent<IPanel>(); // Your loading screen panel
}
// --- Single-mode: menu -> gameplay with a loading screen ---
public async void OnPlayButtonClicked()
{
_menuCts = new CancellationTokenSource();
try
{
var opts = new SceneLoadOptions
{
LoadingScreenPanel = _loadingScreen,
MinDisplayDuration = 1.0f // At least 1 second on screen
};
var result = await _scene.LoadAsync("Gameplay", opts, _menuCts.Token);
if (!result.Succeeded)
Debug.LogError($"Scene load failed: {result.Error.Message}");
}
catch (OperationCanceledException)
{
Debug.Log("Scene load was canceled (player hit Esc)");
}
}
public void OnEscapePressed()
{
_menuCts?.Cancel(); // Cancel the in-flight load
}
// --- Additive: layer a HUD scene over gameplay ---
public async Task ShowHudAsync()
{
await _scene.LoadAdditiveAsync("HudOverlay");
Debug.Log(_scene.ActiveSceneName); // "Gameplay" — unchanged
Debug.Log(string.Join(", ", _scene.ActiveSceneList)); // "Gameplay, HudOverlay"
}
public async Task HideHudAsync()
{
await _scene.UnloadAdditiveAsync("HudOverlay");
}
}
Turning it off
ServiceLocator.Replace<ISceneService>(new NullSceneService());
With the null implementation, every load call returns an already-completed success task — no actual loading, no events, no loading panel. Useful for headless testing or mocking gameplay flows. It still respects cancellation tokens (a token that is already canceled produces a canceled task).
Common pitfalls
-
Main thread only. Call the load methods and read the properties only on the main thread. The service checks this in Debug builds.
-
One scene operation at a time. You cannot start another load while
IsLoadingistrue—awaitthe first one or cancel it. This applies across single-mode loads, additive loads, and additive unloads. -
Cancellation is cooperative. Once the new scene's
Awakecalls begin (LoadPhase.Activating), cancellation is ignored and the load completes — Unity cannot safely abort a scene mid-activation. Cancel is honored during theLoadingandMinDisplayHoldphases. -
MinDisplayDurationruns on the UI clock. The minimum-display wait uses the Time service's UI clock, which keeps flowing while gameplay is paused. If you pause the UI clock during a load, the wait pauses with it. -
Scenes must be in Build Settings. A name that is not registered fails with a
SceneLoadResultwhoseErrorexplains the problem — check the result instead of assuming success. -
IL2CPP + code stripping. If your project builds with IL2CPP and your loading panel or event subscribers get stripped, add these to your project's
link.xml:<type fullname="CommonGameSystem.Core.SceneLoadResult" preserve="all"/> <type fullname="System.Threading.Tasks.Task`1[[CommonGameSystem.Core.SceneLoadResult]]" preserve="all"/>
Related pages
- Bootstrap — how the service starts automatically
- UI Framework — the panel type used for loading screens
- Event Bus — subscribing to the four scene events
- Time — the UI clock that times the minimum display hold