Skip to content

fix: client reconnect using NetworkSceneTable state [MTT-3363] #1886

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
26c8099
fix MTT-3285
NoelStephensUnity Apr 13, 2022
ed814be
update
NoelStephensUnity Apr 13, 2022
349bca1
fix
NoelStephensUnity Apr 14, 2022
5dd35d9
test - manual
NoelStephensUnity Apr 14, 2022
8cde95f
style
NoelStephensUnity Apr 14, 2022
473044d
Merge branch 'develop' into fix/validatescenebeforeloading-aborts-syn…
NoelStephensUnity Apr 14, 2022
2c7fa1d
test
NoelStephensUnity Apr 14, 2022
bd26ba2
fix
NoelStephensUnity Apr 14, 2022
5c7d491
style
NoelStephensUnity Apr 14, 2022
4c196d1
update
NoelStephensUnity Apr 14, 2022
f82f2b8
fix
NoelStephensUnity Apr 14, 2022
f10795e
style
NoelStephensUnity Apr 15, 2022
525a650
test helpers update
NoelStephensUnity Apr 15, 2022
5479511
test
NoelStephensUnity Apr 15, 2022
05cfa7b
fix
NoelStephensUnity Apr 15, 2022
0d5e4b9
style
NoelStephensUnity Apr 15, 2022
826deca
style
NoelStephensUnity Apr 15, 2022
7bb4e8d
fix
NoelStephensUnity Apr 15, 2022
f5b6460
fix metrics tests
NoelStephensUnity Apr 16, 2022
5c3b728
test
NoelStephensUnity Apr 16, 2022
857dc6a
test
NoelStephensUnity Apr 16, 2022
b78e49a
fix metric test
NoelStephensUnity Apr 16, 2022
3e0bf90
style
NoelStephensUnity Apr 16, 2022
c057ce1
style
NoelStephensUnity Apr 17, 2022
e296984
style
NoelStephensUnity Apr 18, 2022
0f778af
fix
NoelStephensUnity Apr 18, 2022
7f693c0
test manual
NoelStephensUnity Apr 19, 2022
d8af783
update
NoelStephensUnity Apr 19, 2022
6fd99ff
Merge branch 'develop' into fix/client-reconnect-using-networktablestate
NoelStephensUnity Apr 19, 2022
0648e53
Update CHANGELOG.md
NoelStephensUnity Apr 20, 2022
6828005
Merge branch 'develop' into fix/client-reconnect-using-networktablestate
NoelStephensUnity Apr 20, 2022
f6ff7b4
style
NoelStephensUnity Apr 20, 2022
5d254ae
Merge branch 'develop' into fix/client-reconnect-using-networktablestate
NoelStephensUnity Apr 20, 2022
9a6963b
Merge branch 'develop' into fix/client-reconnect-using-networktablestate
NoelStephensUnity Apr 20, 2022
689a6a8
style
NoelStephensUnity Apr 20, 2022
e74dfb3
Merge branch 'develop' into fix/client-reconnect-using-networktablestate
NoelStephensUnity Apr 21, 2022
ca4b37b
Merge branch 'develop' into fix/client-reconnect-using-networktablestate
NoelStephensUnity Apr 21, 2022
b1603cc
Merge branch 'develop' into fix/client-reconnect-using-networktablestate
NoelStephensUnity Apr 21, 2022
0157f16
style
NoelStephensUnity Apr 21, 2022
713d8e8
fix - unload-reload same scene during disconnect period
NoelStephensUnity Apr 21, 2022
048a1b3
test - additional scene debug information
NoelStephensUnity Apr 21, 2022
837925e
test - added NetworkRigidbody to RandomMoverObject prefab
NoelStephensUnity Apr 21, 2022
0c2c386
update
NoelStephensUnity Apr 21, 2022
fa672e0
update
NoelStephensUnity Apr 21, 2022
a903128
test fix
NoelStephensUnity Apr 21, 2022
1cf0809
Merge branch 'develop' into fix/client-reconnect-using-networktablestate
NoelStephensUnity Apr 21, 2022
671ab27
style
NoelStephensUnity Apr 22, 2022
71232a2
Merge branch 'develop' into fix/client-reconnect-using-networktablestate
NoelStephensUnity Apr 22, 2022
30c76f9
Merge branch 'develop' into fix/client-reconnect-using-networktablestate
NoelStephensUnity Apr 25, 2022
7d59768
Merge branch 'develop' into fix/client-reconnect-using-networktablestate
NoelStephensUnity Apr 25, 2022
29d7bd3
Merge branch 'develop' into fix/client-reconnect-using-networktablestate
NoelStephensUnity Apr 27, 2022
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
1 change: 1 addition & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
## Unreleased

### Added
- Added `NetworkSceneManager.GetNetworkSceneTableState` and `NetworkSceneManager.SetNetworkSceneTableState` to provide users with a way to reconnect to a network session without having to unload and reload already loaded scenes. (#1886)

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,10 +353,15 @@ public IReadOnlyList<ulong> ConnectedClientsIds
public event Action<ulong> OnClientDisconnectCallback = null;

/// <summary>
/// The callback to invoke once the server is ready
/// The callback to invoke once the server has been started
/// </summary>
public event Action OnServerStarted = null;

/// <summary>
/// The callback to invoke once the client has been started
/// </summary>
public event Action OnClientStarted = null;

/// <summary>
/// Delegate type called when connection has been approved. This only has to be set on the server.
/// </summary>
Expand Down Expand Up @@ -929,6 +934,8 @@ public bool StartClient()
IsClient = true;
IsListening = true;

OnClientStarted?.Invoke();

return true;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System;
using System.Linq;
using System.Runtime.CompilerServices;
using UnityEngine;
using UnityEngine.SceneManagement;

Expand Down Expand Up @@ -376,6 +377,53 @@ public AsyncOperation UnloadSceneAsync(Scene scene, ISceneManagerHandler.SceneEv
/// </summary>
internal Dictionary<int, int> ServerSceneHandleToClientSceneHandle = new Dictionary<int, int>();

internal Dictionary<int, Scene> NetworkSceneTableState = new Dictionary<int, Scene>();
private bool m_AutoFlushRemainingScenes;

/// <summary>
/// Returns the current NetworkSceneTable state for the current network session.
/// Call this to save a client's NetworkSceneTable state when disconnected in order to be able to
/// reconnect without having to unload the currently loaded scenes on the client side.
/// <see cref="NetworkManager.OnClientStarted"/> event.
/// <see cref="SetNetworkSceneTableState(Dictionary{int, Scene}, bool)"/>
/// </summary>
/// <returns>NetworkSceneTable state</returns>
public Dictionary<int, Scene> GetNetworkSceneTableState()
{
var networkSceneTable = new Dictionary<int, Scene>();
foreach (var lookupEntry in ServerSceneHandleToClientSceneHandle)
{
if (ScenesLoaded.ContainsKey(lookupEntry.Value))
{
networkSceneTable.Add(lookupEntry.Key, ScenesLoaded[lookupEntry.Value]);
}
}
return networkSceneTable;
}

/// <summary>
/// Sets the current NetworkSceneTable state to be used when reconnecting to a previous network session.
/// This should be called during <see cref="NetworkManager.OnClientStarted"/> before a client
/// synchronizes in order to bypass reloading of already loaded scenes.
/// Note: The NetworkSceneTable state is server authoritative and so clients can only apply
/// a networkSceneTableState to their <see cref="NetworkSceneManager"/> instance.
///
/// Note: This only applies to the initial client-server synchronization process. Setting it after the
/// client is already connected and synchronized will have no impact on the client's current NetworkSceneTable
/// state as by that time the server is dictating the state through network scene events.
/// </summary>
/// <param name="networkSceneTableState">a previously saved network scene table state</param>
/// <param name="autoFlushRemainingScenes">defaults to true, but when false you must handle the unloading
/// of any scenes that were unloaded between the time a client disconnects to the time the client reconnects</param>
public void SetNetworkSceneTableState(Dictionary<int, Scene> networkSceneTableState, bool autoFlushRemainingScenes = true)
{
if (!m_NetworkManager.IsServer)
{
NetworkSceneTableState = new Dictionary<int, Scene>(networkSceneTableState);
m_AutoFlushRemainingScenes = autoFlushRemainingScenes;
}
}

/// <summary>
/// Hash to build index lookup table
/// </summary>
Expand Down Expand Up @@ -648,7 +696,21 @@ internal Scene GetAndAddNewlyLoadedSceneByName(string sceneName)
var sceneLoaded = SceneManager.GetSceneAt(i);
if (sceneLoaded.name == sceneName)
{
if (!ScenesLoaded.ContainsKey(sceneLoaded.handle))
// We only add the scene if:
// - It is not already in the ScenesLoaded list
// - In the event there is a pre-existing NetworkSceneTableState, we don't add scenes that are already
// in the NetworkSceneTableState.
// For example:
// The client disconnects and while the client is disconnected the server unloads additive scene-a and then
// shortly later reloads scene-a additively. The client then reconnects with the previously loaded instance
// of scene-a tied to the previously loaded scene-a on the server. During synchronization, the client is
// provided a new NetworkSceneHandle for the same scene-a which requires the client to reload scene-a.
// At this point, there is still the previously loaded scene-a which the client will unload later
// during resynchronization and the newly loaded scene-a. Both scenes are not in the ScenesLoaded table.
// So, we need to make sure the scene we are inspecting is not in the NetworkSceneTableState list before
// adding it in order to make sure we don't use any previously loaded scenes when looking for only newly
// loaded scenes.
if (!ScenesLoaded.ContainsKey(sceneLoaded.handle) && !NetworkSceneTableState.Values.Contains(sceneLoaded))
{
ScenesLoaded.Add(sceneLoaded.handle, sceneLoaded);
return sceneLoaded;
Expand All @@ -672,18 +734,18 @@ internal Scene GetAndAddNewlyLoadedSceneByName(string sceneName)
/// <param name="serverSceneHandle"></param>
internal void SetTheSceneBeingSynchronized(int serverSceneHandle)
{
var clientSceneHandle = serverSceneHandle;
var networkSceneHandle = serverSceneHandle;
if (ServerSceneHandleToClientSceneHandle.ContainsKey(serverSceneHandle))
{
clientSceneHandle = ServerSceneHandleToClientSceneHandle[serverSceneHandle];
networkSceneHandle = ServerSceneHandleToClientSceneHandle[serverSceneHandle];
// If we were already set, then ignore
if (SceneBeingSynchronized.IsValid() && SceneBeingSynchronized.isLoaded && SceneBeingSynchronized.handle == clientSceneHandle)
if (SceneBeingSynchronized.IsValid() && SceneBeingSynchronized.isLoaded && SceneBeingSynchronized.handle == networkSceneHandle)
{
return;
}

// Get the scene currently being synchronized
SceneBeingSynchronized = ScenesLoaded.ContainsKey(clientSceneHandle) ? ScenesLoaded[clientSceneHandle] : new Scene();
SceneBeingSynchronized = ScenesLoaded.ContainsKey(networkSceneHandle) ? ScenesLoaded[networkSceneHandle] : new Scene();

if (!SceneBeingSynchronized.IsValid() || !SceneBeingSynchronized.isLoaded)
{
Expand Down Expand Up @@ -1447,7 +1509,7 @@ private void OnClientBeginSync(uint sceneEventId)
var loadSceneMode = sceneHash == sceneEventData.SceneHash ? sceneEventData.LoadSceneMode : LoadSceneMode.Additive;

// Store the sceneHandle and hash
sceneEventData.ClientSceneHandle = sceneHandle;
sceneEventData.NetworkSceneHandle = sceneHandle;
sceneEventData.ClientSceneHash = sceneHash;

// If this is the beginning of the synchronization event, then send client a notification that synchronization has begun
Expand Down Expand Up @@ -1476,7 +1538,9 @@ private void OnClientBeginSync(uint sceneEventId)
return;
}

var shouldPassThrough = false;
// If we preloaded a previously saved NetworkSceneTableState and we already
// have loaded this scene, then don't load it again
var shouldPassThrough = NetworkSceneTableState.ContainsKey(sceneHandle);
var sceneLoad = (AsyncOperation)null;

// Check to see if the client already has loaded the scene to be loaded
Expand Down Expand Up @@ -1512,6 +1576,34 @@ private void OnClientBeginSync(uint sceneEventId)
}
}

/// <summary>
/// If this finds the scene in the NetworkSceneTable then it removes the
/// scene from the NetworkSceneTableState and returns it otherwise it
/// returns null and the scene will be loaded.
///
/// Note: If there were any scenes unloaded (server side) between the time
/// the client disconnected and reconnected, then this is part of the process
/// of automatically unloading scenes that are no longer loaded on the server
/// side if m_AutoFlushRemainingScenes is set to true.
/// See Also:
/// <see cref="SetNetworkSceneTableState(Dictionary{int, Scene})"/>
/// </summary>
private Scene TryGetLoadedScene(int networkSceneHandle)
{
if (NetworkSceneTableState.ContainsKey(networkSceneHandle))
{
var scene = NetworkSceneTableState[networkSceneHandle];
NetworkSceneTableState.Remove(networkSceneHandle);
ScenesLoaded.Add(scene.handle, scene);
LogInfo($"Found a scene that was already loaded: [{networkSceneHandle}][{scene.name}][{scene.handle}]", LogLevel.Developer);
return scene;
}
LogInfo($"Did not find an existing instance for NetworkSceneHandle: {networkSceneHandle}", LogLevel.Developer);

// Otherwise return an invalid scene
return new Scene();
}

/// <summary>
/// Once a scene is loaded ( or if it was already loaded) this gets called.
/// This handles all of the in-scene and dynamically spawned NetworkObject synchronization
Expand All @@ -1521,7 +1613,13 @@ private void ClientLoadedSynchronization(uint sceneEventId)
{
var sceneEventData = SceneEventDataStore[sceneEventId];
var sceneName = SceneNameFromHash(sceneEventData.ClientSceneHash);
var nextScene = GetAndAddNewlyLoadedSceneByName(sceneName);
var nextScene = TryGetLoadedScene(sceneEventData.NetworkSceneHandle);
var skipLoading = nextScene.IsValid() && nextScene.isLoaded;
if (!skipLoading)
{

nextScene = GetAndAddNewlyLoadedSceneByName(sceneName);
}

if (!nextScene.isLoaded || !nextScene.IsValid())
{
Expand All @@ -1536,9 +1634,9 @@ private void ClientLoadedSynchronization(uint sceneEventId)
SceneManager.SetActiveScene(nextScene);
}

if (!ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEventData.ClientSceneHandle))
if (!ServerSceneHandleToClientSceneHandle.ContainsKey(sceneEventData.NetworkSceneHandle))
{
ServerSceneHandleToClientSceneHandle.Add(sceneEventData.ClientSceneHandle, nextScene.handle);
ServerSceneHandleToClientSceneHandle.Add(sceneEventData.NetworkSceneHandle, nextScene.handle);
}
else
{
Expand Down Expand Up @@ -1646,6 +1744,23 @@ private void HandleClientSceneEvent(uint sceneEventId)
ClientId = NetworkManager.ServerClientId, // Server sent this to client
});

// Client's will always receive a ReSynchronize event even if there is nothing to resynchronize.
// This assures that any scenes that are no longer loaded (i.e. remaining NetworkSceneTableState entries)
// are unloaded on the client side when m_AutoFlushRemainingScenes is set.
if (m_AutoFlushRemainingScenes)
{
foreach (var networkStateEntry in NetworkSceneTableState)
{
var scene = networkStateEntry.Value;
if (scene.handle != m_NetworkManager.gameObject.scene.handle && scene.IsValid() && scene.isLoaded)
{
LogInfo($"Scene {scene.name} with the handle [{scene.handle}] is being flushed.", LogLevel.Developer);
SceneManagerHandler.UnloadSceneAsync(networkStateEntry.Value, new ISceneManagerHandler.SceneEventAction() { EventAction = new Action<uint>((c) => { }), SceneEventId = sceneEventId });
}
}
}

NetworkSceneTableState.Clear();
EndSceneEvent(sceneEventId);
break;
}
Expand Down Expand Up @@ -1751,8 +1866,8 @@ private void HandleServerSceneEvent(uint sceneEventId, ulong clientId)
m_NetworkManager.InvokeOnClientConnectedCallback(clientId);

// Check to see if the client needs to resynchronize and before sending the message make sure the client is still connected to avoid
// a potential crash within the MessageSystem (i.e. sending to a client that no longer exists)
if (sceneEventData.ClientNeedsReSynchronization() && !DisableReSynchronization && m_NetworkManager.ConnectedClients.ContainsKey(clientId))
// a potential crash within the MessageSystem (i.e. sending to a client that no longer exists).
if (!DisableReSynchronization && m_NetworkManager.ConnectedClients.ContainsKey(clientId))
{
sceneEventData.SceneEventType = SceneEventType.ReSynchronize;
SendSceneEventData(sceneEventId, new ulong[] { clientId });
Expand Down Expand Up @@ -1864,8 +1979,8 @@ internal void PopulateScenePlacedObjects(Scene sceneToFilterBy, bool clearSceneP
var globalObjectIdHash = networkObjectInstance.GlobalObjectIdHash;
var sceneHandle = networkObjectInstance.gameObject.scene.handle;
// We check to make sure the NetworkManager instance is the same one to be "NetcodeIntegrationTestHelpers" compatible and filter the list on a per scene basis (for additive scenes)
if (networkObjectInstance.IsSceneObject != false && networkObjectInstance.NetworkManager == m_NetworkManager && networkObjectInstance.gameObject.scene == sceneToFilterBy &&
sceneHandle == sceneToFilterBy.handle)
if (networkObjectInstance.IsSceneObject != false && networkObjectInstance.NetworkManager == m_NetworkManager &&
networkObjectInstance.gameObject.scene == sceneToFilterBy && sceneHandle == sceneToFilterBy.handle)
{
if (!ScenePlacedObjects.ContainsKey(globalObjectIdHash))
{
Expand Down Expand Up @@ -1913,5 +2028,14 @@ internal void MoveObjectsFromDontDestroyOnLoadToScene(Scene scene)
}
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void LogInfo(string msg, LogLevel logLevel)
{
if (m_NetworkManager.LogLevel == LogLevel.Developer)
{
NetworkLog.LogInfo(msg);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ internal class SceneEventData : IDisposable

// Used by the client during synchronization
internal uint ClientSceneHash;
internal int ClientSceneHandle;
internal int NetworkSceneHandle;

/// Only used for <see cref="SceneEventType.Synchronize"/> scene events, this assures permissions when writing
/// NetworkVariable information. If that process changes, then we need to update this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,13 @@ private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong
SpawnedObjects.Add(networkObject.NetworkObjectId, networkObject);
SpawnedObjectsList.Add(networkObject);

// For integration testing, this makes sure that the appropriate NetworkManager is assigned to
// the NetworkObject since it uses the NetworkManager.Singleton when not set
if (networkObject.NetworkManagerOwner != NetworkManager)
{
networkObject.NetworkManagerOwner = NetworkManager;
}

if (NetworkManager.IsServer)
{
if (playerObject)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,11 @@ protected void CreateServerAndClients(int numberOfClients)
// Set the player prefab for the server and clients
m_ServerNetworkManager.NetworkConfig.PlayerPrefab = m_PlayerPrefab;

// Renumber client NetworkManagers to reflect the client id labels
var count = 0;
foreach (var client in m_ClientNetworkManagers)
{
client.name = $"NetworkManager - Client - {++count}";
client.NetworkConfig.PlayerPrefab = m_PlayerPrefab;
}

Expand Down Expand Up @@ -490,6 +493,16 @@ protected virtual IEnumerator OnTearDown()
yield return null;
}

/// <summary>
/// Note: For <see cref="NetworkManagerInstatiationMode.PerTest"/> mode
/// Called after <see cref="ShutdownAndCleanUp"/>, this is good for post
/// shutdown clean up tasks.
/// </summary>
protected virtual IEnumerator OnPostTearDown()
{
yield return null;
}

[UnityTearDown]
public IEnumerator TearDown()
{
Expand All @@ -500,7 +513,7 @@ public IEnumerator TearDown()
{
ShutdownAndCleanUp();
}

yield return OnPostTearDown();
VerboseDebug($"Exiting {nameof(TearDown)}");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,19 @@ public static void StopOneClient(NetworkManager clientToStop)
NetworkManagerInstances.Remove(clientToStop);
}

/// <summary>
/// Starts one single client and makes sure to register the required hooks and handlers
/// </summary>
/// <param name="clientToStart"></param>
public static void StartOneClient(NetworkManager clientToStart)
{
clientToStart.StartClient();
s_Hooks[clientToStart] = new MultiInstanceHooks();
clientToStart.MessagingSystem.Hook(s_Hooks[clientToStart]);
// if set, then invoke this for the client
RegisterHandlers(clientToStart);
}

/// <summary>
/// Should always be invoked when finished with a single unit test
/// (i.e. during TearDown)
Expand Down Expand Up @@ -393,13 +406,7 @@ public static bool Start(bool host, NetworkManager server, NetworkManager[] clie

for (int i = 0; i < clients.Length; i++)
{
clients[i].StartClient();
hooks = new MultiInstanceHooks();
clients[i].MessagingSystem.Hook(hooks);
s_Hooks[clients[i]] = hooks;

// if set, then invoke this for the client
RegisterHandlers(clients[i]);
StartOneClient(clients[i]);
}

return true;
Expand Down
Loading