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") or SceneManager.LoadScene(1). Names are easier to read; indices make “next level” arithmetic easy but break when the list is reordered.
  • Loading is not instant. LoadScene loads 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.Single unloads the current scenes; LoadSceneMode.Additive loads 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, static fields and objects marked DontDestroyOnLoad are not part of the scene, so loading a scene does not reset them.
  • sceneLoaded tells 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)

  1. Save each scene in Assets/Scenes (for example Title, Level1, GameOver, Win).
  2. 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.
  3. Drag the rows into order: put Title at the top so the built game starts there.
  4. 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:

SceneButtonsCalls
TitlePlay, QuitLoadScene("Level1"), Application.Quit()
Level1Pause panel: Resume, Restart, Menutoggle Time.timeScale, reload active scene, LoadScene("Title")
GameOverRetry, Menuload the last level, LoadScene("Title")
WinPlay again, Menuload 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 DontDestroyOnLoad with the duplicate guard in Awake shown on unity-gamemanager-pattern: if an instance already exists, destroy the new one and return.
  • 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 in Start, or reconnect in sceneLoaded.
  • Pressing Play in the wrong scene. If the manager lives in Title and you press Play with Level1 open, it does not exist, so GameManager.Instance is null. 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 RestartGame method 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, Quit for UI buttons; every load resets Time.timeScale
  • GameOverTrigger.cs — win or lose trigger with a delay and a fire-once guard
  • GameSession.cs — persistent object with DontDestroyOnLoad, a duplicate guard and sceneLoaded
  • PauseMenu.cs — pause panel toggled with Escape (Input System) or a button
  • LoadingScreen.cs — LoadSceneAsync progress bar

Evidence

All of the following is from Unity’s own documentation for Unity 6.3 LTS.

  • SceneManager.LoadScene loads 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 .unity extension; 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).
  • LoadSceneAsync returns an AsyncOperation whose isDone can 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 UnloadSceneAsync also 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 in Awake (Unity Technologies, Scripting API: Object.DontDestroyOnLoad).
  • sceneLoaded callbacks take (Scene, LoadSceneMode); the example subscribes in OnEnable, unsubscribes in OnDisable, and states that the callback runs after OnEnable but before Start (Unity Technologies, Scripting API: SceneManager.sceneLoaded).
  • FixedUpdate and coroutines suspended with WaitForSeconds are not called when timeScale is zero; deltaTime is scaled by timeScale and becomes 0 when the game is paused that way, so unscaledDeltaTime should 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 2 now 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 hasFired guard.
  • Static fields survive a restart. A static int score keeps 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 DontDestroyOnLoad that counts attempts, prove there is only ever one copy under DontDestroyOnLoad in the Hierarchy, and log the count from a sceneLoaded handler

Self-test

  1. Where do you add scenes to the build in Unity 6, and what decides a scene’s build index?
  2. Write the line that restarts the current level using its build index.
  3. A player pauses, presses Restart, and the reloaded level does not move. What is wrong and what is the fix?
  4. Why does a GameManager that calls DontDestroyOnLoad need a duplicate guard in Awake?
  5. While allowSceneActivation is false, what value does AsyncOperation.progress stop at?
  6. From a separate GameOver scene, why does SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex) not take the player back to the level?

Answers

  1. 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.
  2. SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
  3. Time.timeScale is still 0. It is a global setting that loading a scene does not reset. Set Time.timeScale = 1f; before every scene load.
  4. 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.
  5. 0.9; isDone stays false until activation is allowed.
  6. 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.