Skip to content

fix: NetworkSceneManager exception when DontDestroyOnLoad NetworkObjects are being synchronized #1090

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Aug 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ internal set
/// <summary>
/// Gets whether or not the object should be automatically removed when the scene is unloaded.
/// </summary>
public bool DestroyWithScene { get; internal set; }
public bool DestroyWithScene { get; set; }

/// <summary>
/// Delegate type for checking visibility
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ public class NetworkSceneManager
private const NetworkChannel k_ChannelType = NetworkChannel.Internal;
private const NetworkUpdateStage k_NetworkUpdateStage = NetworkUpdateStage.EarlyUpdate;


internal Scene DontDestroyOnLoadScene;

/// <summary>
/// Constructor
/// </summary>
Expand All @@ -187,6 +190,36 @@ internal NetworkSceneManager(NetworkManager networkManager)
m_NetworkManager = networkManager;
SceneEventData = new SceneEventData(networkManager);
ClientSynchEventData = new SceneEventData(networkManager);

// If NetworkManager has this set to true, then we can get the DDOL (DontDestroyOnLoad) from its GaemObject
if (networkManager.DontDestroy)
{
DontDestroyOnLoadScene = networkManager.gameObject.scene;
}
else // Otherwise, we have to create a GameObject and move it into the DDOL to get the scene
{

#if UNITY_EDITOR || DEVELOPMENT_BUILD
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use UNITY_INCLUDE_TESTS instead of these...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JesseOlmer I am going to fix those in PR-1080 if that is ok?

// During unit and integration tests, we could initialize and then enable scene management
// which would make this generate an extra GameObject per instance. The DontDestroyOnLoadScene
// is internal so tests that are using multiInstance and that are moving NetworkObjects into
// the DDOL scene will have to manually set this. Otherwise, we can exclude DDOL stuff completely
// during unit testing.
if (m_IsRunningUnitTest)
{
return;
}
#endif
// Create our DDOL GameObject and move it into the DDOL scene so we can register the DDOL with
// the NetworkSceneManager and then destroy the DDOL GameObject
var myDDOLObject = new GameObject("DDOL-NWSM");
UnityEngine.Object.DontDestroyOnLoad(myDDOLObject);
DontDestroyOnLoadScene = myDDOLObject.scene;
UnityEngine.Object.Destroy(myDDOLObject);
}

ServerSceneHandleToClientSceneHandle.Add(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene.handle);
ScenesLoaded.Add(DontDestroyOnLoadScene.handle, DontDestroyOnLoadScene);
}

/// <summary>
Expand Down Expand Up @@ -242,16 +275,33 @@ internal void SetTheSceneBeingSynchronized(int serverSceneHandle)
// Get the scene currently being synchronized
SceneBeingSynchronized = ScenesLoaded.ContainsKey(clientSceneHandle) ? ScenesLoaded[clientSceneHandle] : new Scene();

// If the scene was not found (invalid) or was not loaded then throw an exception
if (!SceneBeingSynchronized.IsValid() || !SceneBeingSynchronized.isLoaded)
{
throw new Exception($"[{nameof(NetworkSceneManager)}- {nameof(ScenesLoaded)}] Could not find the appropriate scene to set as being synchronized!");
// Let's go ahead and use the currently active scene under the scenario where a NetworkObject is determined to exist in a scene that the NetworkSceneManager is not aware of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in the older code, we halted and caught fire if 1) the scene being sync'd was invalid or 2) it was not loaded.

Case 1) Seems like we want to still halt and catch fire for situation 1). Changing to some other scene would seem to mask a problem that needs to be dealt with. I mean, why would we think we would want to pop back to the active scene if the scene we're going to is invalid.

Case 2) same kind of thing; if the scene is valid but not loaded yet, perhaps there is another handling - I'm just not sure why we'd just pick whatever scene happens to be loaded now

Copy link
Collaborator Author

@NoelStephensUnity NoelStephensUnity Aug 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is specific to in-scene placed NetworkObjects during the client side deserialization phase for:

  • Newly joined client synchronization where all scenes are already loaded and the client is just running through the in-scene placed NetworkObjects (deserialization) to be instantiated.
  • A level is loaded and the client is synchronizing in-scene placed NetworkObjects.

The only time the offending method (SetTheSceneBeingSynchronized) is truly pertinent is when you have the same additive scene loaded multiple times and there are in-scene NetworkObjects in each instance that need to be synchronized. We use scene handlers to distinguish between "duplicate in-scene NetworkObjects" in order to "find the right NetworkObject" so we can apply the appropriate NetworkObjectId to that specific instance.
For all other cases, the GlobalObjectIdHash should be unique and will not matter really which scene is the current scene being synchronized.
What scenario would cause us to use the currently active scene?
If you set NetworkManager.DontDestroy to false and then have a NetworkObject that moves itself into the DontDestroyOnLoad scene when it is instantiated, then when a client is trying to "synchronize" to this specific scenario the first NetworkObject the client is trying to deserialize with this kind of behavior will not have a DontDestroyOnLoad scene (yet) and so it is perfectly fine to return the currently active scene. Upon that first NetworkObject instantiating (after deserialization) the DontDestroyOnLoad scene will exist so any other NetworkObject being deserialized (with the same behavior) on the client from that point forward will set the DontDestroyOnLoad scene to be the "scene being synchronized".

It is one of those chicken or the egg scenarios:
DontDestroyOnLoad is only created when you move a GameObject into that scene using the GameObject.DontDestroyOnLoad method. If nothing has been moved into the DontDestroyOnLoad scene, then it wont exist and isn't something that can be instantiated. I had thought about adding a chunk of extra code that would tell the client to "pre-seed" the DontDestroyOnLoad scene (i.e. create a temporary GameObject before deserialization and then destroy it afterwards) but that seemed a bit over the top. :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I may just need to trust you on this one, not sure I can parse this out / come to understand all the related systems as quickly as we need

Copy link
Collaborator Author

@NoelStephensUnity NoelStephensUnity Aug 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was refactored a bit and might make more sense at this point. But yeah.., there are a lot of different possible scenarios to consider for sure.

SceneBeingSynchronized = SceneManager.GetActiveScene();

// Keeping the warning here in the event we cannot find the scene being synchronized
Debug.LogWarning($"[{nameof(NetworkSceneManager)}- {nameof(ScenesLoaded)}] Could not find the appropriate scene to set as being synchronized! Using the currently active scene.");
}
}
else
{
// This should never happen, but in the event it does...
throw new Exception($"[{nameof(SceneEventData)}- Scene Handle Mismatch] {nameof(serverSceneHandle)} could not be found in {nameof(ServerSceneHandleToClientSceneHandle)}!");
// Most common scenario for DontDestroyOnLoad is when NetworkManager is set to not be destroyed
if (serverSceneHandle == DontDestroyOnLoadScene.handle)
{
SceneBeingSynchronized = m_NetworkManager.gameObject.scene;
return;
}
else
{
// Let's go ahead and use the currently active scene under the scenario where a NetworkObject is determined to exist in a scene that the NetworkSceneManager is not aware of
// or the NetworkObject has yet to be moved to that specific scene (i.e. no DontDestroyOnLoad scene exists yet).
SceneBeingSynchronized = SceneManager.GetActiveScene();

// This could be the scenario where NetworkManager.DontDestroy is false and we are creating the first NetworkObject (client side) to be in the DontDestroyOnLoad scene
// Otherwise, this is some other specific scenario that we might not be handling currently.
Debug.LogWarning($"[{nameof(SceneEventData)}- Scene Handle Mismatch] {nameof(serverSceneHandle)} could not be found in {nameof(ServerSceneHandleToClientSceneHandle)}. Using the currently active scene.");
}
}
}

Expand Down Expand Up @@ -1148,6 +1198,8 @@ private void HandleClientSceneEvent(Stream stream)
}
else
{
// Include anything in the DDOL scene
PopulateScenePlacedObjects(DontDestroyOnLoadScene, false);
// Synchronize the NetworkObjects for this scene
SceneEventData.SynchronizeSceneNetworkObjects(m_NetworkManager);

Expand Down Expand Up @@ -1331,7 +1383,7 @@ private void MoveObjectsToDontDestroyOnLoad()
sobj.gameObject.transform.parent = null;
}

if (!sobj.DestroyWithScene)
if (!sobj.DestroyWithScene || (sobj.IsSceneObject != null && sobj.IsSceneObject.Value && sobj.gameObject.scene == DontDestroyOnLoadScene))
{
UnityEngine.Object.DontDestroyOnLoad(sobj.gameObject);
}
Expand Down Expand Up @@ -1408,6 +1460,11 @@ private void MoveObjectsToScene(Scene scene)
sobj.gameObject.transform.parent = null;
}

if (sobj.gameObject.scene == DontDestroyOnLoadScene && (sobj.IsSceneObject == null || sobj.IsSceneObject.Value))
{
continue;
}

SceneManager.MoveGameObjectToScene(sobj.gameObject, scene);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ internal void ServerDestroySpawnedSceneObjects()

foreach (var sobj in spawnedObjects)
{
if (sobj.IsSceneObject != null && sobj.IsSceneObject.Value)
if (sobj.IsSceneObject != null && sobj.IsSceneObject.Value && sobj.DestroyWithScene && sobj.gameObject.scene != NetworkManager.SceneManager.DontDestroyOnLoadScene)
{
SpawnedObjectsList.Remove(sobj);
UnityEngine.Object.Destroy(sobj.gameObject);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 39a16938ffb5cd846a9f6df7a686a9c4, type: 3}
m_Name: DestroyNetworkManager_DontDestroyOnLoadSceneReference
m_EditorClassIdentifier:
SceneToReference: {fileID: 102900000, guid: 7dc1ab12373402546befc7d54e447258, type: 3}
m_IncludedScenes: []
m_DisplayName: DestroyNetworkManager DontDestroyOnLoad Test
m_ReferencedScenes:
- DontDestroyOnLoad_DestroyNetworkManagerTest

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 39a16938ffb5cd846a9f6df7a686a9c4, type: 3}
m_Name: DontDestroyOnLoadSceneReference
m_EditorClassIdentifier:
SceneToReference: {fileID: 102900000, guid: ff98b91da4ee7ff44bc3aa8a57ad5c12, type: 3}
m_IncludedScenes: []
m_DisplayName: DontDestroyOnLoad Test
m_ReferencedScenes:
- DontDestroyOnLoadTest

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions testproject/Assets/Tests/Manual/DontDestroyOnLoad.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using UnityEngine;
using Unity.Netcode;

namespace TestProject.ManualTests
{
/// <summary>
/// Spawns a single NetworkPrefab
/// </summary>
public class DontDestroyOnLoadSpawnHandler : NetworkBehaviour
{
public GameObject NetworkPrefabToCreate;

public override void OnNetworkSpawn()
{
if (NetworkPrefabToCreate != null && IsServer)
{
var newGameObject = Instantiate(NetworkPrefabToCreate);
var networkObject = newGameObject.GetComponent<NetworkObject>();
networkObject.Spawn();
}

base.OnNetworkSpawn();
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading