Unity6 Game Development · Lesson 16

Scenes, build profiles and asynchronous loading

Declare the destination in the build scene list and own the loader’s lifetime.

Unity 6 Editor walkthrough

The Unity script needs the Editor and the setup listed below. The separate C# companion practice runs in SharpRune without UnityEngine.

A destination needs a packaging contract

A scene transition joins code, packaged content and object lifetime. A string containing a valid-looking scene name does not ensure that a Player includes that scene. Unity 6.0 build profiles can use the global scene list or an override, so inspect the list actually used by the intended active profile. Keep names unambiguous or use a deliberately managed path or index policy. A scene that opens manually in the Editor can still be absent from a Player whose build configuration packages different content.

The sample chooses Single loading, which replaces the current scene rather than adding another scene beside it. That choice affects the object performing the load: an ordinary source-scene object may disappear during replacement. SceneLoader explicitly calls DontDestroyOnLoad on its root GameObject so the coroutine can observe completion. This is a narrow lifetime decision, not a prescription to make every manager persistent. A production loader should define duplicate prevention and teardown, especially when a destination or a later return scene contains another copy of the same service.

Portable C# companion: validate a name

The SharpRune companion uses a HashSet of allowed names containing Menu and LabDestination. The first membership check prints True and the Missing check prints False. The conditional assignment then changes current from Menu to LabDestination and prints that name. This is plain data validation and state assignment: no scene assets are opened, no build profile is inspected and no objects are replaced. Its successful result proves only that the supplied identifier belongs to the supplied set, which is a useful but limited part of a real loading boundary.

var allowed = new HashSet<string>(StringComparer.Ordinal) { "Menu", "LabDestination" };
Console.WriteLine(allowed.Contains("LabDestination")); Console.WriteLine(allowed.Contains("Missing"));
string current = "Menu"; string next = "LabDestination"; if (allowed.Contains(next)) current = next; Console.WriteLine(current);

Unity Editor: an explicitly persistent loader

SceneLoader.cs belongs in the Unity Editor project with two saved scenes and the correct Scene List. LoadDestination rejects edit-time calls, concurrent requests and a destination that the application cannot load. Its coroutine sets loading before yielding, requests LoadSceneAsync in Single mode and logs the active scene after completion. The excerpt shows the asynchronous operation boundary; it does not promise zero frame cost or a particular number of loading frames. Keep the Loader object at the root and avoid placing another persistent loader in LabDestination for this experiment.

private IEnumerator Load()
    {
        loading = true;
        AsyncOperation operation = SceneManager.LoadSceneAsync(destination, LoadSceneMode.Single);
        if (operation == null) { loading = false; yield break; }
        yield return operation;
        Debug.Log("Loaded: " + SceneManager.GetActiveScene().name, this);
        loading = false;
    }

Scene replacement changes ownership

An asynchronous name does not guarantee that every part of a transition is free of frame spikes. Content activation, initialization and other loading work still need measurement on the intended target. Avoid deriving a progress UI from assumptions about uniform work or assuming an early progress value means the destination is usable. Also consider error recovery before turning the example into a service: a failed or canceled user navigation should not permanently leave the application refusing every later request. The supplied small demo checks availability, but it is not a complete navigation queue or recovery framework.

Two independent scene-loading experiments

First, create an independent packaging experiment with Menu and LabDestination saved and included in the actual profile's scene list. Run the context action in Play mode, then create a development Player and repeat. Exclude the destination in a disposable profile copy and predict an availability error before loading. Hint: record whether the profile overrides the global list; looking at the wrong list explains many Editor-versus-Player surprises. Restore the valid configuration afterward, and keep the missing-scene result distinct from a code compilation or coroutine scheduling failure.

Second, investigate loader lifetime in a separate scene copy by removing the persistence call while leaving Single mode unchanged. Observe which source-scene objects survive replacement and whether the loader can still report its completion message. Hint: the application can finish a load even when the initiating object's coroutine no longer owns a live host. Restore persistence and then inspect a destination containing an accidental second loader. Explain the duplicate-service policy you would add rather than assuming DontDestroyOnLoad automatically provides singleton behavior or merges instances with the same script.

Review configuration and runtime evidence

Review a scene feature with evidence from both configuration and execution. Name the active profile, the effective scene list, the loading mode and the object that owns the operation until completion. A passing console name check cannot substitute for packaged-scene availability, and an Editor transition cannot substitute for a Player packaging test. Keep persistent services small and intentional, verify the actual destination visually and through the active-scene log, and document what repeated requests or duplicate loaders should do before expanding this demonstration into a game-wide navigation system.

  • Inspect the active profile's effective scene list.
  • Keep the persistent loader on a root GameObject.
  • Distinguish Single replacement from additive loading.
  • Repeat the configured transition in a development Player.
Runs in Unity 6 Editor

Unity Editor example

Set up in Unity 6

  1. Save a Menu scene containing a root empty Loader object with SceneLoader.cs.
  2. Save a second scene named LabDestination with a Camera and a distinguishable visible object.
  3. Open File > Build Profiles and add both scenes to the Scene List used by the intended profile.
  4. Start Play mode from Menu and invoke Load Destination; inspect the destination and persistent Loader.
  5. Create a development Player for a separate packaging test; avoid creating another persistent loader in the destination scene.
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
public sealed class SceneLoader : MonoBehaviour
{
    [SerializeField] private string destination = "LabDestination";
    private bool loading;
    private void Awake() { DontDestroyOnLoad(gameObject); }
    [ContextMenu("Load Destination")]
    public void LoadDestination()
    {
        if (!Application.isPlaying || loading) return;
        if (!Application.CanStreamedLevelBeLoaded(destination)) { Debug.LogError("Destination is not available: " + destination, this); return; }
        StartCoroutine(Load());
    }
    private IEnumerator Load()
    {
        loading = true;
        AsyncOperation operation = SceneManager.LoadSceneAsync(destination, LoadSceneMode.Single);
        if (operation == null) { loading = false; yield break; }
        yield return operation;
        Debug.Log("Loaded: " + SceneManager.GetActiveScene().name, this);
        loading = false;
    }
}

What to observe in Unity

The configured destination loads asynchronously in Single mode and the persistent root loader reports the active scene name. Missing build-list configuration is reported before starting a load.

The console practice below isolates the C# logic. It does not run a Unity scene or emulate Unity engine callbacks.

C# companion practice

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

var allowed = new HashSet<string>(StringComparer.Ordinal) { "Menu", "LabDestination" };
Console.WriteLine(allowed.Contains("LabDestination")); Console.WriteLine(allowed.Contains("Missing"));
string current = "Menu"; string next = "LabDestination"; if (allowed.Contains(next)) current = next; Console.WriteLine(current);
Open C# practice

C# practice output

True
False
LabDestination
Try this
A plain string plan is not a packaged scene; check the active build profile’s scene list.

Further reading