MirrorNetworking / Mirror

#1 Open Source Unity Networking Library
https://mirror-networking.com
MIT License
5.12k stars 761 forks source link

Additive Scene Loading Keeps networkidentity objects disabled #2181

Closed francescoStrada closed 3 years ago

francescoStrada commented 4 years ago

When I issue an additive scene load from the server to all the clients via:

NetworkServer.SendToAll(new SceneMessage { sceneName = additiveLoadingScene, sceneOperation = SceneOperation.LoadAdditive });

The scene is correctly loaded and also (on the client) I receive all the callbacks, namely:

public override void OnClientChangeScene(string newSceneName, SceneOperation sceneOperation, bool customHandling) { Debug.Log($"PREPARING LO LOAD SCENE {newSceneName}, {sceneOperation}"); }

public override void OnClientSceneChanged(NetworkConnection conn) { base.OnClientSceneChanged(conn); Debug.Log("FinishedLoading Scene"); }

However, all the GameObjects with attached a network identity component remain disabled in the added scene.

I don't know if it is a bug (I don't really think so) or am I missing something else I need to call? From the documentation and the example I could not find anything, could you please help?

Thanks a lot!

Francesco

francescoStrada commented 4 years ago

After discussing with some users on the Mirror server and digging further their proposed solutions I discovered some stuff which I would like to share.

One user (Shai) suggested to update and rebuild the observers for each networked identities. However, this didn't work since the observers collection was not initialized on the game objects (with a NetworkIdentity attached) contained in the additively added scene. The proposed solution was inspired by the behaviour of NetworkProximityChecker contained in the scene "SubScene" of the AdditiveScenes example.

However, looking at this example, things seemed to work properly and at that point, I realized which was the main difference between the example and my implementation. In the example the additive scene was loaded BEFORE all clients would connect whereas I am adding the scene AFTER some time my clients have all connected.

At this point, I found the function NetworkServer.SpawnObjects() but still things didn't work because the client didn't have in the ClientScene.spawnableObjects collection the objects present in the newly added scene.

SOLUTION: when the clients completes the additive scene loading process (issued by the server), I use the NetworkManager.OnClientSceneChanged callback, I invoke ClientScene.PrepareToSpawnSceneObjects(); so that the ClientScene.spawnableObjects collection gets populated with the objects present in the additive scene. And then once all this is completed (on ALL clients) the server receives a message and issues a NetworkServer.SpawnObjects() is it clear? the only thing that baffles me is that with both ClientScene.PrepareToSpawnSceneObjects() and NetworkServer.SpawnObjects() the iteration included in these functions have to go through ALL objects also the ones which have already previously been correctly spawned. I checked and in these function, there are some simple checks which should not clutter the main thread and probably I guess it is more a pain in the ass (code-wise) to operate only on the newly added objects rather check them all. If I encounter some major performance issues I'll let know.

If you prefer to have a working example I can prepare a simple project showing my implementation. Overall, I don't believe this to be a bug and also maybe it is a very particular scenario, but maybe some few lines in the guides could be helpful.

Let me know in which way I can be of any help, and if you want the example project is it okay a package with simply my components (without mirror source code) or you prefer a complete (with mirror) version.

Regards,

Francesco

francescoStrada commented 4 years ago

I have some updates, I apologize in advance for the long post but I wanted to be as clear as possible. Example project included.

Scenario

I have a multi-scene setup which il loaded additively on both server and clients. The MainScene (A) contains all core logic (Network, user data download, etc...) and a set of secondary scenes (B,C,D,...) which contain environment objects. These scenes are loaded additively AFTER the clients have connected.

Note: all additive scenes (B,C,D,...) contain network identity objects

SceneWorkflow

NetworkServer.SendToAll(new SceneMessage(){ sceneName = additiveSceneLoaded, sceneOperation = SceneOperation.LoadAdditive})

Problem

Initially, in the previous comments, I noted that the main issue was that objects were not showing up (stayed disabled), but I had unintentionally added an Awake method on my implementation of NetworkManager without overriding nor calling the base method.

But know what I get on the clients as soon as they receive the SceneLoad message is this error (one for each net objects in the new scene):

Spawn scene object not found for D763D4974F41D27F SpawnableObjects.Count=0 UnityEngine.Logger:Log(LogType, Object) Mirror.ILoggerExtensions:LogError(ILogger, Object) (at Assets/Mirror/Runtime/Logging/LogFactory.cs:82) Mirror.ClientScene:SpawnSceneObject(SpawnMessage) (at Assets/Mirror/Runtime/ClientScene.cs:832) Mirror.ClientScene:OnSpawn(SpawnMessage) (at Assets/Mirror/Runtime/ClientScene.cs:783) Mirror.<>cDisplayClass31_01:<RegisterHandler>b__0(NetworkConnection, SpawnMessage) (at Assets/Mirror/Runtime/NetworkClient.cs:331) Mirror.<>c__DisplayClass7_02:b0(NetworkConnection, NetworkReader, Int32) (at Assets/Mirror/Runtime/MessagePacker.cs:148) Mirror.NetworkConnection:InvokeHandler(Int32, NetworkReader, Int32) (at Assets/Mirror/Runtime/NetworkConnection.cs:225) Mirror.NetworkConnection:TransportReceive(ArraySegment1, Int32) (at Assets/Mirror/Runtime/NetworkConnection.cs:278) Mirror.NetworkClient:OnDataReceived(ArraySegment1, Int32) (at Assets/Mirror/Runtime/NetworkClient.cs:172) UnityEngine.Events.UnityEvent2:Invoke(ArraySegment1, Int32) Mirror.TelepathyTransport:ProcessClientMessage() (at Assets/Mirror/Runtime/Transport/TelepathyTransport.cs:102) Mirror.TelepathyTransport:LateUpdate() (at Assets/Mirror/Runtime/Transport/TelepathyTransport.cs:136)

and

Could not spawn assetId=00000000-0000-0000-0000-000000000000 scene=15520482487183725183 netId=2 UnityEngine.Logger:Log(LogType, Object) Mirror.ILoggerExtensions:LogError(ILogger, Object) (at Assets/Mirror/Runtime/Logging/LogFactory.cs:82) Mirror.ClientScene:OnSpawn(SpawnMessage) (at Assets/Mirror/Runtime/ClientScene.cs:788) Mirror.<>cDisplayClass31_01:<RegisterHandler>b__0(NetworkConnection, SpawnMessage) (at Assets/Mirror/Runtime/NetworkClient.cs:331) Mirror.<>c__DisplayClass7_02:b0(NetworkConnection, NetworkReader, Int32) (at Assets/Mirror/Runtime/MessagePacker.cs:148) Mirror.NetworkConnection:InvokeHandler(Int32, NetworkReader, Int32) (at Assets/Mirror/Runtime/NetworkConnection.cs:225) Mirror.NetworkConnection:TransportReceive(ArraySegment1, Int32) (at Assets/Mirror/Runtime/NetworkConnection.cs:278) Mirror.NetworkClient:OnDataReceived(ArraySegment1, Int32) (at Assets/Mirror/Runtime/NetworkClient.cs:172) UnityEngine.Events.UnityEvent2:Invoke(ArraySegment1, Int32) Mirror.TelepathyTransport:ProcessClientMessage() (at Assets/Mirror/Runtime/Transport/TelepathyTransport.cs:102) Mirror.TelepathyTransport:LateUpdate() (at Assets/Mirror/Runtime/Transport/TelepathyTransport.cs:136)

Network Checker Solution Based on the comments of some users on Mirror, somebody suggested placing a SceneChecker on EACH network identity game object in the additively loaded scene. In this case, I am not getting the above errors on the client but am experiencing the original problem of having the game objects disabled.

My Solution

In the default NetworkManager.cs Awake() method there is a subscription to SceneManager.sceneLoaded += OnSceneLoaded;

Which invokes:

void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
    if (mode == LoadSceneMode.Additive)
    {
          if (NetworkServer.active)
          {
                    // TODO only respawn the server objects from that scene later!
                    NetworkServer.SpawnObjects();
                    if (logger.LogEnabled()) logger.Log("Respawned Server objects after additive scene load: " + scene.name);
         }
         if (NetworkClient.active)
         {
              ClientScene.PrepareToSpawnSceneObjects();
              if (logger.LogEnabled()) logger.Log("Rebuild Client spawnableObjects after additive scene load: " + scene.name);
         }
     }
}

So I figured out I had to call NetworkServer.SpawnObjects() on the SERVER and ClientScene.PrepareToSpawnSceneObjects() on the CLIENT.

So my order of calls is:

NetworkServer.SendToAll(new SceneMessage(){ sceneName = additiveSceneLoaded, sceneOperation = SceneOperation.LoadAdditive})

Example Project

Platform: WIN10 Unity: 2019.4 Mirror: 16.9.0 (latest from Asset Store) Transport: Telepathy

The project is all included in a single .unitypackage which already contains mirror.

There are 3 scenes: Main.unity -> the main scene containing all logic and MyNetworkManager (which inherits from NetworkManager) AdditiveScene_SceneCheckrer.unity -> a scene with cubes with a net identity and a SceneChecker components AdditiveScene_NoSceneCheckrer.unity -> a scene with cubes with a net identity (NO SceneChecker)

All the logic is contained in the MyNetworkManager.cs

if not already present assign the correct scenes at the bottom of MyNetworkManager accordingly:

image

The bool property Invoke Network Manager Base Awake controls if the base awake method is invoked (see My Solution for explanation). If you want to see the differences in running or not running the Awake method two different builds must be created. To see my solution working DISABLE this field (false).

Build all the 3 scenes with Main.unity as first in the build index. And Run. I am sure you will be familiar with the network hud :) Start a SERVER only AND a CLIENT (if you want even more).

On the SERVER you will have 2 buttons to load/unload each of the scenes (AdditiveScene_SceneCheckrer.unity and AdditiveScene_NoSceneCheckrer.unity)

image

I also include 2 builds: MirrorAdditiveScenes_Awake.zip -> which invokes the base NetworkManager.cs Awake() method MirrorAdditiveScenes_NO_Awake.zip -> which does NOT invoke the base NetworkManager.cs Awake() method

MirrorAdditiveScenes_package.zip MirrorAdditiveScenes_Awake.zip MirrorAdditiveScenes_NO_Awake.zip

SoftwareGuy commented 4 years ago

Has this been resolved?

francescoStrada commented 4 years ago

Hello, I apologize for all the fuss, yes the problem was resolved but it made me wonder on one thing which is described at the end in the Takeaway & Question header.

Solution

Thanks to the help of a user on Mirror (Shai

4509) we managed to make the ExampleScene AdditiveScenes work the way I wanted: load scenes additively AFTER the users have connected.

I report his notes:

Okay I'm not sure if this will help you, but I've got the Mirror example AdditiveScenes project working exactly the way you want it ( if I understand your needs correctly). And I didn't change any lines of code at all. All I did was remove the NetworkProximityChecker component from all the objects in the SUBscene. Then, in the AdditiveNetworkManager, I removed the LoadSubScenes() coroutine out of OnStartServer(), and instead moved it to a button that I can press from server. After I start a server and connect with a client, I press the button, and all the subscene objects load into server and client's scene. Is that the complete solution that you need? And if so could you give that a try and see if it works for you too?

Here's my AdditiveNetworkManager for reference:

public class AdditiveNetworkManager : NetworkManager
    {
        static readonly ILogger logger = LogFactory.GetLogger(typeof(AdditiveNetworkManager));

        [Scene]
        [Tooltip("Add all sub-scenes to this list")]
        public string[] subScenes;

        public bool button;

        private void Update()
        {
            if (button)
            {
                button = false;
                StartCoroutine(LoadSubScenes());
            }
        }

        public override void OnStartServer()
        {
            base.OnStartServer();
            // load all subscenes on the server only
            //StartCoroutine(LoadSubScenes());
        }

        IEnumerator LoadSubScenes()
        {
            logger.Log("Loading Scenes");

            foreach (string sceneName in subScenes)
            {
                NetworkServer.SendToAll(new SceneMessage { sceneName = sceneName, sceneOperation = SceneOperation.LoadAdditive });
                yield return SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
                if (logger.LogEnabled()) logger.Log($"Loaded {sceneName}");
            }
        }

        public override void OnStopServer()
        {
            StartCoroutine(UnloadScenes());
        }

        public override void OnStopClient()
        {
            StartCoroutine(UnloadScenes());
        }

        IEnumerator UnloadScenes()
        {
            logger.Log("Unloading Subscenes");

            foreach (string sceneName in subScenes)
                if (SceneManager.GetSceneByName(sceneName).IsValid() || SceneManager.GetSceneByPath(sceneName).IsValid())
                {
                    yield return SceneManager.UnloadSceneAsync(sceneName);
                    if (logger.LogEnabled()) logger.Log($"Unloaded {sceneName}");
                }

            yield return Resources.UnloadUnusedAssets();
        }
    }

(I didn't draw a GUI button, I just exposed a bool to the inspector)

Result

The result is that in the example scene things are working properly and also in my project, so please don't refer to the above project because there were some stupid mistakes which I overlooked.

Takeaway & Question

All this banging my head however made me think on a specific implementation on the NetworkManager, specifically at the callback OnSceneLoaded.

void OnSceneLoaded(Scene scene, LoadSceneMode mode)
        {
            if (mode == LoadSceneMode.Additive)
            {
                if (NetworkServer.active)
                {
                    // TODO only respawn the server objects from that scene later!
                    NetworkServer.SpawnObjects();
                    if (logger.LogEnabled()) logger.Log("Respawned Server objects after additive scene load: " + scene.name);
                }
                if (NetworkClient.active)
                {
                    ClientScene.PrepareToSpawnSceneObjects();
                    if (logger.LogEnabled()) logger.Log("Rebuild Client spawnableObjects after additive scene load: " + scene.name);
                }
            }
        }

Which is invoked at every scene changed call the methods

NetworkServer.SpawnObjects() -> >SERVER ClientScene.PrepareToSpawnSceneObjects() -> CLIENT

However in my implementation where the scene loading is issued by the server and then invoked on clients what happens if SERVER calls NetworkServer.SpawnObjects() BEFORE CLIENT has called ClientScene.PrepareToSpawnSceneObjects()?

So all I did (and the example above implements this) is waiting on the SERVER that ALL CLIENTS have finished loading (and invoked ClientScene.PrepareToSpawnSceneObjects() ) and only at this point call NetworkServer.SpawnObjects() on the SERVER.

I needed this behaviour also for other game logic purposes but is this unnecessary from a Mirror networking side? No I am running all locally and nothing breaks, but what happens if I have a series of users connected and NetworkServer.SpawnObjects is called BEFORE all clients have the scene ready?

Thanks for the support and please forgive me for the insane long message above which was a mistake I was committing on my side.

ciao,

Francesco

DanielLuu commented 3 years ago

Hi I'm still having this issue. Would it be better to just avoid additive scenes in this case?

MrGadget1024 commented 3 years ago

Hi I'm still having this issue. Would it be better to just avoid additive scenes in this case?

Please visit our Discord for support. Closing this issue.