From 85c1f968f8b4a84b65f8f3011c04b1f90c7fddef Mon Sep 17 00:00:00 2001 From: Felix Johannes Thiel Date: Tue, 4 Jul 2023 10:34:56 +0100 Subject: [PATCH 1/2] Adding an automatic reconnection process to RoomClient for recovery after the connection to Nexus is lost. On connection loss, the room client instructs the scene to drop all connections and periodically attempts at rejoining the room. --- .../Assets/Runtime/Messaging/NetworkScene.cs | 20 ++++ Unity/Assets/Runtime/Rooms/RoomClient.cs | 101 +++++++++++++++++- 2 files changed, 120 insertions(+), 1 deletion(-) diff --git a/Unity/Assets/Runtime/Messaging/NetworkScene.cs b/Unity/Assets/Runtime/Messaging/NetworkScene.cs index 8ba7f3d75..77ebd880d 100644 --- a/Unity/Assets/Runtime/Messaging/NetworkScene.cs +++ b/Unity/Assets/Runtime/Messaging/NetworkScene.cs @@ -246,6 +246,26 @@ public void AddConnection(INetworkConnection connection) connections.Add(connection); } + /// + /// Public method instructing the network scene to drop all current connections and dispose of them. + /// Used to recover from a connection loss to Nexus. + /// + public void ResetConnections() + { + foreach (var c in connections) + { + try + { + c.Dispose(); + } + catch + { + + } + } + connections.Clear(); + } + private void Update() { OnUpdate.Invoke(); diff --git a/Unity/Assets/Runtime/Rooms/RoomClient.cs b/Unity/Assets/Runtime/Rooms/RoomClient.cs index 6fde83595..0f3159c1a 100644 --- a/Unity/Assets/Runtime/Rooms/RoomClient.cs +++ b/Unity/Assets/Runtime/Rooms/RoomClient.cs @@ -1,4 +1,6 @@ -using System; +using Codice.Client.Commands; +using JetBrains.Annotations; +using System; using System.Collections.Generic; using System.Linq; using Ubiq.Dictionaries; @@ -240,6 +242,20 @@ public IEnumerable Peers private float heartbeatSent => Time.realtimeSinceStartup - pingSent; public static float HeartbeatTimeout = 5f; public static float HeartbeatInterval = 1f; + + // Parameters private and public for the reconnection process. + private enum ReconnectionStatus { Off, Reset, Received, Rejoining}; + private ReconnectionStatus reconnectionStatus = ReconnectionStatus.Off; + private float lastResetTime = 0; + private float timeSinceLastReset => Time.realtimeSinceStartup - lastResetTime; + private float lastRejoinTime = 0; + private float timeSinceLastRejoin => Time.realtimeSinceStartup - lastRejoinTime; + private Guid previousRoomGUID = Guid.Empty; + public static bool AttemtReconnecting = true; // Whether the RoomClient attemtps to reconnect to the server on connection loss. + public static float ReconnectTimeout = 5f; // How long a timeout can last until the reconnect procedure is triggered. + public static float ReconnectInterval = 5f; // The intervals in which reconnect attempts are done. + public static float RejoinInterval = 5f; // The intervals in which rejoin attempts are done. + private PeerInterfaceFriend me = new PeerInterfaceFriend(Guid.NewGuid().ToString()); private RoomInterfaceFriend room = new RoomInterfaceFriend(); private NetworkScene scene; @@ -547,6 +563,11 @@ protected void ProcessMessage(ReferenceCountedSceneGraphMessage message) case "Ping": { pingReceived = Time.realtimeSinceStartup; + + // Set flag for first ping after connection loss + if (reconnectionStatus == ReconnectionStatus.Reset) + reconnectionStatus = ReconnectionStatus.Received; + PlayerNotifications.Delete(ref notification); var response = JsonUtility.FromJson(container.args); OnPingResponse(response); @@ -635,6 +656,38 @@ public void Connect(ConnectionDefinition connection) scene.AddConnection(Connections.Resolve(connection)); } + /// + /// Method to reset all current connections and reconnect to the ones defined by the user in the Unity UI. + /// + public void ResetAndReconnect() + { + ResetAndReconnect(servers); + } + + /// + /// Method to reset all current connections and reconnect to the ones defined in the connection definition passed as argument. + /// + /// The connection definition that will be connected after the reset. + public void ResetAndReconnect(ConnectionDefinition[] connectionDefinitions) + { + // Drop all connections + scene.ResetConnections(); + + // Reconnect all connections + foreach (var item in connectionDefinitions) + { + try + { + Connect(item); + } + catch(Exception e) + { + Debug.LogError(e.ToString()); + } + } + } + + private void Update() { actions.ForEach(a => a()); @@ -679,6 +732,52 @@ private void Update() notification = PlayerNotifications.Show(new TimeoutNotification(this)); } } + + // Reconnection behaviour + // Test if dynamic reconnection is enabled + if(AttemtReconnecting) + { + // If enabled, check for current reconnection status + switch (reconnectionStatus) + { + // Reconnection off: If reconnect timeout is exceeded, old room GUID is stored, reset initiated, and next state is initiaded. + case ReconnectionStatus.Off: + if(heartbeatReceived > ReconnectTimeout) + { + previousRoomGUID = new Guid(room.UUID); + ResetAndReconnect(); + lastResetTime = Time.realtimeSinceStartup; + reconnectionStatus = ReconnectionStatus.Reset; + } + break; + // Reconnection Reset: Connection is reset. While no response has been received, reset connection again in regular intervals. + case ReconnectionStatus.Reset: + if (timeSinceLastReset > ReconnectInterval) + { + ResetAndReconnect(); + lastResetTime = Time.realtimeSinceStartup; + } + break; + // Reconnection Received: A response by Nexus has received. Attempt at rejoining room. + case ReconnectionStatus.Received: + Join(previousRoomGUID); + lastRejoinTime = Time.realtimeSinceStartup; + reconnectionStatus = ReconnectionStatus.Rejoining; + break; + // Reconnection Rejoining: Re-attempt rejoining room at regular intervals until succesful. + case ReconnectionStatus.Rejoining: + if(room.UUID == previousRoomGUID.ToString()) + { + reconnectionStatus = ReconnectionStatus.Off; + } + else if(timeSinceLastRejoin > RejoinInterval) + { + Join(previousRoomGUID); + lastRejoinTime = Time.realtimeSinceStartup; + } + break; + } + } } /// From 2d9e6d5ef916d6149318acecac661f65855b55e6 Mon Sep 17 00:00:00 2001 From: bnco <33021110+bnco-dev@users.noreply.github.com> Date: Tue, 25 Jul 2023 11:14:07 +0100 Subject: [PATCH 2/2] Add scene reload, editor gui and user notification for reconnect functionality --- Unity/Assets/Editor/Rooms/RoomClientEditor.cs | 2 + .../Assets/Runtime/Messaging/NetworkScene.cs | 15 +-- Unity/Assets/Runtime/Rooms/RoomClient.cs | 118 ++++++++---------- Unity/Assets/Samples/Start Here.unity | 7 ++ .../_Common/Prefabs/Network Scene.prefab | 7 ++ 5 files changed, 69 insertions(+), 80 deletions(-) diff --git a/Unity/Assets/Editor/Rooms/RoomClientEditor.cs b/Unity/Assets/Editor/Rooms/RoomClientEditor.cs index d9bfdeb4d..6866f15fa 100644 --- a/Unity/Assets/Editor/Rooms/RoomClientEditor.cs +++ b/Unity/Assets/Editor/Rooms/RoomClientEditor.cs @@ -92,6 +92,8 @@ public override void OnInspectorGUI() EditorGUILayout.HelpBox("Joined Room " + component.Room.UUID, MessageType.Info); } + EditorGUILayout.PropertyField(serializedObject.FindProperty("reconnectBehaviour")); + foldoutRooms = EditorGUILayout.BeginFoldoutHeaderGroup(foldoutRooms, "Available Rooms"); if (foldoutRooms) diff --git a/Unity/Assets/Runtime/Messaging/NetworkScene.cs b/Unity/Assets/Runtime/Messaging/NetworkScene.cs index 77ebd880d..d2dd47aa4 100644 --- a/Unity/Assets/Runtime/Messaging/NetworkScene.cs +++ b/Unity/Assets/Runtime/Messaging/NetworkScene.cs @@ -250,7 +250,7 @@ public void AddConnection(INetworkConnection connection) /// Public method instructing the network scene to drop all current connections and dispose of them. /// Used to recover from a connection loss to Nexus. /// - public void ResetConnections() + public void ClearConnections() { foreach (var c in connections) { @@ -349,18 +349,7 @@ public void SendJson(NetworkId objectid, T message) private void OnDestroy() { - foreach (var c in connections) - { - try - { - c.Dispose(); - } - catch - { - - } - } - connections.Clear(); + ClearConnections(); } } } \ No newline at end of file diff --git a/Unity/Assets/Runtime/Rooms/RoomClient.cs b/Unity/Assets/Runtime/Rooms/RoomClient.cs index 0f3159c1a..99b6ab84b 100644 --- a/Unity/Assets/Runtime/Rooms/RoomClient.cs +++ b/Unity/Assets/Runtime/Rooms/RoomClient.cs @@ -1,14 +1,12 @@ -using Codice.Client.Commands; -using JetBrains.Annotations; -using System; +using System; using System.Collections.Generic; -using System.Linq; using Ubiq.Dictionaries; using Ubiq.Messaging; using Ubiq.Networking; using Ubiq.Rooms.Messages; using Ubiq.XR.Notifications; using UnityEngine; +using UnityEngine.SceneManagement; namespace Ubiq.Rooms { @@ -243,18 +241,16 @@ public IEnumerable Peers public static float HeartbeatTimeout = 5f; public static float HeartbeatInterval = 1f; - // Parameters private and public for the reconnection process. - private enum ReconnectionStatus { Off, Reset, Received, Rejoining}; - private ReconnectionStatus reconnectionStatus = ReconnectionStatus.Off; - private float lastResetTime = 0; - private float timeSinceLastReset => Time.realtimeSinceStartup - lastResetTime; - private float lastRejoinTime = 0; - private float timeSinceLastRejoin => Time.realtimeSinceStartup - lastRejoinTime; - private Guid previousRoomGUID = Guid.Empty; - public static bool AttemtReconnecting = true; // Whether the RoomClient attemtps to reconnect to the server on connection loss. - public static float ReconnectTimeout = 5f; // How long a timeout can last until the reconnect procedure is triggered. - public static float ReconnectInterval = 5f; // The intervals in which reconnect attempts are done. - public static float RejoinInterval = 5f; // The intervals in which rejoin attempts are done. + public enum ReconnectBehaviour + { + None, + Reconnect, + ReconnectAndReloadScenes + } + + public ReconnectBehaviour reconnectBehaviour = ReconnectBehaviour.None; + public static float reconnectTimeout = 10.0f; + private float nextReconnectTimeout = reconnectTimeout; private PeerInterfaceFriend me = new PeerInterfaceFriend(Guid.NewGuid().ToString()); private RoomInterfaceFriend room = new RoomInterfaceFriend(); @@ -278,7 +274,15 @@ public override string Message { get { - return $"No Connection ({ client.heartbeatReceived.ToString("0") } seconds ago)"; + if (client.reconnectBehaviour == ReconnectBehaviour.None) + { + return $"Connection lost ({ client.heartbeatReceived.ToString("0") } seconds ago)"; + } + else + { + var timeToReconnect = Mathf.Max(0,client.nextReconnectTimeout - client.heartbeatReceived); + return $"Connection lost (Next reconnect attempt in { timeToReconnect.ToString("0") } seconds)"; + } } } } @@ -564,11 +568,6 @@ protected void ProcessMessage(ReferenceCountedSceneGraphMessage message) { pingReceived = Time.realtimeSinceStartup; - // Set flag for first ping after connection loss - if (reconnectionStatus == ReconnectionStatus.Reset) - reconnectionStatus = ReconnectionStatus.Received; - - PlayerNotifications.Delete(ref notification); var response = JsonUtility.FromJson(container.args); OnPingResponse(response); } @@ -671,7 +670,7 @@ public void ResetAndReconnect() public void ResetAndReconnect(ConnectionDefinition[] connectionDefinitions) { // Drop all connections - scene.ResetConnections(); + scene.ClearConnections(); // Reconnect all connections foreach (var item in connectionDefinitions) @@ -727,55 +726,19 @@ private void Update() if (heartbeatReceived > HeartbeatTimeout) { + // There's been a long interval between server responses + // We may be disconnected, or there may be network issues + if (notification == null) { notification = PlayerNotifications.Show(new TimeoutNotification(this)); } - } - // Reconnection behaviour - // Test if dynamic reconnection is enabled - if(AttemtReconnecting) - { - // If enabled, check for current reconnection status - switch (reconnectionStatus) + if (heartbeatReceived > nextReconnectTimeout + && reconnectBehaviour != ReconnectBehaviour.None) { - // Reconnection off: If reconnect timeout is exceeded, old room GUID is stored, reset initiated, and next state is initiaded. - case ReconnectionStatus.Off: - if(heartbeatReceived > ReconnectTimeout) - { - previousRoomGUID = new Guid(room.UUID); - ResetAndReconnect(); - lastResetTime = Time.realtimeSinceStartup; - reconnectionStatus = ReconnectionStatus.Reset; - } - break; - // Reconnection Reset: Connection is reset. While no response has been received, reset connection again in regular intervals. - case ReconnectionStatus.Reset: - if (timeSinceLastReset > ReconnectInterval) - { - ResetAndReconnect(); - lastResetTime = Time.realtimeSinceStartup; - } - break; - // Reconnection Received: A response by Nexus has received. Attempt at rejoining room. - case ReconnectionStatus.Received: - Join(previousRoomGUID); - lastRejoinTime = Time.realtimeSinceStartup; - reconnectionStatus = ReconnectionStatus.Rejoining; - break; - // Reconnection Rejoining: Re-attempt rejoining room at regular intervals until succesful. - case ReconnectionStatus.Rejoining: - if(room.UUID == previousRoomGUID.ToString()) - { - reconnectionStatus = ReconnectionStatus.Off; - } - else if(timeSinceLastRejoin > RejoinInterval) - { - Join(previousRoomGUID); - lastRejoinTime = Time.realtimeSinceStartup; - } - break; + ResetAndReconnect(); + nextReconnectTimeout += reconnectTimeout; } } } @@ -857,9 +820,30 @@ public void Ping() private void OnPingResponse(PingResponseArgs args) { + PlayerNotifications.Delete(ref notification); + nextReconnectTimeout = reconnectTimeout; + if(SessionId != args.sessionId && SessionId != null) { - Join(name:"",publish:false); // The RoomClient has re-established connectivity with the RoomServer, but under a different state. So, leave the room and let the user code re-establish any state. + // The RoomClient has re-established connectivity with + // the RoomServer, but under a different state. + if (reconnectBehaviour == ReconnectBehaviour.ReconnectAndReloadScenes) + { + var scenes = new Scene[SceneManager.sceneCount]; + for (int i = 0; i < SceneManager.sceneCount; i++) + { + scenes[i] = SceneManager.GetSceneAt(i); + } + + var first = true; + for (int i = 0; i < scenes.Length; i++) + { + SceneManager.LoadScene(scenes[i].buildIndex,mode: first + ? LoadSceneMode.Single + : LoadSceneMode.Additive); + first = false; + } + } } SessionId = args.sessionId; diff --git a/Unity/Assets/Samples/Start Here.unity b/Unity/Assets/Samples/Start Here.unity index dfd101542..32feff72b 100644 --- a/Unity/Assets/Samples/Start Here.unity +++ b/Unity/Assets/Samples/Start Here.unity @@ -413,6 +413,7 @@ MeshRenderer: m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 + m_StaticShadowCaster: 0 m_MotionVectors: 1 m_LightProbeUsage: 1 m_ReflectionProbeUsage: 1 @@ -461,6 +462,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: -1.214, y: 0.634, z: -0.99} m_LocalScale: {x: 0.25361246, y: 0.25361246, z: 0.25361246} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 0} m_RootOrder: 4 @@ -707,6 +709,11 @@ PrefabInstance: propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} + - target: {fileID: 2642065778884258271, guid: 46c1f5d5a11cf5042886aabd56e7b9d7, + type: 3} + propertyPath: m_Enabled + value: 1 + objectReference: {fileID: 0} - target: {fileID: 2642065778884258271, guid: 46c1f5d5a11cf5042886aabd56e7b9d7, type: 3} propertyPath: servers.Array.size diff --git a/Unity/Assets/Samples/_Common/Prefabs/Network Scene.prefab b/Unity/Assets/Samples/_Common/Prefabs/Network Scene.prefab index c9ad23e8a..b690f6abe 100644 --- a/Unity/Assets/Samples/_Common/Prefabs/Network Scene.prefab +++ b/Unity/Assets/Samples/_Common/Prefabs/Network Scene.prefab @@ -27,6 +27,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 7068762446378101729} m_RootOrder: 4 @@ -72,6 +73,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 7068762446378101729} m_RootOrder: 0 @@ -118,6 +120,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: - {fileID: 7068762446051028576} - {fileID: 7068762446560339092} @@ -160,6 +163,7 @@ MonoBehaviour: m_EditorClassIdentifier: servers: - {fileID: -7849211376014683480, guid: 1c91df7c43c1dbe4fb0fb303e71a2790, type: 2} + reconnectBehaviour: 2 --- !u!114 &7068762446378101731 MonoBehaviour: m_ObjectHideFlags: 0 @@ -217,6 +221,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 7068762446378101729} m_RootOrder: 1 @@ -260,6 +265,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 7068762446378101729} m_RootOrder: 2 @@ -309,6 +315,7 @@ Transform: m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 m_Children: [] m_Father: {fileID: 7068762446378101729} m_RootOrder: 3