Search Unity

Unet with additive scene loading and different scenes for each client

Discussion in 'UNet' started by matthijskooijman, Feb 2, 2016.

  1. matthijskooijman

    matthijskooijman

    Joined:
    Jan 30, 2016
    Posts:
    1
    I've just started working with Unity for creating a (somewhat peculiar) multi-client game involving minigames. I'm planning to store each minigame as a separate scene, but multiple players might be running separate minigames at the same time (or even a separate instance of the same game). For this, I need additive loading of scenes, loading a single scene multiple times and loading different scenes on different clients, all while keeping the objects synchronized. I couldn't find documentation on whether this was supported (and I think it isn't really supported out of the box), but by experimenting and looking around the decompiled unity engine code, I found some ways to still make this usecase work. This post is still a work in progress, since I haven't properly tested all of my observations and ideas, but it might be useful in its current state to others (and I needed to keep notes for myself anyway).

    The canonical way of switching scenes in multiplayer is NetworkManager.ServerChangeScene(), which switches the server and all clients to a given scene and then takes care of linking up any NetworkIdenties on all clients with the ones in the server (assigning NetIds in the process) so they will remain synchronized afterwards. ServerChangeScene does not support additive loading, unfortunately.

    The other way of setting up synchronized objects is to instantiate them from a prefab and calling NetworkServer.Spawn() on them. This will instruct all clients to instantiate the same prefab, copies over the synchronized part of the object's state and assigns the same NetId on both sides to link the objects together for further synchronization. However, this only works for objects created from a prefab, not for objects loaded from a scene (since the latter will have a sceneId and thus trigger different code in NetworkServer.Spawn()).


    Looking at how the scene object spawning works, I've observed that:
    • NetworkManager.ServerChangeScene() marks all clients as not-ready, starts loading the scene itself and instructs all clients to do the same. When the server finishes loading a scene, it calls NetworkServer.SpawnObjects().
    • NetworkManager.SpawnObjects() iterates over all scene objects (GameObjects with a sceneId, meaning they were loaded from a scene) with a NetworkIdentity (regardless of what scene they came from) and activates them. Of these objects, it also spawns any of them that have not been spawned before (causing their OnServerStart() method to be called). This spawning also causes the spawn info to be sent to all clients marked as ready (e.g. clients that finish loading before the server), so these objects are activated, populated with an initial synchronized state and assigned a NetId on the clients.
    • When a client finishes loading and becomes ready, the server sends a "spawn finished" message with state 0, which causes the client to prepare for spawning by finding all scene objects with a network identity that are inactive and put them in an "spawnable object" list.
      The server then iterates over all objects previously spawned (based on a list populated when an object is spawned) and (after checking visibility) sends spawn messages for them to the now-ready client. This includes both scene objects (not limited to the scene just loaded) and objects dynamically spawned from a prefab. Note that these spawn messages are only sent when the given object is not being synchronized to the now-ready client already, so in practice this only applies to new objects.
      Finally, the server sends a "spawn finished" message with a state of 1, causing OnStartClient() to be ran on all previously activated objects (so all objects are activated and synchronized by the time OnStartClient() is ran in this case, but AFAICS not when the client is ready before the server is).
    • When a client receives a spawn message for a scene object (either because the server finished loading or because the client just became ready), it first looks for an object with the same netid. If found, the synchronization state in the spawn message is applied to that object (which should not normally really change anything sync the object is already synchronized). If no object with the same NetId is found, it looks for a spawnable object with the right scene id (using the list of inactive objects with a NetworkIdentity built during preparation). This object is then activated, has the synchronization state applied and is assigned the same NetId as the server.
    Seeing this, it seems we can actually bypass NetworkServer.ServerChangeScene() altogether, and just call SceneManager.LoadScene() directly (which *does* support additive loading). To still support synchronized scene objects, we should mimic what ServerChangeScene does:
    1. Mark all clients as not ready
    2. Load the scene on the server and all clients that need it
    3. When the server finishes loading, call NetworkServer.SpawnObjects()
    4. When the client finishes, call ClientScene.Ready()
    I've tested this approach and it does seem to work as expected (see code below).

    However, matching a scene object on the client happens based on the SceneId, which is an identifier of the object within current scene. These identifiers overlap between scenes, so when loading multiple scenes there is a risk of a spawn message being matched to a completely different scene object from a different scene on the client. For this, a couple of cases can be identified:
    • A client is completely in sync with the server (e.g. they have the same NetworkIdenties, which also have been previously spawned to link them with a common NetId) and then both load one additional scene. When a client is ready, the server will iterate *all* previously spawned network identities, including ones that are not in the scene that was just loaded. It might appear that this would allow server-side scene objects from previous scenes to match scene objects in the scene on the client, but this should not happen. Instead of sending spawn messages for all the server-side objects unconditionally, an internal observer mechanism is used on the server. Every connection observes synchronized objects that are visible to a connection and a spawn message is sent once for an object when the connection starts observing the object when it becomes visible (which can be due to the object being created or the client becoming ready). This means that normally, only spawn messages will be sent for objects in the newly loaded scene, which can be cleanly matched to objects from the new scene on the client. Even if spawn messages would somehow be sent for existing objects, the client will then see it already has them based on the NetId and not try to do SceneId matching for them. On the client side, sceneId matching only happens for inactive objects, so any previously spawned objects should also not interfere there.
    • A client has just connected to a server which has multiple scenes loaded. Normally, a server will have one scene loaded, which is also loaded on any connecting clients, so syncing and matching of scene objects works fine. However, when the server has multiple scenes loaded, spawn messages for all of the scene objects will be sent to the new client, even though the client might not have all of these scenes loaded. Since there will likely be duplicate scene ids among the objects sent by the server, the client will end up matching some spawn messages to the wrong client-side objects, and chaos ensues.
      It should be possible to use the UNet visibility mechanism to fix this. The server should track what scenes are loaded on each client and a component should be added to every GameObject with a NetworkIdentity, to allow define a custom OnRebuildObservers() method. This method should then mark an object as visible to a given client only if that client has the corresponding scene loaded (GameObject.scene can be used to match an object to a Scene). This should prevent any spawn messages from being sent, other than for the scene loaded by the client (e.g. the current scene according to NetworkManager). Then, any additional scenes can be loaded, with spawn messages being sent for each in turn. It is important to only load scenes one by one, since when multiple scenes are loaded at once, the client cannot distinguish the objects in them based on just the SceneId.
    • Different clients need different scenes. Loading a scene just one some clients but not all should be possible. However, once the server has some scenes loaded that a particular client does not, loading an addition scene for that client will again lead to SceneId confusion, just like the previous point. I believe that the fix is the same, though: Use visibility to prevent sending spawn messages for objects in scenes a client does not have loaded.
    One remaining caveat is that when a scene object is destroyed on the server, no spawn message will be sent for it to clients connecting after the object was destroyed. Normally this means the object will just hang around on a client remaining inactive, not affecting the game in any way. However, when an additional scene is loaded, this inactive object will still be used to match against any incoming spawn messages (based on the scene id). This will likely cause invalid matching (possibly depending on the ordering of results by Resources.FindObjectsOfTypeAll()). This can probably be solved by, after completing the spawning of a scene, destroy any objects that are still inactive.

    AFAICS, all this should also work when loading the same scene multiple times (e.g. for multiple clients, or even multiple times on/for a single client), as long as the scenes are loaded one by one and the full spawning process is completed for one scene before loading the next. One caveat with loading the same scene multiple times is that SceneManager does not seem to support unloading a specific Scene, but only supports unloading based on the name or build index of the scene. This seems like a simple omission in the API, though.


    There might be more caveats that I've missed so far. The complexity of how this all fits together is probably why there is no standard API for networked additive and per-client loading of scenes (yet?). The biggest problem here is probably that spawn messages for scene objects are only tied to a SceneId, but not to a specific scene. Of course, using a scene name would not be sufficient (if a scene is loaded multiple times), so I guess Scene objects should also get a dynamic NetId assigned (which can be sent from server to client by ServerChangeScene(), and probably needs some kind of API to expose and allow setting these NetIds for manual scene loading). I'm wondering if this was considered when writing the UNet stack (or is perhaps still planned for later?). Any Unity developers reading along this far? :)

    Most of what I've written here assumes a standalone server. For a host, things might be slightly different, but I haven't tried to wrap my head around that yet (I won't really need it either).


    As for the code example I promised, here's a snippet from my Player NetworkBehaviour (don't mind the dutch scene name "Puzzel" in there):

    Code (csharp):
    1.  
    2.     [ClientRpc]
    3.     public void RpcLoadPuzzel () {
    4.         Debug.LogError("Loading puzzel");
    5.         StartCoroutine(clientLoadPuzzel());
    6.     }
    7.  
    8.     public IEnumerator clientLoadPuzzel() {
    9.         yield return SceneManager.LoadSceneAsync("Puzzel", LoadSceneMode.Additive);
    10.         ClientScene.Ready (this.connectionToServer);
    11.     }
    12.  
    13.     [Command]
    14.     public void CmdLoadPuzzel() {
    15.         RpcLoadPuzzel ();
    16.         StartCoroutine(serverLoadPuzzel());
    17.  
    18.     }
    19.  
    20.     public IEnumerator serverLoadPuzzel() {
    21.         NetworkServer.SetAllClientsNotReady();
    22.         yield return SceneManager.LoadSceneAsync ("Puzzel", LoadSceneMode.Additive);
    23.         NetworkServer.SpawnObjects ();
    24.     }
    25.  
     
  2. SteamPunkProgrammer

    SteamPunkProgrammer

    Joined:
    Jan 27, 2015
    Posts:
    8
    I'm actually looking for a solution to this as well, at the moment I'm picking apart the NetworkManager, NetworkServer, and NetworkClient.

    One of the problems you stated has a pretty easy solution, that being the NetworkServer.SpawnObjects calls on ALL objects not just the newly loaded ones, its pretty easy to replicate the SpawnObjects method, but then include a scene name, or reference to a scene object, and only go through the objects within the roots of that scene (a new method was added in the last patch that allows you to access them easily).

    Their are still some remaining problems of course as you outlined, the biggest one being the ID issue. Not sure what the solution would be to that. A few of the other issues would be pretty easy to sort out because of the changes to scenes and there new management stuff, but there are some things that could require a lot of work.
     
  3. Severos

    Severos

    Joined:
    Oct 2, 2015
    Posts:
    181
    I'd suggest building the instances as "mini" exe, and whenever you want to load a new scene you simply start a new process of that instance and pass it the list of connections to accept, also don't forget to send the clients the new connection to the new process.
    This approach will reduce the load on your single process and allow better resource usage for you.

    Note: I haven't tried this at yet, it's what I've read in this forum in couple threads before.
     
  4. SteamPunkProgrammer

    SteamPunkProgrammer

    Joined:
    Jan 27, 2015
    Posts:
    8

    Sadly that is a very complex solution, to a much simpler problem, that would in-turn create more problems then it would solve due to states of characters as well as creating a huge pool of processes.
     
  5. Severos

    Severos

    Joined:
    Oct 2, 2015
    Posts:
    181
    It actually depends on the scenario, loading characters can be done from DB, if you don't want to put extra load on the main DB there can be a side DB used when moving characters between scenes. and depending on the instance nature if it's going to hold large number of players moving it to a separated process will reduce the number of players processing for the "main" server, however running tons of small instances can kill your processing.
     
  6. Ashkan_gc

    Ashkan_gc

    Joined:
    Aug 12, 2009
    Posts:
    1,124
    I could not make APIs like NetworkIdentity.ForceSceneID() and ClientScene.SetLocalObject solve my issue. I had different scenes for client and server and multiple scene objects with NetworkIdentity and I could not manage to make them Match their SceneID and NetID in both client and server.
     
  7. SteamPunkProgrammer

    SteamPunkProgrammer

    Joined:
    Jan 27, 2015
    Posts:
    8
    Using a DB isnt really an option here, its not an MMO being made, its just an action rpg experience.

    I'm starting to think there isn't a solution to the deeper SceneID problem, unless unity solve it internally. So it might be a problem that just has to be designed around for the moment.
     
  8. lucasmontec

    lucasmontec

    Joined:
    Apr 7, 2015
    Posts:
    97
    Unity devs should support this! Additive scenes are great to split individual scene setup and the 'game setup'. I was going to use an additive scene to store all HUD and routers and nw stuff... but after reading this, I'm better of with a prefab.
     
  9. Deleted User

    Deleted User

    Guest

    With your very detailed report, I was able to make my additive scene loading scene objects successfully.
    I had to change my scene ids because some of them overlapped each other :

    Code (CSharp):
    1.  
    2. NetworkIdentity[] sceneObjects = GetComponentsInChildren<NetworkIdentity>(true);
    3. foreach (NetworkIdentity sceneObject in sceneObjects) {
    4.     sceneObject.ForceSceneId(World.nextSceneID);
    5.     ++World.nextSceneID;
    6. }
    7.        
    Don't forget to do this on both client and server side, and before your call on NetworkServer.SpawnObjects()

    Thanks for everything, matthijskooijman !
    I understand much more the UNET spawing process after this reading.
     
    Last edited by a moderator: Nov 5, 2016
    Wabunga likes this.
  10. MFKJ

    MFKJ

    Joined:
    May 13, 2015
    Posts:
    264
    Hi All, its 2017 and i am using 5.4Unity. How can i load additive scene to all client? Please help. Thanks
     
  11. Phedg1

    Phedg1

    Joined:
    Mar 3, 2015
    Posts:
    113
    I am currently working on a workaround for this exception outlined in the OP.

    I will post my example project here once I've dealt with the exception outlined above.
     
    Last edited: Jan 29, 2017
  12. MFKJ

    MFKJ

    Joined:
    May 13, 2015
    Posts:
    264
    Thanks Phdg1 i am able to load the scene through a rpc call, but it will run from the host not from the client ( i don't know why- maybe rpc only host can make-if you know about it please share)

    Code (CSharp):
    1. [ClientRpc]
    2.     public void RpcLoadLevelAcrossNetwork() {
    3.         Debug.Log("NetworkServer.connections.Count :: " + NetworkServer.connections.Count);
    4.         SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
    5.  
    6.     }
    Although above code will load the additive scene but it disable my all objects that have network identity component. I checked UnetSceneObject manual it state that:

    it said that it will automatically active/spawn the newtwork object but in my case it is not doing anything like this. I have stuck here. please help if possible.
     
  13. Phedg1

    Phedg1

    Joined:
    Mar 3, 2015
    Posts:
    113
    EDIT: I had an error where I would prevent a client's connection from getting added to a NetworkIdentity.observers list using OnCheckObserver return false then add it back to the list once the client had loaded the scene. The object would completely lose its connection with the client side version, but only if the object was in an additvely loaded scene. The solution was to set the clients connection as not ready before I re-added the connection to the observers and set it as ready again once I had.
     
    Last edited: Jan 30, 2017
  14. MFKJ

    MFKJ

    Joined:
    May 13, 2015
    Posts:
    264
    I am getting this error as i use NetworkServer.SpawnObjects(); in new additive scene loading but it is not occuring on the server, occuring only on client. How did you load additive scene? and u getting this error!
     
  15. MFKJ

    MFKJ

    Joined:
    May 13, 2015
    Posts:
    264
    Well Sorry to say that i am new to networking. I don't know what you talking about OnCheckObserver etc. I didn't use it yet. Can you please tell me what is the problem in detail? I am having issue with the additive loaded scene, its all network identity object are deactive. I don't know why ? searching it solution form last couple of days. You can see this
     
  16. Phedg1

    Phedg1

    Joined:
    Mar 3, 2015
    Posts:
    113
  17. aranthel09

    aranthel09

    Joined:
    Jul 17, 2015
    Posts:
    66
    @Phedg1 sorry if this is too late to ask, but I downloaded your current project but I guess I'm too dumb to make it work.

    I started it, built and ran in order to make a server and joined the client with the editor, but nothing happens.
     
  18. Phedg1

    Phedg1

    Joined:
    Mar 3, 2015
    Posts:
    113
    My project was made in Unity 5.x, I can't recall exactly which, I have not tried it on any more recent version of Unity.
     
  19. kophax

    kophax

    Joined:
    Jul 19, 2014
    Posts:
    20
  20. Phedg1

    Phedg1

    Joined:
    Mar 3, 2015
    Posts:
    113
    Nested Prefabs later this year should fix all of this.