Search Unity

  1. Unity 2018.1 has arrived! Read about it here
    Dismiss Notice
  2. Scriptable Render Pipeline improvements, Texture Mipmap Streaming, and more! Check out what we have in store for you in the 2018.2 Beta.
    Dismiss Notice
  3. If you couldn't join the live stream, take a peek at what you missed.
    Dismiss Notice
  4. Improve your Unity skills with a certified instructor in a private, interactive classroom. Learn more.
    Dismiss Notice
  5. ARCore is out of developer preview! Read about it here.
    Dismiss Notice
  6. Magic Leap’s Lumin SDK Technical Preview for Unity lets you get started creating content for Magic Leap One™. Find more information on our blog!
    Dismiss Notice
  7. Want to see the most recent patch releases? Take a peek at the patch release page.
    Dismiss Notice

Additive scene loading with UNet, conflicting SceneIds on scene objects

Discussion in 'Connected Games' started by kophax, Apr 7, 2018.

  1. kophax

    kophax

    Joined:
    Jul 19, 2014
    Posts:
    12
    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:
    960
    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:
    12
    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:
    960
    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:
    12
    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:
    12
    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:
    12
    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