Help understanding NetworkSceneManager Scene Validation

Hi, I’ve been trying to convert my local multiplayer game to an online multiplayer experience using NGO, but I’m having trouble understanding how the scene validation on the NetworkSceneManager works…

To start things off, my game uses a multi-scene structure, so I need to load multiple scenes additively for my game to work. I also have a bootstrap scene that contains a bunch of managers that my game needs (including the NetworkManager) that is always loaded.

From my understanding, scenes that were already loaded before starting the server will be also loaded on the clients when the client connects, and any scene loaded by the server using the NetworkSceneManager after that is also loaded on all clients.

Now, reading through the Scene Validation section of the documentation, one of the examples says you can use Scene Validation to check is a scene is already pre-loaded on the client, thus avoiding duplication.

But here’s my problem: The server works as expected, but on the clients, my pre-loaded scenes are being unloaded! And I don’t want that, I want to keep the pre-loaded scenes!

Here’s a simplified version of the code I’m using:

Code

public class MultiSceneManager : MonoBehavior
{
    private NetworkManager NetworkManager => NetworkManager.Singleton;

    private void Awake()
    {
        NetworkManager.OnServerStarted += OnServerStarted;
        NetworkManager.OnServerStopped += OnServerStopped;
        NetworkManager.OnClientStarted += OnClientStarted;
        NetworkManager.OnClientStopped += OnClientStopped;
    }

    private void OnDestroy()
    {
        if(GameManager.IsQuitting)
            return;
     
        NetworkManager.OnServerStarted -= OnServerStarted;
        NetworkManager.OnServerStopped -= OnServerStopped;
        NetworkManager.OnClientStarted -= OnClientStarted;
        NetworkManager.OnClientStopped -= OnClientStopped;
    }

    private void OnServerStarted()
    {
        //The Scene manager only exists when we start a connection
        NetworkManager.SceneManager.OnSceneEvent += OnNetworkSceneEvent;
        NetworkManager.SceneManager.VerifySceneBeforeLoading = NetworkServerSceneValidation;
    }
 
    private void OnServerStopped(bool isHost)
    {
        if(GameManager.IsQuitting)
            return;
     
        NetworkManager.SceneManager.OnSceneEvent -= OnNetworkSceneEvent;
    }
 
    private void OnClientStarted()
    {
        if(NetworkManager.IsServer)
            return;
     
        //The Scene manager only exists when we start a connection
        NetworkManager.SceneManager.OnSceneEvent += OnNetworkSceneEvent;
        NetworkManager.SceneManager.VerifySceneBeforeLoading = NetworkClientSceneValidation;
    }

    private void OnClientStopped(bool isHost)
    {
        if(NetworkManager.IsServer)
            return;
     
        NetworkManager.SceneManager.OnSceneEvent -= OnNetworkSceneEvent;
    }

    private bool NetworkServerSceneValidation(int sceneIndex, string sceneName, LoadSceneMode loadSceneMode)
    {
        return true;
    }
 
    private bool NetworkClientSceneValidation(int sceneIndex, string sceneName, LoadSceneMode loadSceneMode)
    {
        Debug.Log($"===>  [CLIENT] Trying to load scene {sceneName}");
     
        //Don't not load scenes that are already loaded
        var scene = SceneManager.GetSceneByBuildIndex(sceneIndex);

        if (scene.isLoaded)
        {
            Debug.LogWarning($"===>  [CLIENT] Scene {sceneName} is loaded! Thus invalid");
            return false;
        }

        Debug.Log($"===>  [CLIENT] Scene {sceneName} is not loaded, thus valid");
        return true;
    }

    public void LoadScene(int sceneBuildIndex, LoadSceneMode loadSceneMode)
    {
        //Only the server can load/unload scenes
        if(!(NetworkManager.IsServer || NetworkManager.IsHost))
            return;

        var sceneName = GetSceneNameByBuildIndex(sceneBuildIndex);
        var status = NetworkManager.SceneManager.LoadScene(sceneName, loadSceneMode);

        if (status != SceneEventProgressStatus.Started)
        {
            Debug.LogError($"Failed to load {sceneName} with a {nameof(SceneEventProgressStatus)}: {status}");
            return;
        }
    }

    public void UnloadSceneUsingSceneManager(int sceneBuildIndex)
    {
        //Only the server can load/unload scenes
        if(!(NetworkManager.IsServer || NetworkManager.IsHost))
            return;

        var sceneName = GetSceneNameByBuildIndex(sceneBuildIndex);
        var scene = GetSceneByName(sceneName);
        var status = NetworkManager.SceneManager.UnloadScene(scene);

        if (status != SceneEventProgressStatus.Started)
        {
            Debug.LogError($"Failed to unload {sceneName} with a {nameof(SceneEventProgressStatus)}: {status}");
            return;
        }
    }

    public string GetSceneNameByBuildIndex(int buildIndex)
    {
        var scenePath = SceneUtility.GetScenePathByBuildIndex(buildIndex);
        var sceneName = Path.GetFileNameWithoutExtension(scenePath);
        return sceneName;
    }

    public Scene GetSceneByName(string sceneName)
    {
        return SceneManager.GetSceneByName(sceneName);
    }

    private void OnNetworkSceneEvent(SceneEvent sceneEvent)
    {
        switch (sceneEvent.SceneEventType)
        {
            case SceneEventType.Load:
                if (sceneEvent.ClientId == NetworkManager.LocalClientId)
                {
                    Debug.Log($"===> NetworkScene {sceneEvent.SceneName} loading started");
                    _networkOperation = sceneEvent.AsyncOperation;
                }
                break;
            case SceneEventType.Unload:
                if (sceneEvent.ClientId == NetworkManager.LocalClientId)
                {
                    Debug.Log($"===> NetworkScene {sceneEvent.SceneName} unloading started");
                    _networkOperation = sceneEvent.AsyncOperation;
                }
                break;
            case SceneEventType.LoadEventCompleted:
                Debug.Log($"===> NetworkScene {sceneEvent.SceneName} loaded");
                break;
            case SceneEventType.UnloadEventCompleted:
                Debug.Log($"===> NetworkScene {sceneEvent.SceneName} unloaded");
                break;
            case SceneEventType.LoadComplete:
                if (sceneEvent.ClientId == NetworkManager.LocalClientId)
                {
                    Debug.Log($"===> NetworkScene {sceneEvent.SceneName} loading completed");
                    _networkOperation = null;
                }
                break;
            case SceneEventType.UnloadComplete:
                if (sceneEvent.ClientId == NetworkManager.LocalClientId)
                {
                    Debug.Log($"===> NetworkScene {sceneEvent.SceneName} unloading completed");
                    _networkOperation = null;
                }
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }
    }
}

I’ve tried mixing and matching who does the validation, but nothing I did worked:

  • If I return true on both validations, existing scenes gets duplicated for a second, and the everything gets unloaded EXCEPT the ones loaded after the server started;
  • If I return true on the server but do the validation in the client (like in the code above), Apparently I don’t get the duplication, but all scenes are unloaded except for the ones that were loaded after the server started;
  • If I do the validation on the server but return true on the client, nothing get loaded nor unloaded, so basically nothing changes;
  • If I do the validation on both sides, again nothing gets loaded nor unloaded.

So, what am I doing wrong?

I haven’t used this before so I’m just going to throw out some expectations, hoping that it helps. :wink:

First, it’s important to note what exactly is the state when you call StartServer/StartHost. All of these scenes get synchronized to the client. And I believe there is no way of going around that to actually keep already loaded scenes on the client in that case - I may be wrong though, it’s just an expectation. At the very least, any already-loaded scenes on the client will not be networked.

The clients are free to load additional additive scenes after they joined a network session however.

To avoid complications regarding scene manager I think the best solution is to always single-load a specific game scene on the server right after you called StartServer/StartHost. The clients tag along with that scene load and can then additively load local-only scenes, such as GUI.

Now whenever a client (including the host) leaves, then the next thing I do is to single-load the non-game scene. Typically that will be a menu scene. So you basically switch between an offline and an online scene, where you start a new online session in the offline scene. The dedicated server can remain in the online scene, provided state reset is performed particularly when all clients have left since the new clients joining from then on may want to start with a clean slate (it’s a new “session”).

Lastly, the Active Scene Synchronization is perhaps what you may need to disable to make clients keep their already loaded scenes. However, this would seem to require more work to synchronize any networked scenes, at the least by instructing clients to load specific network scenes via RPCs.

PS: also try asking in the multiplayer discord channel since there are very active, knowledgable people around.

@CodeSmile Thanks for the answer. This wasn’t the answer I was looking for but fortunately I’ve figured it out.

As you mentioned, though, pre-loaded scenes are not synchronized, but in my case I don’t need them to be, I just need them to exist.

As for the unloading of pre-loaded scenes, in the Client Synchronization Mode page it’s explained that by default it uses LoadSceneMode.Single, which will unload any pre-loaded scenes in the client during the syncronization step. I had to change the NetworkSceneManager.SetClientSynchronizationMode to LoadSceneMode.Additive and the NetworkSceneManager.PostSynchronizationSceneUnloading to false for it to keep my scenes loaded, and I can even set the NetworkSceneManager.VerifySceneBeforeUnloading callback to do a validation on which scenes actually gets unloaded.

2 Likes