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
- Enable scene management on the
NetworkManager.
- 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.)
- Call
StartHost().
- Load a second scene through
NetworkManager.SceneManager.LoadScene(name, LoadSceneMode.Additive) and await OnLoadEventCompleted.
- 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
Description
In client-server (non-distributed-authority) mode,
NetworkSceneManagerseedsScenesLoadedwith the scenes that were already loaded when the server started, but does not seedServerSceneHandleToClientSceneHandlefor those same scenes.UnloadScenegates on the first table and passes, then fails on the second, so unloading such a scene always logsFailed to remove <Scene> scene handlesat error level even though the unload itself succeeds on every peer.The distributed-authority path does not have this problem:
InitializeScenesLoadedcallsUpdateServerClientSceneHandlefor 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
NetworkManager.UnityEngine.SceneManagement.SceneManagerbefore 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.)StartHost().NetworkManager.SceneManager.LoadScene(name, LoadSceneMode.Additive)and awaitOnLoadEventCompleted.NetworkManager.SceneManager.UnloadScene(scene).Actual Outcome
UnloadScenereturnsSceneEventProgressStatus.Startedand the scene is unloaded on the server and on all connected clients, but the following is logged at error level:RemoveServerClientSceneHandlealso returns before reaching itsScenesLoaded.Remove(clientHandle)line, so the unloaded scene stays inScenesLoadedfor the rest of the session.Expected Outcome
No error, and the entry removed from
ScenesLoaded, for a scene thatNetworkSceneManageritself recorded as a tracked scene at startup.Additional Context
The inconsistency is inside a single constructor.
NetworkSceneManager(NetworkManager)seedsScenesLoadedfor pre-existing scenes, with a comment stating that this case is intended:The handle table is seeded two lines below for the DDOL scene only, not for the scenes the loop just enumerated.
UnloadScenethen treatsScenesLoadedas the authority for whether the scene is known:A pre-existing scene passes that check, so the call proceeds, and then
RemoveServerClientSceneHandlefails onServerSceneHandleToClientSceneHandle.The distributed-authority counterpart seeds both tables:
Applying the same
UpdateServerClientSceneHandlecall 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