At some point in almost every Unity project, you hit a wall that feels embarrassingly simple on the surface: your game has more than one scene, and now nothing works the way it did when everything was in one place. The player's health resets to full every time you load the next level. The score disappears when you transition to the game-over screen. Your music cuts out and restarts from the beginning whenever a new scene loads.
I ran into every single one of these problems on my first multi-scene project. My "fix" at the time was a GameManager singleton that I kept alive across scenes using DontDestroyOnLoad, which then grew into a sprawling 600-line god object that knew about the player, the UI, the audio system, the save system, and the current level index. It worked, technically. It was also the most painful part of the entire codebase to maintain.
This guide teaches you how Unity's scene system actually works, how to load and transition between scenes properly, and — most importantly — how to pass data between scenes cleanly using ScriptableObjects (introduced in our previous guide) so you never need a 600-line GameManager holding the whole project together.
What Is Scene Management in Unity?
Scene management is the system Unity uses to load, unload, and transition between scenes — which are the individual "rooms" your project is divided into, like a main menu, a level, a boss arena, and a game-over screen. Everything you place in the Hierarchy belongs to the currently active scene, and by default when you load a new scene, everything from the old one is completely destroyed and replaced.
Unity's primary tool for this is the SceneManager class, found in the UnityEngine.SceneManagement namespace. It handles everything from instantly swapping one scene for another to loading scenes additively in the background while gameplay continues.
Why Scene Management Causes So Many Problems
The core issue is that Unity's default scene loading behavior destroys everything in the current scene before loading the next one. This is clean and efficient, but it means any data that was living on a GameObject — player health, current score, inventory contents — is gone the moment the new scene loads, unless you explicitly designed a way to preserve it.
A common mistake beginners make is assuming that public static variables or global variables will automatically carry data across scenes. Static variables do persist between scenes in Unity, but using them for complex game state creates hidden dependencies that are difficult to debug and nearly impossible to test in isolation.
The patterns in this guide give you better options.
When To Use Multiple Scenes
- Separating the main menu from gameplay so the menu does not load any gameplay assets until needed.
- Dividing a large game world into separate levels or zones that are loaded individually to manage memory.
- Keeping a persistent loading scene active while heavier content loads in the background.
- Separating boss arenas or cutscene areas from the main exploration scene to keep individual scene complexity manageable.
When To Keep Things in One Scene
- For small games or prototypes where the complexity of managing scene transitions outweighs any benefit.
- When you are still building and testing core mechanics — splitting into scenes too early adds unnecessary friction during development.
- For elements like a persistent HUD or audio manager that need to survive across every scene — these are better handled with DontDestroyOnLoad or ScriptableObjects rather than being placed in every scene.
Advantages of Proper Scene Management
| Advantage | Why It Helps |
| Memory control | Only the assets for the currently active scene need to be in memory, reducing RAM usage significantly on large projects. |
| Faster iteration | Opening a specific scene directly for testing is faster than navigating a huge single scene to find what you need. |
| Cleaner organization | Separating the main menu, each level, and the game-over screen into individual scenes keeps each one focused and manageable. |
| Additive loading possibilities | Scenes can be loaded on top of each other (additively), enabling seamless world streaming without any visible loading screen. |
Disadvantages of Scene Management
| Disadvantage | Why It Matters |
| Data persistence complexity | Any data that needs to survive a scene transition requires deliberate design — it does not happen automatically. |
| Reference management | Scripts that reference objects across scenes need careful handling since objects from one scene are destroyed when loading another. |
| Build settings management | Every scene that needs to be loadable at runtime must be manually added to the Build Settings list, which is easy to forget. |

Project Setup: Adding Scenes to Build Settings
Before any scene can be loaded at runtime by name or index, it must be registered in Build Settings. This is the step that trips up almost every beginner the first time, because the error message you get when a scene is missing from Build Settings is not immediately obvious.
- Go to File > Build Settings.
- Drag each scene file from your Project window into the Scenes In Build list, or click Add Open Scenes while each scene is open.
- The scene at index 0 is always loaded first when the game starts — this should typically be your main menu or a dedicated initialization scene.
A quick tip: name your scenes clearly and consistently. MainMenu, Level_01, Level_02, GameOver, and Loading are much easier to work with than Scene1, Scene2, UntitledScene. This matters because if you ever load a scene by name (rather than by index), a typo in the name string is an easy, hard-to-spot bug.
Part 1: Basic Scene Loading
Loading a Scene Instantly
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneLoader : MonoBehaviour
{
public void LoadSceneByName(string sceneName)
{
SceneManager.LoadScene(sceneName);
}
public void LoadSceneByIndex(int sceneIndex)
{
SceneManager.LoadScene(sceneIndex);
}
public void ReloadCurrentScene()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
public void LoadNextScene()
{
int nextIndex = SceneManager.GetActiveScene().buildIndex + 1;
if (nextIndex < SceneManager.sceneCountInBuildSettings)
{
SceneManager.LoadScene(nextIndex);
}
else
{
Debug.Log("No next scene — already at the last scene in Build Settings.");
}
}
}
Line By Line Explanation
SceneManager.LoadScene(sceneName); is the simplest scene load call. It destroys the current scene immediately and loads the named one. The string must exactly match the scene's filename (without the .unity extension) and the scene must be in Build Settings.
SceneManager.GetActiveScene().name returns the name of the currently active scene, which is useful for reloading the current level on death or retry without hardcoding its name in the script.
SceneManager.GetActiveScene().buildIndex + 1 gets the current scene's index in the Build Settings list and adds one, giving the next scene in sequence — a clean way to implement a "load next level" button without maintaining a manual level order in your scripts.
SceneManager.sceneCountInBuildSettings is the total number of scenes registered, used here to check that a "next" scene actually exists before trying to load it.
Part 2: Async Loading With a Loading Screen
Instant scene loading freezes the game for a noticeable moment on larger scenes. Async loading lets you show a loading screen and track progress while the new scene loads in the background.
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using System.Collections;
public class AsyncSceneLoader : MonoBehaviour
{
public Slider progressBar;
public string sceneToLoad;
public void StartLoading()
{
StartCoroutine(LoadAsync(sceneToLoad));
}
private IEnumerator LoadAsync(string sceneName)
{
AsyncOperation operation = SceneManager.LoadSceneAsync(sceneName);
operation.allowSceneActivation = false;
while (!operation.isDone)
{
float progress = Mathf.Clamp01(operation.progress / 0.9f);
if (progressBar != null)
{
progressBar.value = progress;
}
if (operation.progress >= 0.9f)
{
yield return new WaitForSeconds(0.5f);
operation.allowSceneActivation = true;
}
yield return null;
}
}
}
Key Details
LoadSceneAsync() begins loading the scene in the background without freezing the current frame. It returns an AsyncOperation object you can check for progress.
operation.allowSceneActivation = false prevents the new scene from becoming active even after it finishes loading, giving you control over exactly when the transition happens — useful for ensuring the loading bar reaches 100% visibly before switching.
operation.progress runs from 0 to 0.9 during loading, then stops at 0.9 and waits for allowSceneActivation to become true. This is why the progress calculation divides by 0.9 — it maps the 0-to-0.9 range onto a clean 0-to-1 for your progress bar.
When I first implemented a loading screen without knowing about the 0.9 behaviour, my progress bar would fill to 90% and then visibly freeze there until the next scene appeared. It looked broken even though it was technically working correctly. The allowSceneActivation trick fixed it immediately.
Part 3: Passing Data Between Scenes
This is the section most tutorials skip entirely, which is exactly why so many beginners end up with singleton GameManagers that know about everything.
Method 1: ScriptableObject Data Containers (Recommended)
Since we covered ScriptableObjects in the previous guide, this is the cleanest approach: store shared state in a ScriptableObject asset that both the sending and receiving scene reference directly.
using UnityEngine;
[CreateAssetMenu(fileName = "GameState", menuName = "Game Data/Game State")]
public class GameState : ScriptableObject
{
public int currentScore;
public int currentLevel;
public float playerHealth;
public bool hasUnlockedDoubleJump;
}
Your gameplay scene updates gameState.currentScore as the player earns points. When it loads the game-over scene, that scene reads gameState.currentScore from the same asset and displays it. No singleton, no static variable, no data loss — the ScriptableObject asset itself is the persistent data container.
The one rule to remember from the ScriptableObjects guide applies here too: changes made to this asset during play mode in the Editor persist after you exit play mode. For a runtime score counter this is usually fine during development, but for release builds you may want to reset values to defaults on game start.
Method 2: DontDestroyOnLoad (For GameObjects That Must Persist)
Sometimes you genuinely need a specific GameObject to survive every scene load — an audio manager that keeps music playing, or a system that truly needs to run continuously. DontDestroyOnLoad is the right tool for this case.
using UnityEngine;
public class PersistentAudioManager : MonoBehaviour
{
private static PersistentAudioManager instance;
void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject);
}
}
The pattern here is a guarded singleton: if an instance already exists (because this scene was loaded before and the object survived), the new duplicate is destroyed. If no instance exists, this one claims the singleton slot and is marked to survive scene loads.
Use this pattern sparingly. When I first learned DontDestroyOnLoad, I applied it to almost every manager in my project and ended up with a growing collection of persistent objects that were nearly impossible to test in isolation. The rule I follow now: DontDestroyOnLoad for systems that genuinely need continuous runtime presence (audio, input), and ScriptableObjects for data that simply needs to be available across scenes.
Method 3: PlayerPrefs (For Simple Persistent Values)
// Saving before scene transition
PlayerPrefs.SetInt("CurrentScore", currentScore);
PlayerPrefs.SetFloat("PlayerHealth", playerHealth);
PlayerPrefs.Save();
// Reading in the next scene
int savedScore = PlayerPrefs.GetInt("CurrentScore", 0);
float savedHealth = PlayerPrefs.GetFloat("PlayerHealth", 100f);
PlayerPrefs is the simplest possible option for small amounts of data. It persists between sessions (not just scenes), making it suitable for settings like audio volume, graphics quality, or a high score — but it is not designed for complex game state. Using it for anything beyond simple key-value pairs becomes messy quickly.
Part 4: Additive Scene Loading
Additive loading lets you load a second scene on top of an existing one, without destroying what is already loaded. This is how many modern games implement persistent elements like a HUD or audio manager as a separate scene that is always present, while the actual level content loads and unloads beneath it.
using UnityEngine;
using UnityEngine.SceneManagement;
public class AdditiveSceneManager : MonoBehaviour
{
public void LoadAdditive(string sceneName)
{
SceneManager.LoadScene(sceneName, LoadSceneMode.Additive);
}
public void UnloadScene(string sceneName)
{
SceneManager.UnloadSceneAsync(sceneName);
}
}
LoadSceneMode.Additive is the key parameter — without it, the default LoadSceneMode.Single destroys everything in the current scene first. With Additive, both scenes coexist in memory and the Hierarchy simultaneously.
A practical use of this for the series: load your HUD scene additively at game start, then load and unload individual level scenes beneath it as the player progresses. The HUD never gets destroyed, the player never loses their health bar reference, and you never need DontDestroyOnLoad for the UI — it simply lives in its own always-loaded scene.
Connecting to the Rest of the Series
Your Character Controller player from guide one carries health, current speed, and grounded state. None of that should live on the GameObject itself if you want it to survive a scene change — move the values that matter (starting health, move speed) into a PlayerConfig ScriptableObject from the previous guide, and let the MonoBehaviour read from it fresh each time it spawns.
Your HealthBarUI from the Canvas guide updates a UI Image based on the player's current health. If both the player scene and the HUD scene reference the same GameState ScriptableObject, the health bar reads the same value the player script writes to — zero shared GameObject references, zero scene dependency issues.
Your NavMesh enemy from guide eight targets the player's Transform. When a scene reloads, that Transform reference is destroyed and the enemy's target becomes null. The fix is to use a RuntimeSet ScriptableObject — a list of currently active player objects that enemies register themselves and their targets against at runtime, rather than holding a direct cross-scene reference.
Beginner Mistakes
- Forgetting to add scenes to Build Settings. A scene that exists in your Project window but is not in the Build Settings list cannot be loaded at runtime — you get a warning in the console and nothing happens.
- Storing scene-transition data in public static variables. Static variables persist between scenes, but they create hidden global state that makes code hard to test, debug, and reason about.
- Using DontDestroyOnLoad on every manager script. This leads to an ever-growing collection of persistent objects that duplicate themselves on scene reload if the singleton guard is missing.
- Holding direct Transform or Component references across scenes. Any reference to a GameObject in a scene that gets unloaded becomes null, causing NullReferenceException errors in every script that held that reference.
- Not accounting for the 0.9f cap in async loading. AsyncOperation.progress stops at 0.9 until allowSceneActivation is true — progress calculations that do not account for this produce a bar that freezes visibly before completing.
- Loading scenes by index instead of name in production builds. Build index order can accidentally change when scenes are reordered in Build Settings; loading by name is more resilient to that kind of accidental reordering.
Troubleshooting Guide
Problem: SceneManager.LoadScene() does nothing and logs a warning.
Likely cause: The scene is not added to Build Settings, or the scene name string has a typo.
Fix: Open File > Build Settings and confirm the scene is in the Scenes In Build list. Double-check the exact name string including capitalization — scene names are case-sensitive.
Problem: My player health resets every time I load a new scene.
Likely cause: Health is stored on a MonoBehaviour component that gets destroyed with the old scene.
Fix: Move current health to a GameState ScriptableObject that both scenes reference, or keep the player GameObject alive using DontDestroyOnLoad if the player truly needs to persist between scenes.
Problem: I have duplicate persistent GameObjects after reloading a scene.
Likely cause: The singleton guard in the DontDestroyOnLoad script is missing, so each scene load creates a new instance instead of destroying the duplicate.
Fix: Implement the guarded singleton pattern shown in this guide — check if an instance already exists in Awake() and destroy the new duplicate if so.
Problem: My loading bar freezes at 90% before the scene appears.
Likely cause: allowSceneActivation is set to false and never switched back to true, or the progress calculation does not account for the 0.9f cap.
Fix: Confirm your coroutine sets operation.allowSceneActivation = true after the progress check, and divide by 0.9f when mapping progress to your slider value.
Problem: My NavMesh enemy loses its player target reference after a scene reload.
Likely cause: The enemy holds a direct Transform reference to the player, which is destroyed and recreated as a new object when the scene reloads.
Fix: Use a RuntimeSet ScriptableObject pattern where the player registers itself on Start() and unregisters on OnDestroy(), and enemies look up the current player from the set rather than holding a direct reference.
Problem: My additive scene content overlaps my main scene unexpectedly.
Likely cause: The additively loaded scene contains objects positioned at the same coordinates as the main scene, or both scenes have cameras active simultaneously.
Fix: Design additive scenes carefully so their content coordinates do not overlap, and manage camera activation explicitly — only one camera should be active as the main camera at any given time unless you intend split-screen or stacked rendering.
Best Practices
- Always add every loadable scene to Build Settings before testing transitions, not as an afterthought before building.
- Load scenes by name rather than by index in production code, since index order can accidentally change.
- Use ScriptableObjects from the previous guide as your primary data-passing mechanism between scenes — they are cleaner and more testable than static variables or bloated singletons.
- Apply DontDestroyOnLoad selectively, only to systems that genuinely need continuous runtime presence, with a guarded singleton pattern to prevent duplicates.
- Use async loading for any scene that takes more than a fraction of a second to load, to avoid a visible freeze in gameplay.
- Consider a dedicated initialization scene (index 0) that sets up persistent systems and then immediately loads the main menu, keeping initialization logic separate from both gameplay and UI scenes.
- Test scene transitions regularly during development, not just at the end — cross-scene reference bugs are much easier to catch while the project is still small.
Key Takeaways
- SceneManager.LoadScene() destroys the current scene by default — any data on GameObjects is lost unless you explicitly preserve it.
- Every scene must be registered in Build Settings before it can be loaded at runtime.
- ScriptableObjects from the previous guide are the cleanest way to share data between scenes for most use cases.
- DontDestroyOnLoad is appropriate for systems needing continuous runtime presence, but must be paired with a singleton guard to prevent duplicates.
- Async loading with allowSceneActivation gives you full control over loading screen display and scene activation timing.
- Additive loading lets multiple scenes coexist simultaneously, enabling persistent HUDs and seamless world streaming.
Action Steps
- Create two simple test scenes (MainMenu and Level_01), add both to Build Settings, and implement the SceneLoader script to transition between them.
- Add a GameState ScriptableObject and write a score value from Level_01 that is displayed correctly in a GameOver scene.
- Implement the async loading coroutine with a simple UI Slider progress bar between two scenes.
- Build the guarded singleton PersistentAudioManager and confirm it does not duplicate across multiple scene loads.
- Test reloading your existing level scenes from the NavMesh guide and confirm enemy references survive correctly using the ScriptableObject approach.
Next Topics To Learn
- Unity ScriptableObjects Explained — the data-passing patterns in this guide depend directly on the ScriptableObject fundamentals covered there.
- Unity UI and Canvas Explained — wire your loading screen progress bar and game-over UI to the scene management system built in this guide.
- Unity Character Controller Explained for Complete Beginners — revisit how the player's data should be structured for clean scene transitions.