Skip to main content
Version: 5.0.0 (pre-release)

Scene Operation

Every operation returns a SceneOperationsynchronously, before the work starts. It is a live handle on that work: what phase it is in, how far along it is, what it produced, and how to wait for it.

Why a handle and not a Task

A Task gives you one thing: the eventual result. Anything else you want to know about a scene load — how far along it is, which phase it is in, whether you can still stop it — has to be decided before the call, as extra parameters.

A SceneOperation is something you hold instead, so all of that attaches after the call:

SceneOperation op = MySceneManager.TransitionAsync("target", "loading");

op.Progressed += progress => bar.value = progress;
op.StateChanged += o => { if (o.State == SceneOperationState.ScreenOut) BeginIntro(); };

SceneResult result = await op;

This is why none of the four methods take a progress or cancellation parameter: there is somewhere better to put them.

Waiting for it

Three ways, all supported on the same handle:

SceneResult result = await op; // direct, no Task allocated
yield return op.ToCoroutine(); // from a coroutine; faults rethrow
Task<SceneResult> task = op.AsTask(); // bridge for third-party interop

await op is the primary path. GetAwaiter() returns a SceneOperationAwaiter over the operation's own continuation list — no Task, no Awaitable. Because the pump runs on the player loop, continuations resume on the main thread by construction, with no SynchronizationContext round-trip.

It is also re-awaitable — awaiting twice returns the same result, and op.Result stays readable after completion. That is the specific reason Awaitable is not used internally: its objects return to a pool after a single await.

info

AsTask() is a convenience, not a design pillar. It costs a TaskCompletionSource per call and await op does not, so reach for it only when a third-party API demands a Task.

What it reports

Member
KindWhich operation this is — Load, Unload, Transition, Reload, Composite
StateThe phase it is in
Progress0 to 1
ResultThe scenes produced, empty until completed
ExceptionWhy it faulted, or null
IsDoneWhether it finished, successfully or not

And the events:

Event
ProgressedFires when Progress moves. Not raised for unchanged values.
StateChangedFires on every State change
SceneLoaded / SceneUnloadedOnce per scene
CompletedOnce when it finishes — success, cancellation and fault alike. Subscribing after completion invokes it immediately.
note

A subscriber that throws is reported through SceneManagerLog and contained. It will not fault the operation, and it will not prevent the other subscribers or the awaiters from running.

States

PendingResolvingScreenInUnloadingLoadingActivatingScreenOutCompleted, with Canceled and Faulted as terminal alternatives.

Which of these you see depends on the operation — a plain load never reaches ScreenIn. The order follows the transition flow, which is why Unloading comes before Loading: the source scene goes away once the loading screen is up, before the target is brought in.

So "the loading screen has finished fading out, start the cutscene" is a state you subscribe to:

op.StateChanged += o =>
{
if (o.State == SceneOperationState.ScreenOut)
BeginIntroCutscene();
};

Cancelling

op.Cancel();
op.CancelWith(destroyCancellationToken); // the opt-in bridge
warning

The underlying Unity operations keep running. A scene the engine has started loading cannot be aborted, so it will finish. What stops is this operation's reporting, its remaining phases, and its waiters.

Combining

SceneOperation both = SceneOperation.WhenAll(first, second);
SceneOperation any = SceneOperation.WhenAny(first, second);

Prefer these over Task.WhenAll on AsTask(): they run over the operations' own continuation lists, so they do not allocate a Task per operation.

Progress

Progress is the average across every scene in the operation.

warning

A group mixing backends advances unevenly. Addressables includes download time in its progress and the standard path does not, so a mixed group is not a straight line. Treat it as a progress bar, not a clock.

note

SceneOperation is deliberately not pooled. This API encourages you to keep the handle — op.Result after completion and awaiting twice are both supported — so nothing can know when it is free. That is one small allocation per operation, against the tens of kilobytes a scene load costs. The per-operation buffers are pooled.