A package that standardizes scene loading operations between the Unity Scene Manager and Addressables, allowing multiple alternatives of awaiting such as Coroutines, Async or UniTask.
- Installation
- Dependencies
- Overview
- Usage
- Tests
This package is available on the OpenUPM registry. Add the package via the openupm-cli:
openupm add com.mygamedevtools.scene-loader
Installing from Git (requires Git installed and added to the PATH)
- Open
Edit/Project Settings/Package Manager
. - Click +.
- Select
Add package from git URL...
. - Paste
https://github.com/mygamedevtools/scene-loader.git#upm
into url. - Click
Add
.
The package works without any dependencies, but it supports integration with some packages. If you wish to use it with Addressables, UniTask or TextMeshPro, make sure you install the packages:
com.unity.addressables
>= 1.19.0com.unity.textmeshpro
>= 2.2.0com.cysharp.unitask
* >= 2.0.0
*Installed via UPM or OpenUPM. Check the package documentation for more details.
Loading scenes in Unity is very simple, mostly, but when you start to deal with other systems such as Unity Addressables, it can get a little messy. Also, there are some common scene load scenarios that you'd usually reimplement every project, like scene transitions.
In this package, you'll have the possibility to standardize the scene loading process between the standard Unity Scene Manager and Addressables, while still being able to choose how to await (if you want) the operations, be it Coroutines, standard Async (through ValueTasks) or UniTask.
Aside from the ordinary Load and Unload actions, the Scene Loading tools introduce the Transition as a new standard to control transitions between scenes with an optional intermediate "loading scene" in between.
ℹ️ You don't need to understand what Addressables or UniTask do in order to use this package. There are scene loaders that only rely on basic Unity Engine functionalities.
Loading scenes with this package implies that the scenes will always be loaded as Additive. That is simply because there is no advantage in loading scenes in the Single load scene mode when you expect to work with multiple scenes.
In order to standardize how the scenes are loaded, you'll be using ISceneLoader
, ISceneManager
and ILoadSceneInfo
objects.
The ISceneManager
interface exposes a few methods and events to standardize the scene load operations:
public interface ISceneManager
{
event Action<Scene, Scene> ActiveSceneChanged;
event Action<Scene> SceneUnloaded;
event Action<Scene> SceneLoaded;
int SceneCount { get; }
void SetActiveScene(Scene scene);
ValueTask<Scene> LoadSceneAsync(ILoadSceneInfo sceneInfo, bool setActive = false, IProgress<float> progress = null);
ValueTask<Scene> UnloadSceneAsync(ILoadSceneInfo sceneInfo);
Scene GetActiveScene();
Scene GetLoadedSceneAt(int index);
Scene GetLastLoadedScene();
Scene GetLoadedSceneByName(string name);
}
You can find many similarities between Unity's SceneManager class, and that's both for maintaining an easy learning curve as well as because some of these operations will end up calling the Unity Scene Manager internally (like SetActiveScene
for instance).
There's also the ILoadSceneInfo
interface showing up there, but we will get to that in a moment.
The package includes two scene managers:
- The
SceneManager
, for standard scene loading. - The
SceneManagerAddressable
, for addressable scene loading.
You can also use their implementation as a reference to build your own Scene Manager.
Note that, scenes loaded by a scene manager are in a local scope, which means that if you plan to work with multiple scene managers, they will not be aware of the others' scenes. In this context, the Unity Scene Manager would be something like a global scope scene manager, since it's aware of every scene loaded in runtime.
Speaking of multiple scene managers, you can use a SceneManager
and a SceneManagerAddressable
at the same time, just keep in mind they will have their own contexts in isolation to the other.
flowchart RL
subgraph usm [Unity Scene Manager]
a1((Scene Manager)) --> b1[Scene A]
a1 --> b2[Scene B]
a2((Scene Manager Addressable)) --> c1[Scene X]
a2 --> c2[Scene Y]
end
The ISceneManager
interface defines that both LoadSceneAsync
and UnloadSceneAsync
methods return a ValueTask<Scene>
.
This means you can await those methods if they are implemented with the async keyword, or you can also subscribe to the SceneLoaded
or SceneUnloaded
events to receive the same Scene
you would via the async methods.
Both these methods also receive an ILoadSceneInfo
object.
So, instead of having multiple methods for receiving the scene's build index or the scene's name, we simply have an object instead.
As its name states, these objects hold references to a scene to be loaded (or unloaded) and are able to validate whether they are a reference to a loaded scene.
The ILoadSceneInfo
interface simply defines:
public interface ILoadSceneInfo
{
object Reference { get; }
bool IsReferenceToScene(Scene scene);
}
Since the Reference
field is able to hold any type of reference, the scene manager will be responsible to decide what to do with its value.
The load scene info objects simply hold these references, and that's why the implementations included with the package are all structs.
You can choose to work with four load scene infos:
- The
LoadSceneInfoName
, that in standard scene manager is a reference to the scene name, and in the addressable scene manager, is a reference to its address. - The
LoadSceneInfoIndex
, that only works in the standard scene manager, since the build index is not an addressable information. - The
LoadSceneInfoScene
, that actually holds a reference to a scene, and can be used to unload specific scenes (useful if you have multiple scenes loaded with the same name, for example). - The
LoadSceneInfoAssetReference
, that only works in the addressable scene manager.
You can also build your own ILoadSceneInfo
implementation if have special needs, but that will probably require you to build a scene manager to interpret its Reference
value as well.
The scene loaders are meant to be the interface that you will use to load scenes in your game, as they work like a wrapper layer to the scene managers, but adding the Scene Transition operation.
There are two interfaces for them, the base one with a reference to the ISceneManager
that will be used, and an async interface, to be able to await the load operations.
The ISceneLoader
interface defines:
public interface ISceneLoader
{
ISceneManager Manager { get; }
void TransitionToScene(ILoadSceneInfo targetSceneInfo, ILoadSceneInfo intermediateSceneInfo = default);
void UnloadScene(ILoadSceneInfo sceneInfo);
void LoadScene(ILoadSceneInfo sceneInfo, bool setActive = false);
}
And the ISceneLoaderAsync
:
public interface ISceneLoaderAsync<TAsync> : ISceneLoader
{
TAsync TransitionToSceneAsync(ILoadSceneInfo targetSceneReference, ILoadSceneInfo intermediateSceneReference = default);
TAsync LoadSceneAsync(ILoadSceneInfo sceneReference, bool setActive = false, IProgress<float> progress = null);
TAsync UnloadSceneAsync(ILoadSceneInfo sceneReference);
}
Note that the ISceneLoaderAsync
interface inherits from ISceneLoader
.
The TAsync
type should return a Scene
instance, and can be anything you mean to await or a Coroutine (that can't return anything without additional code), for example Task<Scene>
, ValueTask<Scene>
or UniTask<Scene>
.
The Manager
property can be used to listen to the SceneLoaded
, SceneUnloaded
, and ActiveSceneChanged
events.
Both LoadSceneAsync
and UnloadSceneAsync
methods will simply call the ISceneManager
equivalents, while the LoadScene
and UnloadScene
will do the same but without await.
It's important to understand that LoadScene
, UnloadScene
and TransitionToScene
will still invoke asynchronous operations, instead of blocking the execution until they are done.
You can use the ISceneManager
events to react to the completion of those methods.
The Transition is a combination of load and unload operations to effectively perform scene transitions, with or without an intermediate scene. For example, usually, if you'd want to go from scene A to scene B you would:
- Load the scene B.
- Unload the scene A.
That's only two operations right? What if you wanted to have a loading screen as well? In this case you would:
- Load the loading scene.
- Load the scene B.
- Unload the scene A.
- Unload the loading scene.
That's four operations now.
The TransitionToScene
and TransitionToSceneAsync
methods let you only provide where you want to go from the currently active scene and if you want an intermediary scene (loading scene for example).
When creating your scene loader, you must first create your scene manager.
Ideally, you will not need to store the scene manager anywhere as it will be accessible through the ISceneLoader
interface.
Also, you will also need to build your scene info objects to hold references to scenes.
For the first example, let's build a standard scene manager and a Coroutine scene loader:
// Make sure to add 'using MyGameDevTools.SceneLoading;' on the top of the script
ISceneManager sceneManager = new SceneManager();
ISceneLoader sceneLoader = new SceneLoaderCoroutine(sceneManager);
The scene loaders are able to receive any type of ISceneManager
, for example:
ISceneManager standardSceneManager = new SceneManager();
ISceneLoader coroutineSceneLoader = new SceneLoaderCoroutine(standardSceneManager);
ISceneManager addressableSceneManager = new SceneManagerAddressable();
ISceneLoader asyncSceneLoader = new SceneLoaderAsync(addressableSceneLoader);
You can also define the scene loader types as their ISceneLoaderAsync
implementations:
ISceneManager sceneManager = new SceneManager();
ISceneLoaderAsync<Coroutine> coroutineSceneLoader = new SceneLoaderCoroutine(sceneManager);
// Or
ISceneLoaderAsync<ValueTask<Scene>> asyncSceneLoader = new SceneLoaderAsync(sceneManager);
// Or
ISceneLoaderAsync<UniTask<Scene>> unitaskSceneLoader = new SceneLoaderUniTask(sceneManager);
You'll use the load scene info objects to reference scenes. This can lead to differences when using the standard scene manager or the addressable scene manager.
Let's assume you have included the following scenes in your Build Settings:
- Main Menu
- Loading
- Stage 1
You can load the scenes by their name or the build index:
ILoadSceneInfo mainMenuSceneInfo = new LoadSceneInfoName("Main Menu");
ILoadSceneInfo loadingSceneInfo = new LoadSceneInfoIndex(1);
ILoadSceneInfo stageSceneInfo = new LoadSceneInfoName("Stage 1");
sceneLoader.LoadScene(mainMenuSceneInfo);
sceneLoader.LoadScene(loadingSceneInfo);
sceneLoader.LoadScene(stageSceneInfo);
// Or the async alternatives
await sceneLoader.LoadSceneAsync(mainMenuSceneInfo);
await sceneLoader.LoadSceneAsync(loadingSceneInfo);
await sceneLoader.LoadSceneAsync(stageSceneInfo);
For unloading, you can do the same, or you can use the scene reference returned during the LoadSceneAsync
:
ILoadSceneInfo mainMenuSceneInfo = new LoadSceneInfoName("Main Menu");
ILoadSceneInfo loadingSceneInfo = new LoadSceneInfoIndex(1);
Scene stageScene = await sceneLoader.LoadSceneAsync(new LoadSceneInfoName("Stage 1"));
ILoadSceneInfo stageSceneInfo = new LoadSceneInfoScene(stageScene);
sceneLoader.UnloadScene(mainMenuSceneInfo);
sceneLoader.UnloadScene(loadingSceneInfo);
sceneLoader.UnloadScene(stageSceneInfo);
// Or the async alternatives
await sceneLoader.UnloadSceneAsync(mainMenuSceneInfo);
await sceneLoader.UnloadSceneAsync(loadingSceneInfo);
await sceneLoader.UnloadSceneAsync(stageSceneInfo);
Instead of using the async method, you can also register to the ISceneManager.SceneLoaded
event:
sceneLoader.Manager.SceneLoaded += loadedScene =>
{
ILoadSceneInfo loadedSceneInfo = new LoadSceneInfoScene(loadedScene);
sceneLoader.UnloadScene(loadedSceneInfo);
}
Finally, you can combine different load scene info objects on the transition method:
ILoadSceneInfo stageSceneInfo = new LoadSceneInfoName("Stage 1");
ILoadSceneInfo loadingSceneInfo = new LoadSceneInfoIndex(1);
sceneLoader.TransitionToScene(stageSceneInfo, loadingSceneInfo);
// Or the async alternative
await sceneLoader.TransitionToSceneAsync(stageSceneInfo, loadingSceneInfo);
Let's assume you have the following addressable scenes with their own names as their address:
- Main Menu
- Loading
- Stage 1
You can load the scenes by their addresses or by an AssetReference (usually exposed via MonoBehaviours):
ILoadSceneInfo mainMenuSceneInfo = new LoadSceneInfoName("Main Menu");
ILoadSceneInfo loadingSceneInfo = new LoadSceneInfoName("Loading");
ILoadSceneInfo stageSceneInfo = new LoadSceneInfoName("Stage 1");
sceneLoader.LoadScene(mainMenuSceneInfo);
sceneLoader.LoadScene(loadingSceneInfo);
sceneLoader.LoadScene(stageSceneInfo);
// Or the async alternatives
await sceneLoader.LoadSceneAsync(mainMenuSceneInfo);
await sceneLoader.LoadSceneAsync(loadingSceneInfo);
await sceneLoader.LoadSceneAsync(stageSceneInfo);
You cannot create AssetReference
objects from code, unless you're in an editor context.
So the best way to use an AssetReference
is to use a MonoBehaviour or a ScriptableObject, for example:
public class MyBehavior : MonoBehaviour
{
[SerializeField]
AssetReference _loadingScene;
// [...]
void LoadScene()
{
ILoadSceneInfo loadingSceneInfo = new LoadSceneInfoAssetReference(_loadingScene);
sceneLoader.LoadScene(loadingSceneInfo);
}
}
Same as the standard scene manager, you can unload scenes with the Scene
reference as well:
ILoadSceneInfo mainMenuSceneInfo = new LoadSceneInfoName("Main Menu");
ILoadSceneInfo loadingSceneInfo = new LoadSceneInfoAssetReference(_loadingSceneReference);
Scene stageScene = await sceneLoader.LoadSceneAsync(new LoadSceneInfoName("Stage 1"));
ILoadSceneInfo stageSceneInfo = new LoadSceneInfoScene(stageScene);
sceneLoader.UnloadScene(mainMenuSceneInfo);
sceneLoader.UnloadScene(loadingSceneInfo);
sceneLoader.UnloadScene(stageSceneInfo);
// Or the async alternatives
await sceneLoader.UnloadSceneAsync(mainMenuSceneInfo);
await sceneLoader.UnloadSceneAsync(loadingSceneInfo);
await sceneLoader.UnloadSceneAsync(stageSceneInfo);
The ISceneManager.SceneLoaded
event subscription also works exactly the same as the standard scene manager:
sceneLoader.Manager.SceneLoaded += loadedScene =>
{
ILoadSceneInfo loadedSceneInfo = new LoadSceneInfoScene(loadedScene);
sceneLoader.UnloadScene(loadedSceneInfo);
}
And you can also combine different load scene info objects on the transition method:
ILoadSceneInfo stageSceneInfo = new LoadSceneInfoName("Stage 1");
ILoadSceneInfo loadingSceneInfo = new LoadSceneInfoAssetReference(_loadingSceneReference);
sceneLoader.TransitionToScene(stageSceneInfo, loadingSceneInfo);
// Or the async alternative
await sceneLoader.TransitionToSceneAsync(stageSceneInfo, loadingSceneInfo);
During scene transitions, you have the option to provide an intermediate scene that will work just like a loading screen. This could be an animated splash screen or a loading progress bar, for example. This package provides implementations to help you build your loading screens faster.
The Loading Behavior is a MonoBehaviour component, which you can attach to Unity GameObjects, that receives the progress value from the scene manager.
You need to add a LoadingBehavior
component to a GameObject in your loading scene in order to be able to display scene loading feedbacks.
It exposes its LoadingProgress
instance, that you can use to listen to the loading events:
public class LoadingProgress : IProgress<float>
{
public event LoadingStateChangeDelegate StateChanged;
public event SceneLoadProgressDelegate Progressed;
public LoadingState State { get; }
}
The StateChanged
event expects a LoadingState
parameter, to report the current state of the scene loading operation, and you can query the active state at any time by retrieving the value in the State
property.
The Progressed
event expects a float
parameter, ranging from 0 to 1 to report the progress of the scene loading operation.
Back to the LoadingBehavior
, it has a few options you can set on the Unity Inspector:
- Wait For Scripted Start: enable if the loading screen will have a transition in effect, such as a fade in.
- Wait For Scripted End: enable if the loading screen will have a transition out effect, such as a fade out.
- Reduced Load Ratio: enable if you're working with standard scene loading operations (non-addressable).
The loading scene transition can be customized to delay some parts of the operation to deliver a smooth visual experience for the user.
That means we can fade in/out or use other transition effects and wait for them to complete to continue the scene loading operations.
The LoadingState
enum reflects those states:
public enum LoadingState
{
WaitingToStart,
Loading,
TargetSceneLoaded,
TransitionComplete
}
These states are ordered, which means that the first state will always be WaitingToStart
and the last will be TransitionComplete
.
They mean:
WaitingToStart
: it's waiting for a trigger to allow the scene loading to actually start loading. This could be if the loading scene does not instantly appear, otherwise causing weird experiences with things simply disappearing. You can transition the loading screen with a fade in or a similar effect, for example.Loading
: the loading screen transition has occurred and the scene loading operation is running. During this state, theLoadingProgress
instance will be receiving the progress value from the scene manager.TargetSceneLoaded
: the target scene has been loaded, but the loading screen is still displaying. You can use this state to transition the loading screen out, such as a fade out or a similar effect.TransitionComplete
: the target scene has been loaded and the loading screen is already out of the way. Shortly after this state, the loading scene will be unloaded.
At this point, you should already have your loading scene with a LoadingBehavior
attached to one of your GameObjects.
Now you can also add some other components to display the loading progress feedback.
This package comes with three feedbacks:
LoadingFeedbackSlider
: attach on an UI Slider to display the loading progress feedback as a progress bar.LoadingFeedbackTextMeshPro
: attach on an UI Text Mesh Pro to display the loading progress feedback as text normalized from 0 to 100.LoadingFeedbackText
(also known as Legacy): attach on an UI Legacy Text to display the loading progress feedback as text normalized from 0 to 100.
You can use a combination of these feedback components in the loading scene.
Remember to assign the LoadingBehavior
field of these components to the LoadingBehavior
component you created before.
Another feedback that you could make is a fade in/out effect.
The LoadingFader
component does just that.
Add it to an [UI CanvasGroup] GameObject to control the group's alpha value during the visual transitions.
You can also set the fade time and customize the fade in/out animation curves to suit your preference.
In order to use the LoadingFader
effectively, you must enable both WaitForScriptedStart
and WaitForScriptedEnd
toggles in your LoadingBehavior
component.
Take the following loading screen scene hierarchy as an example:
- Canvas - (Canvas, CanvasScaler,
LoadingBehavior
)- Group - (CanvasGroup,
LoadingFader
)- Background - (Image)
- Text_Message - (TextMeshProUGUI)
- Slider_Progress - (Slider,
LoadingFeedbackSlider
)- Text_Progress - (TextMeshProUGUI,
LoadingFeedbackTextMeshPro
)
- Text_Progress - (TextMeshProUGUI,
- Group - (CanvasGroup,
By having this hierarchy in your loading scene, it would be able to fade in/out and to display both loading progress bar and loading progress text feedbacks.
As this scene has the LoadingFader
component, remember to enable both WaitForScriptedStart
and WaitForScriptedEnd
toggles in the LoadingBehavior
component.
Also, if you're not using an addressable scene manager, enable the ReducedLoadRatio
toggle.
You can test this scene by passing its ILoadSceneInfo
reference as the intermediateSceneInfo
in an ISceneLoader.TransitionToScene
method.
The idea behind the interfaces is first to decouple things and second to allow you to build your own systems if you require something very different from the provided content. Sometimes projects require very specific implementations, and instead of making the system extremely complex and detailed, I'd rather have it broken into many different pieces that you can replace to fit with whatever works best in each use case.
I am always open to suggestions, so please if you have any, don't hesistate to share!
This package includes tests to assert most use cases of the Scene Managers and Scene Loaders. The tests do not have any effect on a runtime build of the game, they only mean to work in a development environment.
Don't hesitate to create issues for suggestions and bugs. Have fun!