Search Unity

Resolved Additive scene loading with UNet, conflicting SceneIds on scene objects

Discussion in 'UNet' started by kophax, Apr 7, 2018.

  1. kophax

    kophax

    Joined:
    Jul 19, 2014
    Posts:
    20
    I'm working on a multiplayer game and I haven't been able to find an answer for this yet.

    A similar outline was posted in this question 2 years ago, though as far as I can tell the APIs haven't really changed.

    https://forum.unity.com/threads/une...-and-different-scenes-for-each-client.383468/

    I have a basic cut down version of what I'm trying to build.

    Scene A has 2 NetworkIdentity in it, Scene B has 1 NetworkIdentity.

    I have a scene manager class that sends messages from the server to the clients, having each of them perform a SceneManager.LoadScene additively. They send a message back to the server when they are done loading, and the server spawns objects with NetworkServer.SpawnObjects.

    The scene loading is working fine, except for when it comes to the NetworkIdenties.

    The problem I encounter is that when the NetworkServer.SpawnObjects() call reaches the clients, it seems to try and use the same NetworkIdentity.sceneId. For some reason this isn't a problem on the host (or when i run a single host game), but it results in the objects not spawning on the client with an error:

    Spawn scene object not found for X

    X in this case corresponds to the SceneId (as I understand it this is an incrementing value and not at all reslated to scene build index), which I'm assuming is an auto incremented value for each NetworkIdentity spawned in the scene.

    When I load a scene additively, it doesn't care about that object relative to the others loaded in the previous scene. This results in it colliding with other objects and failing the spawn. In the example above, this means that the single NetworkIdentity in Scene B, doesn't spawn because of one of the objects with that SceneId in Scene A. If i had two objects in Scene B, I would encounter the same problem twice:

    Spawn scene object not found for 1
    Spawn scene object not found for 2

    Any additional objects in Scene B, that aren't in Scene A would get a SceneId of 3+ and would work fine.

    Am I missing a core concept here that I'm not understanding properly? I can think of a solution whereby I put in some kind of placeholder object that spawns a prefab at runtime (the prefab would have the NetworkIdentity), but this seems like an awkward workaround.

    Is there a better way of ensuring the SceneId don't conflict with each other? This seems to be an entirely internal field that shouldn't be touched.

    Is there an easier way to additively load scenes, in UNet, with scene objects (there at build time, not spawned at runtime) that are NetworkIdentities?
     
  2. HiddenMonk

    HiddenMonk

    Joined:
    Dec 19, 2014
    Posts:
    987
    I am pretty sure unet HLAPI doesnt support additive scenes. I dont think many other unity networking solutions handle it either.
    Your workaround of having a placeholder and instantiating at runtime is probably the more simple and elegant solution.

    Another possible way, which is prone to breaking and I think only works so long as you never have the same scene loaded multiple times at once, could be to make your own script that sets up the scene ids in a way that they are unique across all scenes.
    I think this is how unity handles setting a scenes scene ids for all the networkidentities https://bitbucket.org/Unity-Technol...rocess.cs?at=5.3&fileviewer=file-view-default
    Notice they use the [PostProccessScene] attribute. You can make your own script that runs after unitys run to overwrite the scene ids to your desire by putting [PostProcessScene(1)]. Youd be limited to uint.MaxValue (4294967295) networked scene objects in all scenes combined though, but that should be more than enough for many. I guess a simple way would be to take uint.MaxValue and divide it by how many scenes you have in your build, the result will be the max allowed of networked scene objects you can have per scene. Then you can just use the scenes build index and multiply it by the max allowed networked scene objects per scene and that should give you a starting point for each of your scenes postprocessor to set the scene ids.
    Even with 1000 scenes, youll still be able to have over 4 million networked scene objects per scene. If they ever change it from a uint to ushort then youll have a lot less though.

    I am on a tablet, but I will try to give some untested code.

    Code (CSharp):
    1.  
    2. using System;
    3. using UnityEditor.Callbacks;
    4. using UnityEngine;
    5. using UnityEngine.Networking;
    6. using UnityEditor.SceneManagement
    7.  
    8. namespace UnityEditor
    9. {
    10.     public class CustomNetworkScenePostProcess
    11.     {
    12.         [PostProcessScene(1)]
    13.         public static void OnPostProcessScene()
    14.         {
    15.             int sceneCount = EditorSceneManager.sceneCountInBuildSettings > 0 ? EditorSceneManager.sceneCountInBuildSettings : 1;
    16.             int sceneBuildIndex = EditorSceneManager.GetActiveScene().buildIndex; //might not work due to this bug? https://issuetracker.unity3d.com/issues/postprocessscene-build-index-of-active-scene-is-equal-to-1
    17.  
    18.             int maxSceneIdSize = (NetworkSceneId.Value.MaxValue - 1) / sceneCount; // we -1 since we dont use 0. Unity might be using 0 as a way to say its not set.
    19.  
    20.             int nextSceneId = (sceneBuildIndex * maxSceneIdSize) + 1; // +1 so its never 0
    21.             int maxSceneId = nextSceneId + maxSceneIdSize;
    22.  
    23.             foreach (NetworkIdentity uv in UnityEngine.Object.FindObjectsOfType<NetworkIdentity>())
    24.             {
    25.                 // if we had a [ConflictComponent] attribute that would be better than this check.
    26.                 // also there is no context about which scene this is in.
    27.                 if (uv.GetComponent<NetworkManager>() != null)
    28.                 {
    29.                     Debug.LogError("NetworkManager has a NetworkIdentity component. This will cause the NetworkManager object to be disabled, so it is not recommended.");
    30.                 }
    31.                 if (uv.isClient || uv.isServer)
    32.                     continue;
    33.  
    34.                 if(nextSceneId >= maxSceneId)
    35.                 {
    36.                     Debug.LogError("Scene index " + sceneBuildIndex + " has more than the max allowed scene NetworkIdentities (" + maxSceneIdSize + "). Ignoring the extras.");
    37.                     break;
    38.                 }
    39.  
    40.                 uv.gameObject.SetActive(false);
    41.                 uv.ForceSceneId(nextSceneId++);
    42.             }
    43.         }
    44.     }
    45. }

    The code above might not work due to the bug I explain next to sceneBuildIndex.
    If it doesnt work, then maybe this will.

    Code (CSharp):
    1.  
    2. using System;
    3. using UnityEditor.Callbacks;
    4. using UnityEngine;
    5. using UnityEngine.Networking;
    6. using UnityEditor.Build;
    7. using UnityEngine.SceneManagement;
    8. using UnityEditor.SceneManagement;
    9.  
    10. namespace UnityEditor
    11. {
    12.     public class CustomNetworkScenePostProcess : IProcessScene
    13.     {
    14.         public int callbackOrder { get { return 1; } }
    15.         public void OnPostProcessScene(Scene scene)
    16.         {
    17.             int sceneCount = EditorSceneManager.sceneCountInBuildSettings > 0 ? EditorSceneManager.sceneCountInBuildSettings : 1;
    18.             int sceneBuildIndex = scene.buildIndex;
    19.  
    20.             int maxSceneIdSize = (NetworkSceneId.Value.MaxValue - 1) / sceneCount; // we -1 since we dont use 0. Unity might be using 0 as a way to say its not set.
    21.  
    22.             int nextSceneId = (sceneBuildIndex * maxSceneIdSize) + 1; // +1 so its never 0
    23.             int maxSceneId = nextSceneId + maxSceneIdSize;
    24.  
    25.             foreach (NetworkIdentity uv in UnityEngine.Object.FindObjectsOfType<NetworkIdentity>())
    26.             {
    27.                 // if we had a [ConflictComponent] attribute that would be better than this check.
    28.                 // also there is no context about which scene this is in.
    29.                 if (uv.GetComponent<NetworkManager>() != null)
    30.                 {
    31.                     Debug.LogError("NetworkManager has a NetworkIdentity component. This will cause the NetworkManager object to be disabled, so it is not recommended.");
    32.                 }
    33.                 if (uv.isClient || uv.isServer)
    34.                     continue;
    35.  
    36.                 if(nextSceneId >= maxSceneId)
    37.                 {
    38.                     Debug.LogError("Scene index " + sceneBuildIndex + " has more than the max allowed scene NetworkIdentities (" + maxSceneIdSize + "). Ignoring the extras.");
    39.                     break;
    40.                 }
    41.  
    42.                 uv.gameObject.SetActive(false);
    43.                 uv.ForceSceneId(nextSceneId++);
    44.             }
    45.         }
    46.     }
    47. }

    I never tried this so its all theoretical and I think only works if you just want to load additively different scenes at once and not load a single scene additively multiple times at once (so you can have scene1 and scene2 loaded, but not scene1 and scene1 again).
     
    Last edited: Apr 7, 2018
    kophax likes this.
  3. kophax

    kophax

    Joined:
    Jul 19, 2014
    Posts:
    20
    Thanks for the detailed reply HiddenMonk.

    I ended up writing this as a IProcessScene editor script:

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Networking;
    3. using UnityEngine.SceneManagement;
    4. using UnityEditor.Build;
    5. using UnityEditor.SceneManagement;
    6.  
    7. namespace UnityEditor
    8. {
    9.     public class AdditiveSceneIdPostProcessor : IProcessScene
    10.     {
    11.         private bool sceneIdsAssigned = false;
    12.  
    13.         public int callbackOrder
    14.         {
    15.             get
    16.             {
    17.                 return 1;
    18.             }
    19.         }
    20.  
    21.         public void OnProcessScene(Scene s)
    22.         {
    23.             // execute this only once in build mode instead of for each scene
    24.             // could not find a nice way of executing in play/build mode
    25.             // instead of processing this scene only, process all scenes in build
    26.             //if(sceneIdsAssigned) { return; }
    27.  
    28.             //sceneIdsAssigned = true;
    29.  
    30.             int sceneCount = EditorSceneManager.sceneCountInBuildSettings > 0 ? EditorSceneManager.sceneCountInBuildSettings : 1;
    31.             int maxSceneIdSize = s.buildIndex + 1 * 100;// (int.MaxValue - 1) / sceneCount;
    32.  
    33.             ForceSceneIds(s, maxSceneIdSize);
    34.  
    35.             //EditorBuildSettingsScene[] scenes = EditorBuildSettings.scenes;
    36.             //foreach(EditorBuildSettingsScene editorScene in EditorBuildSettings.scenes)
    37.             //{
    38.             //    Scene scene = EditorSceneManager.GetSceneByPath(editorScene.path);
    39.             //    ForceSceneIds(scene, maxSceneIdSize);
    40.             //}
    41.         }
    42.  
    43.         private static int ForceSceneIds(Scene scene, int maxSceneIdSize)
    44.         {
    45.  
    46.             int nextSceneId = (scene.buildIndex * maxSceneIdSize) + 1;
    47.             int maxSceneId = nextSceneId + maxSceneIdSize;
    48.  
    49.             foreach (GameObject rootObject in scene.GetRootGameObjects())
    50.             {
    51.                 foreach (NetworkIdentity uv in rootObject.GetComponentsInChildren<NetworkIdentity>(true))
    52.                 {
    53.                     Debug.Log(string.Format("{0} ForceSceneId({1})", uv.name, nextSceneId));
    54.                    
    55.                     if (uv.GetComponent<NetworkManager>() != null)
    56.                     {
    57.                         Debug.LogWarning("NetworkManager in "+ scene.name + " has a NetworkIdentity component. This will cause the NetworkManager object to be disabled, so it is not recommended.");
    58.                     }
    59.                    
    60.                     if (nextSceneId >= maxSceneId)
    61.                     {
    62.                         Debug.LogError("Scene index " + scene.buildIndex + " has more than the max allowed scene NetworkIdentities (" + maxSceneIdSize + "). Ignoring the extras.");
    63.                         break;
    64.                     }
    65.  
    66.                     uv.gameObject.SetActive(false);
    67.                     uv.ForceSceneId(nextSceneId++);
    68.                 }
    69.             }
    70.  
    71.             return nextSceneId;
    72.         }
    73.     }
    74. }
    75.  

    You can see where I commented out the code related to building all scenes on play (so I can run this in editor mode). After several attempts, the same problems seem to arise even though I can confirm that the SceneId was properly set on the additive scene I load at runtime. I still get the same error, I even have logs confirming that the NetworkIdentity on that object has a SceneId that matches the server but still no luck.

    I've tried delaying the SpawnObjects call until I'm certain the scene is fully loaded, but no luck. It also seems to run inconsistently when testing between clients and the unity editor, but run properly when testing between to built clients.

    I'm going to try a runtime approach similar to this, where I ForceSceneId on all NetworkIdentity as soon as I load the scene, instead of at build time. I'll follow up with my results.
     
  4. HiddenMonk

    HiddenMonk

    Joined:
    Dec 19, 2014
    Posts:
    987
    Youve said in your original post that scene objects in other scenes would work fine so long as their sceneID was different than the other scenes.

    That would lead me to believe that the new sceneID setting code we have is either not actually working, or is running to late.
    Remember that unity will also attempt to set the sceneID's when you hit play and build, so setting the sceneIDs in the editor before playing or building is useless unless its just for debugging purposes.

    You mean that in the editor you get errors, but in an actual build everything is working fine?

    If it works in builds but not in the Editor, then we might have a clue as to whats going wrong.
    IProcessScene seems to get called after Awake and OnEnable is called. If it works in the Build, its because the sceneIDs are set during the build, which means they will be set and ready before Awake and OnEnable is called, while in the Editor theyll get set only after Awake and OnEnable is called. (Id consider this a bug)
    Does any of your code cause scene stuff to happen during Awake or OnEnable? You said you delayed calling SpawnObjects, but I am not sure if there is anything else being called that might cause issues.

    Also, IProcessScene might get called twice during build time, so watch out for that. Youd need to make sure your code doesnt just run once and is able to be called both those times, otherwise the second time unity might overwrite your sceneIDs. (someone said only scene 0 is called twice?)
    https://issuetracker.unity3d.com/issues/postprocessscene-called-twice-during-a-build
     
  5. kophax

    kophax

    Joined:
    Jul 19, 2014
    Posts:
    20
    OK. I finally got it working the way I wanted.

    I moved away from IProcessScene because I found it too restrictive for testing in the editor when doing additive scene loads. Instead, I do it on the server/client right after I load the scenes, before I send my ready message.

    There was an extra step where i had to set ready false, then true on all clients after load, after all that my SpawnObjects call worked fine.

    Here's the script in case you or anyone else is interested in this. It's basic, but gets the job done. Has a few flaws around just auto decrementing a counter to assume "done", but it's good enough.

    https://gist.github.com/kristianpd/485fd7d78512a22a80117a2d22664185

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Networking;
    5. using UnityEngine.Networking.NetworkSystem;
    6. using UnityEngine.SceneManagement;
    7. using VRTK;
    8. using System.Linq;
    9.  
    10. /// <summary>
    11. /// The NetworkSceneManager class is used by the server to load, unload, and change the scenes players are currently playing through.
    12. /// The calls are networked, so when you load a scene it will load on all other clients. You can listen to events to know when the actions
    13. /// have been completed on the clients.
    14. /// </summary>
    15. public class NetworkSceneManager : NetworkBehaviour
    16. {
    17.     public delegate void SceneLoadedHandler(string sceneName);
    18.     /// <summary>
    19.     /// Event fired when all clients have finished loading a scene asynchronously.
    20.     /// </summary>
    21.     public event SceneLoadedHandler SceneLoaded;
    22.  
    23.     public delegate void SceneUnloadedHandler(string sceneName);
    24.     /// <summary>
    25.     /// Event fired when all clients have finished unloading a scene asynchronously.
    26.     /// </summary>
    27.     public event SceneUnloadedHandler SceneUnloaded;
    28.  
    29.     public delegate void ActiveSceneChangedHandler(string activeSceneName);
    30.     /// <summary>
    31.     /// Event fired when all clients have changed their active scene.
    32.     /// </summary>
    33.     public event ActiveSceneChangedHandler ActiveSceneChanged;
    34.  
    35.     /// <summary>
    36.     /// Singleton instance of this manager
    37.     /// </summary>
    38.     public static NetworkSceneManager instance;
    39.  
    40.     private Dictionary<string, int> sceneLoadedStatus;
    41.     private Dictionary<string, int> sceneUnloadedStatus;
    42.     private Dictionary<string, int> sceneSetActiveStatus;
    43.    
    44.     private NetworkClient m_Client;
    45.     private const short ClientLoadeSceneMsg = 1001;
    46.  
    47.     private void Awake()
    48.     {
    49.         sceneLoadedStatus = new Dictionary<string, int>();
    50.         sceneUnloadedStatus = new Dictionary<string, int>();
    51.         sceneSetActiveStatus = new Dictionary<string, int>();
    52.  
    53.         if (instance == null)
    54.         {
    55.             instance = this;
    56.             DontDestroyOnLoad(gameObject);
    57.         }
    58.         else if(instance != this)
    59.         {
    60.             Destroy(this);
    61.         }
    62.     }
    63.    
    64.     private void Start()
    65.     {
    66.         // register custom network message handlers
    67.         NetworkServer.RegisterHandler(NetworkMessages.CLIENT_SCENE_LOADED, MsgClientLoadedScene);
    68.         NetworkServer.RegisterHandler(NetworkMessages.CLIENT_SCENE_UNLOADED, MsgClientUnloadedScene);
    69.         NetworkServer.RegisterHandler(NetworkMessages.CLIENT_ACTIVE_SCENE_CHANGED, MsgClientActiveSceneChanged);
    70.     }
    71.  
    72.     private void OnDestroy()
    73.     {
    74.         // unregister custom network message handlers
    75.         NetworkServer.UnregisterHandler(NetworkMessages.CLIENT_SCENE_LOADED);
    76.         NetworkServer.UnregisterHandler(NetworkMessages.CLIENT_SCENE_UNLOADED);
    77.         NetworkServer.UnregisterHandler(NetworkMessages.CLIENT_ACTIVE_SCENE_CHANGED);
    78.     }
    79.  
    80.     /// <summary>
    81.     /// Additively loads a scene on all clients. This method can only be called from the server.
    82.     /// </summary>
    83.     /// <param name="sceneName">Name of scene to load, make sure it's in your build settings scene list.</param>
    84.     public void ServerLoadScene(string sceneName)
    85.     {
    86.         if (!isServer)
    87.         {
    88.             Debug.LogError("Attempting to call LoadScene from client. This method should only be called on the server");
    89.             return;
    90.         }
    91.  
    92.         ServerListenForLoadScene(sceneName);
    93.         LoadSceneAdditively(sceneName);
    94.         RpcLoadScene(sceneName);
    95.  
    96.         // Set all clients to not ready, they will flag themselves ready after scene load
    97.         NetworkServer.SetAllClientsNotReady();
    98.     }
    99.  
    100.     /// <summary>
    101.     /// Unloads a scene on all clients. This method can only be called from the server.
    102.     /// </summary>
    103.     /// <param name="sceneName">Name of scene to unload, make sure it's in your build settings scene list.</param>
    104.     public void ServerUnloadScene(string sceneName)
    105.     {
    106.         if (!isServer)
    107.         {
    108.             Debug.LogError("Attempting to call UnloadScene from client. This method should only be called on the server");
    109.             return;
    110.         }
    111.  
    112.         ServerListenForUnloadScene(sceneName);
    113.         UnloadSceneAsync(sceneName);
    114.         RpcUnloadScene(sceneName);
    115.     }
    116.  
    117.     /// <summary>
    118.     /// Changes the currently active scene. This method can only be called from the server.
    119.     /// </summary>
    120.     /// <param name="sceneName">Name of scene to make the active scene.</param>
    121.     public void ServerSetActiveScene(string sceneName)
    122.     {
    123.         if (!isServer)
    124.         {
    125.             Debug.LogError("Attempting to call SetActiveScene from client. This method should only be called on the server");
    126.             return;
    127.         }
    128.  
    129.         ServerListenForSetActiveScene(sceneName);
    130.         SetActiveScene(sceneName);
    131.         RpcSetActiveScene(sceneName);
    132.     }
    133.  
    134.     private void MsgClientLoadedScene(NetworkMessage msg)
    135.     {
    136.         string sceneName = msg.ReadMessage<StringMessage>().value;
    137.         ServerDecrementSceneLoadedCount(sceneName);
    138.     }
    139.  
    140.     private void MsgClientUnloadedScene(NetworkMessage msg)
    141.     {
    142.         string sceneName = msg.ReadMessage<StringMessage>().value;
    143.         ServerDecrementSceneUnloadedCount(sceneName);
    144.     }
    145.  
    146.     private void MsgClientActiveSceneChanged(NetworkMessage msg)
    147.     {
    148.         string sceneName = msg.ReadMessage<StringMessage>().value;
    149.         ServerDecrementSetActiveSceneCount(sceneName);
    150.     }
    151.  
    152.     private void ServerListenForLoadScene(string sceneName)
    153.     {
    154.         sceneLoadedStatus[sceneName] = NetworkServer.connections.Count;      
    155.     }
    156.  
    157.     private void ServerListenForUnloadScene(string sceneName)
    158.     {
    159.         sceneUnloadedStatus[sceneName] = NetworkServer.connections.Count;
    160.     }
    161.  
    162.     private void ServerListenForSetActiveScene(string sceneName)
    163.     {
    164.         sceneSetActiveStatus[sceneName] = NetworkServer.connections.Count;
    165.     }
    166.  
    167.     /// <summary>
    168.     /// HACK: These methods aren't really meant to be used long term, but are more temporary
    169.     /// while we rough in the network events. The current problem with this approach is it doesn't
    170.     /// handle a client disconnect properly. To do that, we'll need to watch the network manager
    171.     /// and adjust our expectations accordingly (GameNetworkManager.OnServerRemovePlayer) if someone
    172.     /// disconnects while loading. For now this is good enough.
    173.     /// </summary>
    174.     /// <param name="sceneName"></param>
    175.     private void ServerDecrementSceneLoadedCount(string sceneName)
    176.     {
    177.         if (sceneLoadedStatus.ContainsKey(sceneName))
    178.         {
    179.             sceneLoadedStatus[sceneName]--;
    180.  
    181.             if (sceneLoadedStatus[sceneName] <= 0)
    182.             {
    183.                 DoneLoading(sceneName);
    184.             }
    185.         }
    186.     }
    187.  
    188.     private void DoneLoading(string sceneName)
    189.     {
    190.         NetworkServer.SpawnObjects();
    191.         FireSceneLoaded(sceneName);
    192.     }
    193.    
    194.     private void ServerDecrementSceneUnloadedCount(string sceneName)
    195.     {
    196.         if (sceneUnloadedStatus.ContainsKey(sceneName))
    197.         {
    198.             sceneUnloadedStatus[sceneName]--;
    199.  
    200.             if (sceneUnloadedStatus[sceneName] <= 0)
    201.             {
    202.                 DoneUnloading(sceneName);
    203.             }
    204.         }
    205.     }
    206.  
    207.     private void DoneUnloading(string sceneName)
    208.     {
    209.         FireSceneUnloaded(sceneName);
    210.     }
    211.  
    212.     private void ServerDecrementSetActiveSceneCount(string activeSceneName)
    213.     {
    214.         if (sceneSetActiveStatus.ContainsKey(activeSceneName))
    215.         {
    216.             sceneSetActiveStatus[activeSceneName]--;
    217.  
    218.             if (sceneSetActiveStatus[activeSceneName] <= 0)
    219.             {
    220.                 DoneSetActiveScene(activeSceneName);
    221.             }
    222.         }
    223.     }
    224.  
    225.     private void DoneSetActiveScene(string activeSceneName)
    226.     {
    227.         FireActiveSceneChanged(activeSceneName);
    228.     }
    229.  
    230.     [ClientRpc]
    231.     private void RpcLoadScene(string sceneName)
    232.     {
    233.         // do not load the scene if the client is also the host
    234.         if (!isServer)
    235.         {
    236.             LoadSceneAdditively(sceneName);
    237.         }
    238.     }
    239.  
    240.     [ClientRpc]
    241.     private void RpcUnloadScene(string sceneName)
    242.     {
    243.         // do not unload the scene if the client is also the host
    244.         if (!isServer)
    245.         {
    246.             UnloadSceneAsync(sceneName);
    247.         }
    248.     }
    249.  
    250.     [ClientRpc]
    251.     private void RpcSetActiveScene(string sceneName)
    252.     {
    253.         // do not set the active scene if the client is also the host
    254.         if (!isServer)
    255.         {
    256.             SetActiveScene(sceneName);
    257.         }
    258.     }
    259.  
    260.     private void LoadSceneAdditively(string sceneName)
    261.     {
    262.         StartCoroutine(DoLoadSceneAsync(sceneName));
    263.     }
    264.  
    265.     private IEnumerator DoLoadSceneAsync(string sceneName)
    266.     {
    267.         AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
    268.  
    269.         while (!asyncLoad.isDone)
    270.         {
    271.             yield return null;
    272.         }
    273.        
    274.         // ForceSceneId on all network identities to prevent collisions when loading multiple scenes
    275.         SetSceneIds(SceneManager.GetSceneByName(sceneName));
    276.  
    277.         // Send ready message to server indicating this client is ready to spawn objects
    278.         ClientScene.Ready(Player.localPlayer.connectionToServer);
    279.  
    280.         if (isServer)
    281.         {
    282.             ServerDecrementSceneLoadedCount(sceneName);
    283.         }
    284.         else
    285.         {
    286.             SendClientSceneLoadedMessage(sceneName);
    287.         }
    288.     }
    289.  
    290.     private void UnloadSceneAsync(string sceneName)
    291.     {
    292.         Scene oldScene = SceneManager.GetSceneByName(sceneName);
    293.  
    294.         if (oldScene.buildIndex == -1)
    295.         {
    296.             Debug.LogError("Attempting to unload scene that is not in the build settings: " + sceneName);
    297.         }
    298.         else if (oldScene == SceneManager.GetActiveScene())
    299.         {
    300.             Debug.LogError("Attempting to unload active scene: " + sceneName + ". You can not unload the active scene");
    301.         }
    302.         else if(oldScene.isLoaded)
    303.         {
    304.             StartCoroutine(DoUnloadSceneAsync(oldScene));
    305.         }
    306.     }
    307.  
    308.     private IEnumerator DoUnloadSceneAsync(Scene scene)
    309.     {
    310.         string sceneName = scene.name;
    311.         AsyncOperation asyncUnload = SceneManager.UnloadSceneAsync(scene);
    312.  
    313.         while (!asyncUnload.isDone)
    314.         {
    315.             yield return null;
    316.         }
    317.        
    318.         if (isServer)
    319.         {
    320.             ServerDecrementSceneUnloadedCount(sceneName);
    321.         }
    322.         else
    323.         {
    324.             SendCLientSceneUnloadedMessage(sceneName);
    325.         }
    326.     }
    327.  
    328.     private void SetActiveScene(string sceneName)
    329.     {
    330.         Scene previouslyActiveScene = SceneManager.GetActiveScene();
    331.         Scene newScene = SceneManager.GetSceneByName(sceneName);
    332.  
    333.         if (!newScene.isLoaded)
    334.         {
    335.             Debug.LogError("Attempting to set unloaded scene to active scene: " + sceneName);
    336.         }
    337.         else if (newScene == previouslyActiveScene)
    338.         {
    339.             Debug.LogWarning("Attempting to set active scene to already active scene: " + sceneName);
    340.         }
    341.         else
    342.         {
    343.             SceneManager.SetActiveScene(newScene);
    344.            
    345.             if (isServer)
    346.             {
    347.                 ServerDecrementSetActiveSceneCount(newScene.name);
    348.             }
    349.             else
    350.             {
    351.                 SendClientActiveSceneChangedMessage(newScene.name);
    352.             }
    353.         }
    354.     }
    355.    
    356.     private void FireSceneLoaded(string sceneName)
    357.     {
    358.         if(SceneLoaded != null)
    359.         {
    360.             SceneLoaded(sceneName);
    361.         }
    362.     }
    363.  
    364.     private void FireSceneUnloaded(string sceneName)
    365.     {
    366.         if (SceneUnloaded != null)
    367.         {
    368.             SceneUnloaded(sceneName);
    369.         }
    370.     }
    371.  
    372.     private void FireActiveSceneChanged(string activeSceneName)
    373.     {
    374.         if (ActiveSceneChanged != null)
    375.         {
    376.             ActiveSceneChanged(activeSceneName);
    377.         }
    378.     }
    379.  
    380.     private void SendClientSceneLoadedMessage(string sceneName)
    381.     {
    382.         GameNetworkManager.singleton.client.Send(NetworkMessages.CLIENT_SCENE_LOADED, new StringMessage(sceneName));
    383.     }
    384.  
    385.     private void SendCLientSceneUnloadedMessage(string sceneName)
    386.     {
    387.         GameNetworkManager.singleton.client.Send(NetworkMessages.CLIENT_SCENE_UNLOADED, new StringMessage(sceneName));
    388.     }
    389.  
    390.     private void SendClientActiveSceneChangedMessage(string sceneName)
    391.     {
    392.         GameNetworkManager.singleton.client.Send(NetworkMessages.CLIENT_ACTIVE_SCENE_CHANGED, new StringMessage(sceneName));
    393.     }
    394.  
    395.     private void SetSceneIds(Scene scene)
    396.     {
    397.         int sceneCount = SceneManager.sceneCountInBuildSettings > 0 ? SceneManager.sceneCountInBuildSettings : 1;
    398.         int maxSceneIdSize = (int.MaxValue - 1) / sceneCount;
    399.  
    400.         ForceSceneIds(scene, maxSceneIdSize);
    401.     }
    402.  
    403.     private void ForceSceneIds(Scene scene, int maxSceneIdSize)
    404.     {
    405.         int nextSceneId = (scene.buildIndex * maxSceneIdSize) + 1;
    406.         int maxSceneId = nextSceneId + maxSceneIdSize;
    407.  
    408.         foreach (GameObject rootObject in scene.GetRootGameObjects().OrderBy(ro => ro.name))
    409.         {
    410.             foreach (NetworkIdentity networkIdentity in rootObject.GetComponentsInChildren<NetworkIdentity>(true))
    411.             {
    412.                 if (networkIdentity.GetComponent<NetworkManager>() != null)
    413.                 {
    414.                     Debug.LogWarning("NetworkManager in " + scene.name + " has a NetworkIdentity component. This will cause the NetworkManager object to be disabled, so it is not recommended.");
    415.                 }
    416.  
    417.                 if (nextSceneId >= maxSceneId)
    418.                 {
    419.                     Debug.LogError("Scene index " + scene.buildIndex + " has more than the max allowed scene NetworkIdentities (" + maxSceneIdSize + "). Ignoring the extras.");
    420.                     break;
    421.                 }
    422.                
    423.                 networkIdentity.ForceSceneId(nextSceneId++);
    424.             }
    425.         }
    426.     }
    427. }

    Player.localPlayer is my isLocal player, that's the connection I use to send the ClientScene.Ready message back to the server.
     
  6. Aupuma

    Aupuma

    Joined:
    Feb 15, 2017
    Posts:
    42
    Hi, first of all thanks for sharing your code! I'm trying to use it, but I'm not familiar with Network Messages (Unity Documentation does not help), so I'm a bit lost :confused:
    Do I need to create a NetworkMessages class and declare the message variables there? And from where I do need to receive the messages and send the callback?
     
  7. kophax

    kophax

    Joined:
    Jul 19, 2014
    Posts:
    20
    Hi @Aupuma, sorry for missing this reply. Yes you are correct.

    NetworkMessages was just a class with static short variables like CLIENT_SCENE_LOADED. I made it for any custom messages so I wouldn't accidentally use the same message id (short) for multiple messages.

    I use the `GameNetworkManager` to send the message because as far as I know, you need to send your messages via a `NetworkManager.instance`.

    You'll see where I register for those messages like this:

    NetworkServer.RegisterHandler(NetworkMessages.CLIENT_SCENE_LOADED, MsgClientLoadedScene);

    on line 67
     
  8. Juruhn

    Juruhn

    Joined:
    Jun 25, 2013
    Posts:
    13
    Hi Kophax,
    EXACTLY what I am looking for. Getting really strange behaviours with the Networkserver.Loadscene combined with VRTK and Singleton patterns with DND flags.

    Trying to get your solution to work, one thing I don't understand how you set it up though:
    So, do you store the Player reference to the 'isLocalPlayer' instance in all Player instances on the network somehow?

    Thanks for your post!
    Cheers,
    Jur
     
    Last edited: Jun 26, 2018
  9. kophax

    kophax

    Joined:
    Jul 19, 2014
    Posts:
    20
    Player.localPlayer

    is simply a static Player reference assigned in OnStartLocalPlayer()

    Code (CSharp):
    1.  
    2. public class Player : NetworkBehaviour
    3. {
    4.     public static Player localPlayer;
    5.  
    6.     public override void OnStartLocalPlayer()
    7.     {
    8.         localPlayer = this;
    9.         // ...
    10.      }
    11. }
    12.  
    Player of course being the behaviour on the player prefab object I spawn via the NetworkManager
     
  10. Juruhn

    Juruhn

    Joined:
    Jun 25, 2013
    Posts:
    13
    Thanks for the quick reply.

    Looks very simple indeed, but it doesn't click in my head yet.
    Is it not so that there are multiple 'Player' scripts on a client, only one is isLocalPlayer and therefor runs the OnStartLocalPlayer and therefor HIS localPlayer variable is set.
    The other 'Player's are merely representations of other spawned players (clients) and don't have that variable set.
    How is Player.localPlayer then the reference to that Player instance that is the local player. What am I missing? :)
     
  11. kophax

    kophax

    Joined:
    Jul 19, 2014
    Posts:
    20
    Yes. Only the player assigned to your client runs OnStartLocalPlayer. Other Player behaviour instances will not call this method.

    Player.localPlayer is a static variable. So although technically it could be seen as "shared across all player instances" local on this client (in that it belongs to the class), it is only ever assigned once and only ever represents the Player I have local authority over.

    I normally use it from other behaviours, such as in this case where I use it to find the right connection to send the message on. I don't want to teach them about how to find the "current player", so the static variable makes that an easy abstraction to use.

    Does that make more sense?
     
  12. IEdge

    IEdge

    Joined:
    Mar 25, 2017
    Posts:
    51
    @kophax Hello.

    This is my scenario:

    I have multiple scenes, from server I load all of those scenes additively. Client starts at login scene (that is the only scene that server does not load), when I change from login scene to "Level 1", scene begins in conflicts with the NetworkIdentity throwing the stupid errors Spawn scene object not found for 1 and bla, bla, bla...

    The script provided for you above, could help me in my situation?
     
  13. kophax

    kophax

    Joined:
    Jul 19, 2014
    Posts:
    20
    Yes @Triggeredge that sounds like the same kind of error. In this case, the server loads the scene, and asks other clients to do the same. They respond that they have loaded it and the server then spawns all the NetworkIdentity scene objects (the ones that are there from the start).

    Before saying they are ready (the clients) and before the server spawns, this scripts makes sure to rewrite each sceneid on the networkidentity's to be a unique ID (allocated by segmenting out chunks of integers up to max int based on the number of scenes in the build index)
     
  14. PortalVRit

    PortalVRit

    Joined:
    Oct 19, 2018
    Posts:
    15
    Hi @kophax,
    I'm struggling with this same problem, and seems you have found a solution.
    I've used your script and the additive scene is loading in both server and clients, but the sync objects (with Networ Identity compoennt )in scenes are not ativated with the additive loading.

    how do you have managed this?

    thanks.
     
    Last edited: Oct 28, 2018
  15. kophax

    kophax

    Joined:
    Jul 19, 2014
    Posts:
    20
    Hi @PortalVRit

    Did you get it working? Can you make sure that `SetSceneIds` is being called?
     
  16. mischa2k

    mischa2k

    Joined:
    Sep 4, 2015
    Posts:
    4,347
    Default UNET scene switching is broken. Clients receive network packets for the new scene before the new scene was fully loaded. This was never fixed, your only option is to use Mirror. I fixed it by pausing the update receive loop while a scene load is in progress.

    Note that I never tried async scene load though. So there could be more hidden errors.
     
  17. kophax

    kophax

    Joined:
    Jul 19, 2014
    Posts:
    20
    I think it's a broad statement to say your only option is to use Mirror (your asset), this was functional for the tests I ran with it. The particular issue I encountered had to do with SceneIds conflicting between scenes (restarting at 0). This approach basically just forces sceneids to be unique based on a scene build index offset. The default unity approach is to reload everything, at which point the scene id's starting over isn't a big deal.

    From my preliminary testing it was sufficient in loading a scene additively, and identities exhibited the same behaviours they would if the scene was loaded first and not additively. I will be back working on this additive scene loading stuff shortly and can report any problems I encounter that I hadn't noticed.

    What sort of messages are being sent by the server that are missed on the client @vis2k ?
     
  18. mischa2k

    mischa2k

    Joined:
    Sep 4, 2015
    Posts:
    4,347
    It's not your only option.
    Your other option is to download the HLAPI source code and fix it yourself.

    The bug is there though, no way around it :)

    All of them can be missed. The server happily keeps sending to clients during scene changes. If you have a fast computer then you won't notice it. If you have a slow computer then the client will receive half the messages for the new scene before it was loaded.
     
  19. kophax

    kophax

    Joined:
    Jul 19, 2014
    Posts:
    20
    OK i'll have to investigate more. Our game/testing has all been LAN so maybe there's bigger issues with latency.

    I'm going to be back on the additive scene loads shortly, I'll explore maybe looking at both options (Mirror plus something like this) to make sure the scene id stuff works as well as the messages.

    It's very unfortunate that HLAPI has been so abandoned...
     
  20. mischa2k

    mischa2k

    Joined:
    Sep 4, 2015
    Posts:
    4,347
    We reopened the issue on the Mirror repo: https://github.com/vis2k/Mirror/issues/72
    I think I know how to fix it, see the discussion there
     
  21. NatureRaph

    NatureRaph

    Joined:
    Aug 13, 2021
    Posts:
    8
    So I saw your Github with your solution(https://gist.github.com/kristianpd/485fd7d78512a22a80117a2d22664185) and have a question, so is the NetworkSceneManager the Replacement for Unity´s one? So can I load my Scenes with Addressables by changing that?