Summary
A scene is a Unity asset that holds one screen or level of a game: a title menu, a level, a game-over screen. Moving between them is done in code with the SceneManager class from the UnityEngine.SceneManagement namespace. Loading a scene normally replaces everything that is currently open, which is exactly what a restart needs: reload the level and every object returns to its starting state. Almost every first game needs the same small loop, Title → Game → Game Over or Win → Retry or Menu, and the same three gotchas appear in nearly every student project: a scene missing from the scene list, a game that stays frozen because Time.timeScale was never reset, and duplicated managers after a reload.
Key ideas
- The scene list decides what can load. In Unity 6 the old Build Settings window has been replaced by File > Build Profiles. Its Scene List holds the scenes that go into a build, in order. The order gives each scene its build index (the top scene is 0 and is the first scene a built game opens). The Scripting API still says “Build Settings” in many places; it means this list.
- Load by name or by index.
SceneManager.LoadScene("Level1")orSceneManager.LoadScene(1). Names are easier to read; indices make “next level” arithmetic easy but break when the list is reordered. - Loading is not instant.
LoadSceneloads the new scene in the next frame, so code after the call still runs this frame. - Single mode replaces, additive mode adds. The default
LoadSceneMode.Singleunloads the current scenes;LoadSceneMode.Additiveloads a scene alongside the ones already open. - Restart = reload the active scene. Every GameObject in that scene is destroyed and recreated from the scene file, so their fields return to the values saved in the scene.
- Some things survive a reload.
Time.timeScale,staticfields and objects markedDontDestroyOnLoadare not part of the scene, so loading a scene does not reset them. sceneLoadedtells persistent objects that a new scene has arrived. It is how a manager that survives loads can reconnect to the new scene.
In practice
1. Put your scenes in the scene list (Unity 6)
- Save each scene in
Assets/Scenes(for exampleTitle,Level1,GameOver,Win). - Open File > Build Profiles. The Scene List is shown for platform profiles. Press Add Open Scenes to add the scene that is open now, or drag scene assets from the Project window into the list.
- Drag the rows into order: put
Titleat the top so the built game starts there. - A ticked scene is built; an unticked scene stays in the list but is left out of the build.
A separate build profile (for example a “Demo” profile) uses this shared list unless you choose Add Settings > Scene List on that profile to give it its own list (Unity Technologies, Unity 6.3 Manual: Manage scenes in your build). Beginners should leave the shared list alone and use one list.
2. Load a scene by name or build index
using UnityEngine;
using UnityEngine.SceneManagement; // SceneManager lives here
public class MenuButtons : MonoBehaviour
{
// Wire a Button's On Click () to this and type the scene name in the box.
public void LoadByName(string sceneName)
{
Time.timeScale = 1f; // see section 8
SceneManager.LoadScene(sceneName); // no ".unity" extension
}
public void LoadNextLevel()
{
int next = SceneManager.GetActiveScene().buildIndex + 1;
if (next < SceneManager.sceneCountInBuildSettings)
SceneManager.LoadScene(next);
else
SceneManager.LoadScene(0); // back to the title scene
}
}Public methods with no parameter, or one string, int, float, bool or UnityEngine.Object parameter, appear in a uGUI Button’s On Click () list, so menu buttons need no extra code.
3. Restart the current level
public void Restart()
{
Time.timeScale = 1f;
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}Using buildIndex rather than name avoids a mistyped string. This works only from inside the level. If the player has already been sent to a separate GameOver scene, the active scene is now GameOver, so “Retry” must remember which level was played (the GameSession script in the full example below stores it).
4. A win/lose flow
The usual structure for a small game:
| Scene | Buttons | Calls |
|---|---|---|
| Title | Play, Quit | LoadScene("Level1"), Application.Quit() |
| Level1 | Pause panel: Resume, Restart, Menu | toggle Time.timeScale, reload active scene, LoadScene("Title") |
| GameOver | Retry, Menu | load the last level, LoadScene("Title") |
| Win | Play again, Menu | load the first or next level, LoadScene("Title") |
The level decides when it has been won or lost. A trigger is the simplest way:
[SerializeField] private string sceneToLoad = "GameOver"; // "Win" on the goal
private bool hasFired;
private void OnTriggerEnter2D(Collider2D other)
{
if (hasFired || !other.CompareTag("Player")) return;
hasFired = true; // end the game once only
Invoke(nameof(EndLevel), 0.5f); // let a death or win animation play
}
private void EndLevel() => SceneManager.LoadScene(sceneToLoad);An alternative to separate end scenes is a game-over panel inside the level: a hidden Canvas panel that is shown on death while Time.timeScale is set to 0. It keeps everything in one scene and makes Restart trivial; separate scenes are cleaner once there are several levels. unity-animator-scripting shows the same flow driven by a GameState enum and Animator triggers, and unity-collider2d-and-triggers explains why the player needs a Rigidbody2D for the trigger to fire. Application.Quit() is ignored in the Editor and, on the Web platform, stops the player without closing the page (Unity Technologies, Scripting API: Application.Quit), so a Quit button needs testing in a build.
5. A simple loading screen with LoadSceneAsync
LoadScene can cause a visible stall on a large scene. LoadSceneAsync loads in the background and returns an AsyncOperation you can watch from a coroutine:
private IEnumerator LoadRoutine(string sceneName)
{
loadingPanel.SetActive(true);
AsyncOperation op = SceneManager.LoadSceneAsync(sceneName);
op.allowSceneActivation = false; // hold the scene back while the bar fills
while (op.progress < 0.9f) // progress stops at 0.9 while activation is held
{
progressBar.value = op.progress / 0.9f;
yield return null;
}
progressBar.value = 1f;
op.allowSceneActivation = true; // now swap to the new scene
}Start it with StartCoroutine(LoadRoutine("Level1")). Small student scenes load almost instantly, so the full example adds a minimum display time; otherwise the loading panel only flickers.
6. Additive loading (overview)
Additive loading keeps the current scene open and adds another one:
SceneManager.LoadSceneAsync("Level1_Audio", LoadSceneMode.Additive);
// later
SceneManager.UnloadSceneAsync("Level1_Audio");Teams use it to split a level into parts that different people edit (geometry, lighting, gameplay), to keep a permanent “core” scene with managers and UI open while levels come and go, or to stream areas of a large world. New objects are created in the active scene, which SceneManager.SetActiveScene changes. Treat additive loading as a second-stage topic: a single-scene game with LoadScene is the right starting point.
7. Keeping objects between scenes: DontDestroyOnLoad
DontDestroyOnLoad(gameObject) moves a GameObject out of its scene into a special DontDestroyOnLoad section of the Hierarchy, so a Single-mode load does not destroy it. It is how a music player, a save system or a GameManager singleton survives from menu to level. Its pitfalls:
- Duplicates. The persistent object came from a scene. Load that scene again (return to the menu, or restart a level that contains the manager) and Unity creates a second copy. Always pair
DontDestroyOnLoadwith the duplicate guard inAwakeshown on unity-gamemanager-pattern: if an instance already exists, destroy the new one andreturn. - Root objects only. It works only on root GameObjects (and components on them); children come along with their root (Unity Technologies, Scripting API: Object.DontDestroyOnLoad). Do not parent a manager under another object.
- Stale references. A persistent manager that holds a reference to a scene object (a score label, the player) loses it when that scene unloads. Using the destroyed reference gives a
MissingReferenceException. Have scene objects register themselves with the manager inStart, or reconnect insceneLoaded. - Pressing Play in the wrong scene. If the manager lives in
Titleand you press Play withLevel1open, it does not exist, soGameManager.Instanceisnull. Null-check it, or place a copy in every scene and let the duplicate guard remove extras. - Persistent state needs a manual reset. A restart rebuilds the scene, not the manager. Reset its score and lives yourself (the
RestartGamemethod on unity-gamemanager-pattern does this).
8. The Time.timeScale gotcha
Pausing usually sets Time.timeScale = 0f. At zero, Time.deltaTime also becomes 0, and FixedUpdate and coroutines waiting on WaitForSeconds stop being called (Unity Technologies, Scripting API: Time.timeScale and Time.deltaTime). Update keeps running, which is why a pause key can still unpause.
timeScale is a single engine-wide static setting, not part of any scene. If the player pauses (or a game-over panel sets it to 0) and then presses Restart or Menu, the next scene opens with timeScale still 0 and nothing moves. The fix is one line before every load: Time.timeScale = 1f;. Putting all loads behind one method (like SceneFlow.LoadScene in the example) means the line cannot be forgotten. Use Time.unscaledDeltaTime for anything that must animate while paused, such as a pause-menu tween.
9. Reacting to a new scene: sceneLoaded
private void OnEnable() => SceneManager.sceneLoaded += OnSceneLoaded;
private void OnDisable() => SceneManager.sceneLoaded -= OnSceneLoaded;
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
Debug.Log("Loaded " + scene.name + " (index " + scene.buildIndex + ")");
}This is most useful on a DontDestroyOnLoad object, which would otherwise never learn that the scene changed. Always unsubscribe in OnDisable: an object destroyed while still subscribed leaves a dead handler behind (see unity-object-communication on static events).
Other engines
For comparison: Godot 4 uses get_tree().change_scene_to_file("res://level_1.tscn") and get_tree().reload_current_scene(), and keeps persistent managers as autoloads rather than with a DontDestroyOnLoad call. Unreal Engine opens a level with UGameplayStatics::OpenLevel and keeps cross-level state in the GameInstance, which lives for the whole session.
Full example
The runnable example in code/unity-csharp/scene-flow/ builds the whole Title → Level → Game Over/Win loop, with setup steps and experiments in its README:
- SceneFlow.cs —
StartGame,Restart,LoadNextLevel,RetryLastLevel,LoadMenu,Quitfor UI buttons; every load resetsTime.timeScale - GameOverTrigger.cs — win or lose trigger with a delay and a fire-once guard
- GameSession.cs — persistent object with
DontDestroyOnLoad, a duplicate guard andsceneLoaded - PauseMenu.cs — pause panel toggled with Escape (Input System) or a button
- LoadingScreen.cs —
LoadSceneAsyncprogress bar
Evidence
All of the following is from Unity’s own documentation for Unity 6.3 LTS.
SceneManager.LoadSceneloads a scene “by its name or index in Build Settings”; the load happens in the next frame rather than immediately. A name can be given without the.unityextension; if several scenes share a name, the first match in the list is loaded, so a full path should be used. The default mode is Single (Unity Technologies, Scripting API: SceneManager.LoadScene).LoadSceneAsyncreturns anAsyncOperationwhoseisDonecan be polled from a coroutine, an approach the page recommends for loading screens; an invalid scene name or index causes an exception (Unity Technologies, Scripting API: SceneManager.LoadSceneAsync).- “When allowSceneActivation is false, Unity stops AsyncOperation.progress at 0.9, and AsyncOperation.isDone remains false.” While it is held, later queued operations such as
UnloadSceneAsyncalso wait (Unity Technologies, Scripting API: AsyncOperation.allowSceneActivation). - The scene list is opened with File > Build Profiles; scenes are added with Add Open Scenes or by dragging them from the Project window, are built in list order, and a cleared checkbox excludes a scene without removing it. A build profile uses the global scene list unless a Scene List is added to it (Unity Technologies, Unity 6.3 Manual: Manage scenes in your build).
DontDestroyOnLoad“only works for root GameObjects or components on root GameObjects”, and the object’s Transform children are preserved with it. The page’s own example destroys a second copy inAwake(Unity Technologies, Scripting API: Object.DontDestroyOnLoad).sceneLoadedcallbacks take(Scene, LoadSceneMode); the example subscribes inOnEnable, unsubscribes inOnDisable, and states that the callback runs afterOnEnablebut beforeStart(Unity Technologies, Scripting API: SceneManager.sceneLoaded).FixedUpdateand coroutines suspended withWaitForSecondsare not called whentimeScaleis zero;deltaTimeis scaled bytimeScaleand becomes 0 when the game is paused that way, sounscaledDeltaTimeshould be used for wall-clock time (Unity Technologies, Scripting API: Time.timeScale and Time.deltaTime).
The Scripting API does not describe the persistence of Time.timeScale across scene loads in words; that timeScale is a static engine setting is visible in its declaration, and the frozen-restart experiment in the example’s README demonstrates the consequence.
Implications
- Route every scene change through one small method. It is the natural place to reset
Time.timeScale, check the scene exists, play a transition or save progress, and it removes scattered string literals. - Decide early what lives in a scene and what lives across scenes. Most beginner state (player health, enemies, pickups) belongs in the scene and is reset for free by a reload; only things like settings, total score or music need a persistent object.
- A persistent object is a commitment: it needs a duplicate guard, manual resets and a way to reconnect to new scenes. If a static field or a ScriptableObject asset would do, it is often simpler.
- Testing should include the paths players take that developers rarely do: pause then restart, win then menu then play again, and starting the game from the first scene in the list rather than the scene open in the Editor.
Open questions
- When does a separate GameOver scene become worth it compared with an in-level game-over panel? For a one-level jam game the panel is arguably simpler; for a level-based game the scene keeps levels free of menu UI.
- How far should beginners go with additive “bootstrap” scenes before they have shipped a single-scene game?
Gotchas
- A Console error saying the scene couldn’t be loaded because it has not been added to the build (exact wording varies between versions): add it in File > Build Profiles and make sure its box is ticked.
- Renamed or moved scene: a string such as
"Level1"in code or in a Button’s On Click () box does not update itself. Search for the old name. - Reordering the scene list changes every build index; code that loads
2now loads something else. - Button calls nothing: the On Click () slot needs the GameObject that holds the script (from the Hierarchy), not the script asset from the Project window.
- The end scene loads twice: two colliders, or two frames of overlap, called the trigger twice. Use a
hasFiredguard. - Static fields survive a restart. A
static int scorekeeps its value after a reload; reset it yourself or make it an instance field.
Practice
Build a three-screen loop: a Title scene with a Play button, a Level1 scene with a hazard and a goal, and a GameOver scene with Retry and Menu buttons. Use one script with public methods for every scene change, and give Level1 a pause key that sets Time.timeScale to 0.
Success test:
- all three scenes are in the File > Build Profiles scene list, with Title at index 0
- touching the hazard loads GameOver once, even if the player stays inside the trigger
- Retry from GameOver returns to Level1 with everything back in its starting position
- pausing, then restarting the level from a pause button, leaves the game running rather than frozen
- the Console shows no errors when you go Title → Level1 → GameOver → Menu → Level1
Extension:
- add a persistent object with
DontDestroyOnLoadthat counts attempts, prove there is only ever one copy under DontDestroyOnLoad in the Hierarchy, and log the count from asceneLoadedhandler
Self-test
- Where do you add scenes to the build in Unity 6, and what decides a scene’s build index?
- Write the line that restarts the current level using its build index.
- A player pauses, presses Restart, and the reloaded level does not move. What is wrong and what is the fix?
- Why does a
GameManagerthat callsDontDestroyOnLoadneed a duplicate guard inAwake? - While
allowSceneActivationisfalse, what value doesAsyncOperation.progressstop at? - From a separate GameOver scene, why does
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex)not take the player back to the level?
Answers
- In the Scene List of File > Build Profiles (the Unity 6 replacement for Build Settings). The scene’s position in the list sets its build index, starting from 0 at the top.
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);Time.timeScaleis still 0. It is a global setting that loading a scene does not reset. SetTime.timeScale = 1f;before every scene load.- The manager came from a scene. Loading that scene again creates another copy, so without the guard there are two managers with separate state; the guard destroys the newcomer if an instance already exists.
- 0.9;
isDonestaysfalseuntil activation is allowed. - Because the active scene is now GameOver, so the line reloads GameOver. The game must remember which level was played (for example in a persistent object) and load that.
Related
- unity-gamemanager-pattern — singleton GameManager,
DontDestroyOnLoadduplicate guard andRestartGame - unity-animator-scripting —
GameStateenum, Animator triggers and delayed scene loads - monobehaviour-lifecycle —
Awake,OnEnableandStart, which run again for every object in a reloaded scene - unity-collider2d-and-triggers — the trigger setup behind win and lose zones
- unity-object-communication — static events that must be unsubscribed before a scene unloads
- scriptableobject-event-channels — shared assets that outlive scene loads without
DontDestroyOnLoad - state-pattern — structuring Playing, Paused, Won and Lost as explicit states
- csharp-enums — the
GameStateandOutcomeenums used for win/lose logic - unity-prefabs — keeping managers and UI as prefabs so every scene gets the same setup
- unity-input — the pause key and player movement in the example
- ui-design — designing the menus and end screens this flow connects
- build-and-release-management — producing and testing playable builds