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

Upgrading from 4.x to 5.x

Start here: the headline call has not changed.

MySceneManager.TransitionAsync("my-target-scene", "my-loading-scene"); // 4.x and 5.x, identical

If that is most of what your project does, your migration is small. Most projects face renames and dropped arguments — find-and-replace scale, not a rearchitecture.

Addressable calls now look identical to non-addressable ones. A bare string resolves itself, so the *AddressableAsync family is gone rather than renamed:

MySceneManager.TransitionAsync("target", "loading"); // build settings
MySceneManager.TransitionAsync("target-address", "loading-address"); // Addressables
MySceneManager.TransitionAsync(SceneRef.Address("target"), "loading"); // forced, and the fast path

There is no compatibility layer — no [Obsolete] shims, no forwarding methods. That matches what 3.0 and 4.0 both did, and it means every call site that needs changing produces a plain compile error at exactly the line to change. 4.x receives no further maintenance; the answer to a 4.x bug report is to upgrade.

Asset Store users

Remove the previous version completely before importing 5.0. This has always been true, but a major version makes it more likely to bite.

Key changes

  • 64 public async methods became 4. Every reference kind, arity and host is reachable through SceneParameters' implicit conversions instead of its own method.
  • SceneRef replaces ILoadSceneInfo and its five implementing structs — one non-boxing value type for names, paths, addresses, build indices, AssetReferences and Scenes.
  • A bare string resolves itself, against the build settings first and Addressables second.
  • Every operation returns a SceneOperation instead of a Task<SceneResult>: progress, cancellation, phase and per-scene events all live on the handle.
  • CancellationToken and IProgress<float> are gone from the public API.
  • ISceneBackend replaces ISceneData and IAsyncSceneOperation, so backend selection happens once per operation rather than at every call site.
  • Loading screens no longer have to be scenesLoadingScreen covers prefabs and UI Toolkit documents too.
  • SceneManagerLog gives the package one configurable, routable logging layer.
  • Fixed: LoadingProgress no longer throws when a transition is started twice.

Removed types and their replacements

This is the table to read first. Method renames are one IntelliSense keystroke away; removed types are not — LoadSceneInfoName does not autocomplete to SceneRef.

ILoadSceneInfo and the LoadSceneInfo* structs → SceneRef

Also covers LoadSceneInfoType.

// 4.x
ILoadSceneInfo byName = new LoadSceneInfoName("sceneA");
ILoadSceneInfo byPath = new LoadSceneInfoName("Assets/Scenes/sceneA.unity");
ILoadSceneInfo byIndex = new LoadSceneInfoIndex(1);
ILoadSceneInfo byScene = new LoadSceneInfoScene(someScene);
ILoadSceneInfo byAddress = new LoadSceneInfoAddress("sceneA");
ILoadSceneInfo byAsset = new LoadSceneInfoAssetReference(assetReference);

// 5.x
SceneRef byName = "sceneA"; // implicit
SceneRef byPath = "Assets/Scenes/sceneA.unity"; // implicit
SceneRef byIndex = 1; // implicit
SceneRef byScene = someScene; // implicit
SceneRef byAddress = SceneRef.Address("sceneA"); // explicit: forces Addressables
SceneRef byAsset = assetReference; // implicit

Most of the time you will not name SceneRef at all — the conversions mean you pass the string, index or AssetReference straight to the operation.

ISceneData, SceneData*, SceneDataBuilder, SceneDataUtilitiesISceneBackend

Also covers IAsyncSceneOperation, AsyncSceneOperationStandard and AsyncSceneOperationAddressable.

// 4.x — half-implemented by design: each type warned when you called the wrong half
public interface ISceneData
{
IAsyncSceneOperation AsyncOperation { get; }
void SetSceneReferenceManually(Scene scene); // warns on the addressable implementation
void UpdateSceneReference(); // warns on the standard implementation
// ...
}

// 5.x — every method is meaningful on every implementation
public interface ISceneBackend
{
bool CanHandle(SceneRefKind kind);
SceneBackendHandle Load(SceneRef sceneRef);
SceneBackendHandle Unload(SceneBackendHandle handle);
float GetProgress(SceneBackendHandle handle);
bool IsDone(SceneBackendHandle handle);
bool TryResolveScene(SceneBackendHandle handle, out Scene scene);
}

Register your own with SceneBackendRegistry.Register(backend); it takes precedence over the built-in backends for the kinds it claims.

WaitTask<T> and TaskExtensionsSceneOperation.ToCoroutine()

// 4.x
yield return MySceneManager.LoadAsync("sceneA").ToWaitTask();
yield return new WaitTask<SceneResult>(MySceneManager.LoadAsync("sceneA"));

// 5.x
yield return MySceneManager.LoadAsync("sceneA").ToCoroutine();

SceneManagerExtensions → deleted

The 698 lines of extension methods existed to spell out every combination of operation, arity and reference kind. SceneParameters' implicit conversions replace all of them; see the method table below.

LoadingProgress.TransitionInTask / TransitionOutTaskWaitForShowAsync() / WaitForHideAsync()

These were public TaskCompletionSource<bool> fields, so any consumer could complete them and desynchronise the transition. If you were reading them to find out when a transition finished a phase, use SceneOperation.StateChanged instead — see Watching a transition.

// 4.x
await loadingBehavior.Progress.TransitionInTask.Task;

// 5.x
await loadingBehavior.Progress.WaitForShowAsync();
bool shown = loadingBehavior.Progress.IsShown;

Every 4.x method and its 5.x equivalent

Each group leads with the case that does not change.

Load

4.x5.x
LoadAsync(sceneParameters, progress, token)LoadAsync(sceneParameters) + op.Progressed / op.CancelWith(token)
LoadAsync(string sceneName, bool setActive, ...)LoadAsync(sceneName) — or LoadAsync(new SceneParameters(sceneName, setActive: true))
LoadAsync(string[] sceneNames, int setIndexActive, ...)LoadAsync(sceneNames) — or LoadAsync(new SceneParameters(sceneNames, setIndexActive))
LoadAsync(int buildIndex, bool setActive, ...)LoadAsync(buildIndex) — or LoadAsync(new SceneParameters((SceneRef)buildIndex, true))
LoadAsync(int[] buildIndices, int setIndexActive, ...)LoadAsync(buildIndices) — or LoadAsync(new SceneParameters(buildIndices, setIndexActive))
LoadAddressableAsync(string address, bool setActive, ...)LoadAsync(SceneRef.Address(address))
LoadAddressableAsync(string[] addresses, int setIndexActive, ...)LoadAsync(new SceneParameters(addresses.Select(SceneRef.Address).ToArray(), setIndexActive))
LoadAddressableAsync(AssetReference assetReference, bool setActive, ...)LoadAsync(assetReference)
LoadAddressableAsync(AssetReference[] assetReferences, int setIndexActive, ...)LoadAsync(assetReferences) — or LoadAsync(new SceneParameters(assetReferences, setIndexActive))

A bare address only needs SceneRef.Address(...) when the same name also exists in your build settings; otherwise LoadAsync(address) resolves to Addressables on its own.

Unload

4.x5.x
UnloadAsync(sceneParameters, token)UnloadAsync(sceneParameters)
UnloadAsync(string sceneName, token)UnloadAsync(sceneName)
UnloadAsync(string[] sceneNames, token)UnloadAsync(sceneNames)
UnloadAsync(int buildIndex, token)UnloadAsync(buildIndex)
UnloadAsync(int[] buildIndices, token)UnloadAsync(buildIndices)
UnloadAsync(Scene scene, token)UnloadAsync(scene)
UnloadAsync(Scene[] scenes, token)UnloadAsync(scenes)
UnloadAddressableAsync(string address, token)UnloadAsync(SceneRef.Address(address))
UnloadAddressableAsync(string[] addresses, token)UnloadAsync(addresses.Select(SceneRef.Address).ToArray())
UnloadAddressableAsync(AssetReference assetReference, token)UnloadAsync(assetReference)
UnloadAddressableAsync(AssetReference[] assetReferences, token)UnloadAsync(assetReferences)

Transition

4.x5.x
TransitionAsync(sceneParameters, intermediateSceneReference, token)TransitionAsync(sceneParameters, loadingScreen)
TransitionAsync(string target, string loading, token)TransitionAsync(target, loading)unchanged
TransitionAsync(string[] targets, string loading, int setIndexActive, token)TransitionAsync(new SceneParameters(targets, setIndexActive), loading)
TransitionAsync(int target, int loading, token)TransitionAsync(target, loading)unchanged
TransitionAsync(int[] targets, int loading, int setIndexActive, token)TransitionAsync(new SceneParameters(targets, setIndexActive), loading)
TransitionAddressableAsync(string target, string loading, token)TransitionAsync(SceneRef.Address(target), SceneRef.Address(loading))
TransitionAddressableAsync(string[] targets, string loading, int setIndexActive, token)TransitionAsync(new SceneParameters(targets.Select(SceneRef.Address).ToArray(), setIndexActive), SceneRef.Address(loading))
TransitionAddressableAsync(AssetReference target, AssetReference loading, token)TransitionAsync(target, loading)
TransitionAddressableAsync(AssetReference[] targets, AssetReference loading, int setIndexActive, token)TransitionAsync(new SceneParameters(targets, setIndexActive), loading)

setIndexActive defaulted to 0 on every 4.x transition overload, and a transition still activates index 0 unless you say otherwise — so dropping the argument keeps the same behaviour.

Reload

4.x5.x
ReloadActiveSceneAsync(intermediateSceneReference, token)ReloadActiveSceneAsync(loadingScreen)
ReloadActiveSceneAsync(string loadingSceneName, token)ReloadActiveSceneAsync(loadingSceneName)unchanged
ReloadActiveSceneAsync(int loadingBuildIndex, token)ReloadActiveSceneAsync(loadingBuildIndex)unchanged
ReloadActiveSceneAddressableAsync(string loadingAddress, token)ReloadActiveSceneAsync(SceneRef.Address(loadingAddress))
ReloadActiveSceneAddressableAsync(AssetReference loadingAssetReference, token)ReloadActiveSceneAsync(loadingAssetReference)

Awaiting, progress and cancellation

Everything that used to be a constructor argument is now something you attach to the handle.

// 4.x
var progress = new Progress<float>(p => bar.value = p);
var cts = new CancellationTokenSource();
Task<SceneResult> task = MySceneManager.LoadAsync("sceneA", progress: progress, token: cts.Token);
SceneResult result = await task;

// 5.x
SceneOperation op = MySceneManager.LoadAsync("sceneA");
op.Progressed += p => bar.value = p;
SceneResult result = await op;

await op needs no Task. If you need one for third-party interop, op.AsTask() gives you one.

Cancellation has one mechanism now:

op.Cancel(); // stops this operation
op.CancelWith(destroyCancellationToken); // opt-in bridge for structured concurrency
note

Unity scene operations cannot be aborted — 4.x's own documentation said so on all 64 methods, and the token only ever cancelled the await. Cancel() stops progress reporting, skips the remaining phases and completes the operation in Canceled; the underlying load still finishes.

Watching a transition

A SceneOperation reports which phase it is in, which is what previously required reaching into a LoadingBehavior and calling ContinueWith on a publicly exposed TaskCompletionSource:

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

op.StateChanged += o =>
{
if (o.State == SceneOperationState.ScreenOut)
BeginIntroAnimation(); // the loading screen has finished hiding
};

await op;

States run Pending → Resolving → ScreenIn → Unloading → Loading → Activating → ScreenOut → Completed, and an operation skips the phases its kind has no use for.

Custom loading screens

A loading screen no longer has to be a scene. Everything that worked in 4.x still works — a scene name, path, address, build index, Scene or AssetReference all convert to a scene-based screen — and you can now write your own:

public class MyScreen : LoadingScreen
{
public override SceneOperationPump.ConditionAwaiter PrepareAsync(LoadingScreenHost host, SceneOperation op) { /* instantiate into host */ }
public override SceneOperationPump.ConditionAwaiter ShowAsync(SceneOperation op) { /* gate transition-in */ }
public override void ReportProgress(float progress) { /* drive the UI */ }
public override SceneOperationPump.ConditionAwaiter HideAsync(SceneOperation op) { /* gate transition-out */ }
public override void Dispose() { /* tear it down */ }
}

await MySceneManager.TransitionAsync("target", new MyScreen());

LoadingScreenHost is a package-owned scene that exists for the length of one transition, so a screen that instantiates something has somewhere to put it that survives the outgoing scene being unloaded. It also replaces 4.x's internal temp-transition-scene.

The Loading Scene Examples sample ships PrefabLoadingScreen and UIDocumentLoadingScreen as reference implementations to copy.

String resolution and its precedence

A bare string is resolved when the operation starts:

  1. Build settings, by name or path. One dictionary lookup, synchronous, and the common case.
  2. Addressables, if the build settings do not have it and Addressables is installed. This needs the catalog, so it is asynchronous — the first addressable-by-string load pays catalog-initialisation latency, and later loads of any key hit a cache.
  3. Neither → an exception naming both places we looked.

The build settings win. If Level1 exists in both, LoadAsync("Level1") loads the build settings one, and SceneRef.Address("Level1") is the override.

Resolution is observable behaviour

Adding a scene to the build settings later can flip a string from the addressable backend to the standard one, with no code change. A key matching both is reported at Warning level, and every first resolution is logged at Verbose, so this is diagnosable rather than mysterious.

Logging

The package now has one logging layer instead of nine scattered Debug.LogWarning calls.

SceneManagerLog.Level = SceneLogLevel.Verbose; // Off | Error | Warning | Info | Verbose
SceneManagerLog.Handler = myLogHandler; // route into an in-game console or analytics

It defaults to Warning in development builds and Error in release, and is settable at runtime so a shipped build can be raised to diagnose a live problem. Define MSM_DISABLE_LOGGING to strip the layer entirely.

Verbose is where the scene-linking layer narrates itself — which reference resolved to what, and which loaded scene got linked to which reference. That is historically the sharpest part of the package, so it is worth turning on when something links wrongly.

A note on progress

Progress means slightly different things per backend, and always has. Addressables' progress spans download, load and activation; the standard path covers load only. A group mixing the two therefore advances unevenly. This is documented rather than corrected — rescaling one to match the other would be inventing a number neither backend reports.