Skip to content

UnloadScene always errors on scenes loaded before the server started #4140

Description

@taygunsavas

Description

In client-server (non-distributed-authority) mode, NetworkSceneManager seeds ScenesLoaded with the scenes that were already loaded when the server started, but does not seed ServerSceneHandleToClientSceneHandle for those same scenes. UnloadScene gates on the first table and passes, then fails on the second, so unloading such a scene always logs Failed to remove <Scene> scene handles at error level even though the unload itself succeeds on every peer.

The distributed-authority path does not have this problem: InitializeScenesLoaded calls UpdateServerClientSceneHandle for each already-loaded scene, seeding both tables in the same loop.

This is not the "unloading a purely local scene through NetworkSceneManager" case that comes up in the forums. The scene here is one the server intends clients to have, clients do receive it during synchronization, and it does need to be unloaded through NetworkSceneManager.

Reproduce Steps

  1. Enable scene management on the NetworkManager.
  2. With scene management enabled, additively load a scene using UnityEngine.SceneManagement.SceneManager before starting the session. (In our project this is the normal flow: the player is in an offline room scene and only creates a lobby later, from an in-world UI.)
  3. Call StartHost().
  4. Load a second scene through NetworkManager.SceneManager.LoadScene(name, LoadSceneMode.Additive) and await OnLoadEventCompleted.
  5. Unload the scene from step 2 with NetworkManager.SceneManager.UnloadScene(scene).

Actual Outcome

UnloadScene returns SceneEventProgressStatus.Started and the scene is unloaded on the server and on all connected clients, but the following is logged at error level:

Failed to remove SC_Room scene handles [Server (-56222)][Local(-56222)]

RemoveServerClientSceneHandle also returns before reaching its ScenesLoaded.Remove(clientHandle) line, so the unloaded scene stays in ScenesLoaded for the rest of the session.

Expected Outcome

No error, and the entry removed from ScenesLoaded, for a scene that NetworkSceneManager itself recorded as a tracked scene at startup.

Additional Context

The inconsistency is inside a single constructor. NetworkSceneManager(NetworkManager) seeds ScenesLoaded for pre-existing scenes, with a comment stating that this case is intended:

// Since the server tracks loaded scenes, we need to add any currently loaded scenes on the
// server side when the NetworkManager is started and NetworkSceneManager instantiated when
// scene management is enabled.

if (!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer && networkManager.NetworkConfig.EnableSceneManagement)
{
    for (int i = 0; i < SceneManager.sceneCount; i++)
    {
        var loadedScene = SceneManager.GetSceneAt(i);
        ScenesLoaded.Add(loadedScene.handle, loadedScene);
    }
    SceneManagerHandler.PopulateLoadedScenes(ref ScenesLoaded, NetworkManager);
}

// Add to the server to client scene handle table
UpdateServerClientSceneHandle(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene);

The handle table is seeded two lines below for the DDOL scene only, not for the scenes the loop just enumerated.

UnloadScene then treats ScenesLoaded as the authority for whether the scene is known:

if (!ScenesLoaded.ContainsKey(sceneHandle))
{
    Debug.LogError($"{nameof(UnloadScene)} internal error! {sceneName} with handle {scene.handle} is not within the internal scenes loaded dictionary!");
    return SceneEventProgressStatus.InternalNetcodeError;
}

A pre-existing scene passes that check, so the call proceeds, and then RemoveServerClientSceneHandle fails on ServerSceneHandleToClientSceneHandle.

The distributed-authority counterpart seeds both tables:

internal void InitializeScenesLoaded()
{
    ...
    for (int i = 0; i < SceneManager.sceneCount; i++)
    {
        var loadedScene = SceneManager.GetSceneAt(i);
        UpdateServerClientSceneHandle(loadedScene.handle, loadedScene.handle, loadedScene);
    }
    SceneManagerHandler.PopulateLoadedScenes(ref ScenesLoaded, NetworkManager);
}

Applying the same UpdateServerClientSceneHandle call in the client-server branch of the constructor looks like it would close the gap, though I have not tested that change against your test suite.

Related documented behaviour, which is why we believe the scenario is supported rather than misuse: the manual states that a scene loaded server-side with UnityEngine.SceneManagement.SceneManager "will be synchronized with late joining clients unless you use server-side scene validation". We observe exactly that. Synchronization enumerates the loaded scenes rather than the handle table, so joining clients do receive the pre-existing scene and unload it correctly when the server unloads it. Only the server-side bookkeeping fails.

Environment

  • OS: macOS 15 (Darwin 25.6.0)
  • Unity Version: 6000.3.13f1
  • Netcode Version: 2.13.1
  • Netcode Topology: Client-Server

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions