Skip to main content

๐ŸŽž๏ธ Lesson 5.1: Loading Scenes: Single, Async & Loading Screens

Your game is more than one scene: a main menu, a level, maybe a boss arena. Moving between them is the job of the SceneManager. Do it the naive way and your game freezes for a second every time. Do it the right way โ€” asynchronously, with a loading screen โ€” and transitions feel smooth and professional. This lesson covers both, and exactly when to use each.

๐ŸŽฏ Learning Objectives

By the end of this lesson, you will be able to:

  • Add scenes to Build Settings so they can be loaded at runtime
  • Load a scene instantly with SceneManager.LoadScene in Single mode
  • Explain why a synchronous load causes a freeze and when that's acceptable
  • Load asynchronously with LoadSceneAsync and read AsyncOperation.progress
  • Build a loading screen coroutine with a progress bar using allowSceneActivation
  • Reference scenes by build index or name and know the trade-offs

Estimated Time: 40 minutes  ยท  Prerequisite: Lesson 4.4 (World-Space UI), plus knowing how to make a build from Fundamentals

In This Lesson

Meet the SceneManager

A scene is a container for a slice of your game: its GameObjects, lighting, and settings. In Fundamentals you built inside a single scene. Real games have many, and the class that swaps between them at runtime is UnityEngine.SceneManagement.SceneManager.

The whole API starts with one using line:

using UnityEngine.SceneManagement;

Forget that line and SceneManager won't resolve โ€” it lives in its own namespace, not the base UnityEngine. With it imported, loading the next level is a single call.

๐Ÿ“– Definition

SceneManager: Unity's static class for loading, unloading, and querying scenes at runtime. You never instantiate it; you call its methods directly (SceneManager.LoadScene(...)). It works with scenes you've registered in Build Settings, addressed by name or build index.

Scenes In Build: The Registry

Here's the rule that trips up everyone the first time: a scene can only be loaded at runtime if it's listed in Build Settings. A scene file sitting in your Project window is not enough. You have to register it.

Open File โ–ธ Build Profiles (in older layouts, File โ–ธ Build Settings). At the top is the Scenes In Build list. Drag your scenes in, or click Add Open Scenes. Each row gets an index, starting at 0 โ€” and the scene at index 0 is the one your built game opens with.

The Unity Build Settings window showing the Scenes In Build list A recreation of Unity's Build Profiles / Build Settings window. The Scenes In Build panel lists four scenes with checkboxes: Scenes/Boot at index 0, Scenes/MainMenu at index 1, Scenes/Loading at index 2, and Scenes/Level_1 at index 3. A fifth scene, Scenes/Sandbox, is unticked and greyed with no index. Buttons for Add Open Scenes and a platform list appear below. Build Settings Scenes In Build Scenes/Boot 0 Scenes/MainMenu 1 Scenes/Loading 2 Scenes/Level_1 3 Scenes/Sandbox โ€” Add Open Scenes Platform ๐Ÿ–ฅ๏ธ Windows / Mac / Linux Build
Figure 1: The Build Settings "Scenes In Build" list (faithfully recreated). Ticked scenes get an index; index 0 is the startup scene. The greyed Sandbox row is unticked, so it exists in the project but cannot be loaded in a build.

โš ๏ธ "The scene couldn't be loaded because it is not added to the build settings"

This runtime error is Unity's number-one scene gotcha. It means the scene you asked for exists in the Project but isn't ticked in Scenes In Build. Add it (and make sure the checkbox is on โ€” an unticked row is ignored). It works in the Editor sometimes but always fails in a real build, so test builds early.

Loading a Scene: Single Mode

The simplest load replaces everything on screen with a new scene. That's Single mode โ€” the default. It destroys the current scene's objects, then loads the new one.

using UnityEngine;
using UnityEngine.SceneManagement;

public class MenuButtons : MonoBehaviour
{
    // Hook this to a "Play" button's OnClick in the Inspector
    public void StartGame()
    {
        // By name (must match the scene's file name, no extension)
        SceneManager.LoadScene("Level_1", LoadSceneMode.Single);
    }

    public void QuitToMenu()
    {
        // By build index โ€” MainMenu is index 1 in Figure 1
        SceneManager.LoadScene(1);
    }
}

You can reference a scene two ways, and both appear above:

  • By name ("Level_1") โ€” readable and stable if you reorder the list. This is what you'll use most.
  • By build index (1) โ€” the number from the Scenes In Build list. Fast to type, but if you drag rows around the numbers shift under you.

LoadSceneMode.Single is the default, so SceneManager.LoadScene("Level_1") does the same thing. We'll meet Additive mode in the next lesson.

โœ… Pro Tip

Prefer loading by name in gameplay code so a designer reordering the build list can't silently send the player to the wrong scene. Reserve index loading for the rare case where the name isn't known but the position is (like "load the next level, whatever it's called").

Why Big Scenes Freeze

LoadScene is synchronous: it stops everything, tears down the old scene, loads the new one, then hands control back โ€” all in one frame. For a tiny menu that's instant and fine. For a large level with heavy meshes, textures, and lighting data, that "one frame" can stretch to a noticeable hitch: the window stops responding, audio may stutter, and on some platforms the OS shows a "Not Responding" flash.

The fix is to load asynchronously โ€” spread the work across many frames so the game keeps rendering (a spinning loading animation, a progress bar) while the new scene streams in. That's LoadSceneAsync.

๐Ÿ’ก Rule of thumb: Menus and tiny scenes โ†’ LoadScene is fine. Actual gameplay levels โ†’ always LoadSceneAsync with a loading screen. Players forgive a progress bar; they don't forgive a frozen window.

LoadSceneAsync & Progress

SceneManager.LoadSceneAsync returns an AsyncOperation โ€” a handle you can poll while the load runs in the background. Its two useful members are:

  • progress โ€” a float from 0 to 0.9 while loading, jumping to 1.0 only after the scene is activated.
  • allowSceneActivation โ€” set it to false to hold the finished scene in the wings until you say go.

That 0.9 cap surprises everyone, so let's be precise about the flow before we write the coroutine:

flowchart TD A["Call LoadSceneAsync
(returns AsyncOperation op)"] --> B["op.allowSceneActivation = false"] B --> C{"op.progress < 0.9 ?"} C -- "Yes" --> D["Update progress bar
fill = progress / 0.9
yield return null"] D --> C C -- "No (0.9 = ready)" --> E["Scene fully loaded,
waiting to activate"] E --> F["Show 100% / 'Press any key'"] F --> G["op.allowSceneActivation = true"] G --> H["Scene activates,
progress hits 1.0"]

Figure 2: The async load lifecycle. Progress climbs to 0.9 while loading; the last 10% is reserved for the activation step you control with allowSceneActivation.

โš ๏ธ Progress never naturally reaches 1.0

While allowSceneActivation is false, progress tops out at 0.9, not 1.0. If your bar maps 0โ†’1 directly it will visibly stall at 90%. Divide by 0.9 (progress / 0.9f) so the bar reads a satisfying 100%, then flip allowSceneActivation to finish.

A Real Loading Screen

Now we put it together. The pattern is a dedicated Loading scene (index 2 in Figure 1) containing just a full-screen Canvas with a fill-image progress bar. It reads which scene to load from a static field, runs a coroutine, and animates the bar.

using UnityEngine;

public static class SceneLoader
{
    // Any menu sets this, then loads the "Loading" scene.
    public static string TargetScene = "Level_1";
}

Your menu button first records the destination, then switches to the loading scene:

public void PlayLevel1()
{
    SceneLoader.TargetScene = "Level_1";
    SceneManager.LoadScene("Loading");   // tiny scene, instant is fine
}

And the loading scene runs this on Start:

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using System.Collections;

public class LoadingScreen : MonoBehaviour
{
    [SerializeField] Image progressBar;   // a UI Image set to Filled
    [SerializeField] Text  progressLabel; // e.g. "42%"
    [SerializeField] GameObject pressAnyKeyPrompt;

    void Start() => StartCoroutine(LoadTarget());

    IEnumerator LoadTarget()
    {
        AsyncOperation op = SceneManager.LoadSceneAsync(SceneLoader.TargetScene);
        op.allowSceneActivation = false;   // hold at the finish line

        while (op.progress < 0.9f)
        {
            // remap 0..0.9 to a clean 0..1 for the bar
            float display = Mathf.Clamp01(op.progress / 0.9f);
            progressBar.fillAmount = display;
            progressLabel.text = Mathf.RoundToInt(display * 100f) + "%";
            yield return null;             // wait one frame, keep rendering
        }

        // Fully loaded and waiting. Show 100% and invite the player in.
        progressBar.fillAmount = 1f;
        progressLabel.text = "100%";
        pressAnyKeyPrompt.SetActive(true);

        // Wait for any key, then activate. (Input System: any control)
        while (!Input.anyKeyDown)
            yield return null;

        op.allowSceneActivation = true;    // swap to the new scene
    }
}

A few things worth noticing:

  • yield return null pauses the coroutine for exactly one frame, so the UI redraws and the bar animates smoothly instead of the game freezing.
  • Setting allowSceneActivation = false lets you gate the final swap on a "Press any key" prompt โ€” great for showing a tip or letting a slow player catch up.
  • If you'd rather activate instantly, just delete the allowSceneActivation line entirely and the scene swaps the moment it finishes.

โœ… Pro Tip

Set your progress bar's Image Type to Filled (Horizontal) in the Inspector so fillAmount drives it directly โ€” no math to size a RectTransform. For extra polish, lerp fillAmount toward the target instead of snapping, so the bar glides even when a chunk of the scene loads in one frame.

๐Ÿ’ก Why a separate Loading scene? Because Single-mode loading destroys the current scene. If your progress UI lived in the menu, it would be torn down mid-load. A dedicated tiny Loading scene survives on its own until it chooses to activate the target โ€” nothing can destroy it out from under you.

Hands-on Challenge

๐Ÿ‹๏ธ Exercise 1: A three-scene flow

Objective: Wire MainMenu โ†’ Loading โ†’ Level_1 the way a shipped game does.

  1. Create three scenes: MainMenu, Loading, and Level_1. Add all three to Build Settings.
  2. In MainMenu, add a UI Button. Give it a script that sets SceneLoader.TargetScene = "Level_1" and calls SceneManager.LoadScene("Loading").
  3. In Loading, add a Canvas with a Filled Image bar and a Text label, and attach the LoadingScreen script.
  4. Fill Level_1 with enough heavy objects (or a Thread.Sleep-free artificial delay) that loading takes a moment, then Play from MainMenu.
๐Ÿ’ก Hint: my bar snaps to full instantly

An empty Level_1 loads in a single frame, so you never see intermediate progress. Add real content โ€” a few thousand objects, big textures โ€” or temporarily loop the coroutine a set number of frames to demonstrate the animation. In a real project the level's own size provides the delay.

โœ… Success check

Clicking Play swaps to the Loading scene, the bar fills smoothly to 100%, the "Press any key" prompt appears, and pressing a key drops you into Level_1. The window never shows "Not Responding".

๐Ÿ‹๏ธ Exercise 2: Restart the current level

Add a "Restart" button that reloads whatever scene is active, without hard-coding its name. Use SceneManager.LoadScene(SceneManager.GetActiveScene().name). Confirm it works from any level โ€” a reusable pattern for death/retry screens.

๐ŸŽฏ Quick Quiz

Question 1: Your built game throws "scene couldn't be loaded because it is not added to the build settings." What's the fix?

Question 2: While allowSceneActivation is false, what's the highest value AsyncOperation.progress will report?

Question 3: Why load a large gameplay level with LoadSceneAsync instead of LoadScene?

Summary

๐ŸŽ‰ Key Takeaways

  • SceneManager (in UnityEngine.SceneManagement) loads scenes at runtime, by name or build index.
  • A scene must be in Build Settings โ–ธ Scenes In Build (ticked) to load; index 0 is the startup scene.
  • LoadScene is synchronous โ€” fine for menus, a freeze risk for big levels.
  • LoadSceneAsync returns an AsyncOperation; poll progress across frames to animate a loading screen.
  • progress caps at 0.9 until allowSceneActivation is true โ€” remap with progress / 0.9f.
  • Use a dedicated Loading scene so the progress UI can't be destroyed mid-load.

๐Ÿš€ What's Next?

So far every load has been Single mode โ€” the new scene wipes out the old. But what if you want a persistent manager or a UI overlay to stay while levels stream in and out beneath it? In Lesson 5.2: Additive & Multi-Scene Workflows we load scenes on top of each other and learn to unload them cleanly.

๐ŸŽž๏ธ You can move between scenes now

Register scenes in Build Settings, load them by name, and hide the load behind a smooth progress bar. That's the difference between a jarring cut and a polished transition.