diff --git a/.yamato/_triggers.yml b/.yamato/_triggers.yml index 0d982ea67b..d51d6b3940 100644 --- a/.yamato/_triggers.yml +++ b/.yamato/_triggers.yml @@ -75,20 +75,20 @@ pr_code_changes_checks: # Run API validation to early-detect all new APIs that would force us to release new minor version of the package. Note that for this to work the package version in package.json must correspond to "actual package state" which means that it should be higher than last released version - .yamato/vetting-test.yml#vetting_test - # Run package EditMode and Playmode package tests on 6000.5 and an older supported editor (6000.0) - - .yamato/package-tests.yml#package_test_-_ngo_6000.5_mac + # Run package EditMode and Playmode package tests on trunk and an older supported editor (6000.0) + - .yamato/package-tests.yml#package_test_-_ngo_trunk_mac - .yamato/package-tests.yml#package_test_-_ngo_6000.0_win - # Run testproject EditMode and Playmode project tests on 6000.5 and an older supported editor (6000.0) - - .yamato/project-tests.yml#test_testproject_win_6000.5 + # Run testproject EditMode and Playmode project tests on trunk and an older supported editor (6000.0) + - .yamato/project-tests.yml#test_testproject_win_trunk - .yamato/project-tests.yml#test_testproject_mac_6000.0 # Run standalone test. We run it only on Ubuntu since it's the fastest machine, and it was noted that for example distribution on macOS is taking 40m since we switched to Apple Silicon # Coverage on other standalone machines is present in Nightly job so it's enough to not run all of them for PRs # desktop_standalone_test and cmb_service_standalone_test are both reusing desktop_standalone_build dependency so we run those in the same configuration on PRs to reduce waiting time. - # Note that our daily tests will anyway run both test configurations in "minimal supported" and "6000.5" configurations - - .yamato/desktop-standalone-tests.yml#desktop_standalone_test_testproject_ubuntu_il2cpp_6000.5 - - .yamato/cmb-service-standalone-tests.yml#cmb_service_standalone_test_testproject_ubuntu_il2cpp_6000.5 + # Note that our daily tests will anyway run both test configurations in "minimal supported" and "trunk" configurations + - .yamato/desktop-standalone-tests.yml#desktop_standalone_test_testproject_ubuntu_il2cpp_trunk + - .yamato/cmb-service-standalone-tests.yml#cmb_service_standalone_test_testproject_ubuntu_il2cpp_trunk triggers: expression: |- (pull_request.comment eq "ngo" OR diff --git a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs index fbfb780028..f146101441 100644 --- a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs @@ -972,27 +972,39 @@ internal void HandleConnectionApproval(ulong ownerClientId, bool createPlayerObj } // Server-side spawning (only if there is a prefab hash or player prefab provided) - if (!NetworkManager.DistributedAuthorityMode && createPlayerObject && (playerPrefabHash.HasValue || NetworkManager.NetworkConfig.PlayerPrefab != null)) + var idHashToSpawn = playerPrefabHash ?? NetworkManager.NetworkConfig.PlayerPrefab?.GetComponent()?.GlobalObjectIdHash; + if (!NetworkManager.DistributedAuthorityMode && createPlayerObject && idHashToSpawn.HasValue) { - var playerObject = playerPrefabHash.HasValue ? NetworkManager.SpawnManager.GetNetworkObjectToSpawn(playerPrefabHash.Value, ownerClientId, playerPosition, playerRotation) - : NetworkManager.SpawnManager.GetNetworkObjectToSpawn(NetworkManager.NetworkConfig.PlayerPrefab.GetComponent().GlobalObjectIdHash, ownerClientId, playerPosition, playerRotation); + var playerObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(idHashToSpawn.Value, ownerClientId, playerPosition, playerRotation); if (playerObject == null) { - Debug.LogError($"[{nameof(NetworkObject)}] Player prefab is null! Cannot spawn player object!"); + if (NetworkManager.LogLevel <= LogLevel.Error) + { + NetworkLog.LogError($"[{nameof(NetworkObject)}] Player prefab is null! Cannot spawn player object!"); + } } else { // Spawn the player NetworkObject locally - NetworkManager.SpawnManager.AuthorityLocalSpawn( + if (NetworkManager.SpawnManager.AuthorityLocalSpawn( playerObject, NetworkManager.SpawnManager.GetNetworkObjectId(), sceneObject: false, playerObject: true, ownerClientId, - destroyWithScene: false); + destroyWithScene: false)) + { + client.AssignPlayerObject(ref playerObject); + } + else + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + NetworkLog.LogError($"[{nameof(NetworkObject)}] Player prefab failed to spawn!"); + } + } - client.AssignPlayerObject(ref playerObject); } } @@ -1122,8 +1134,25 @@ internal void CreateAndSpawnPlayer(ulong ownerId) } return; } - var globalObjectIdHash = playerPrefab.GetComponent().GlobalObjectIdHash; - var networkObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(globalObjectIdHash, ownerId, playerPrefab.transform.position, playerPrefab.transform.rotation); + var prefabObject = playerPrefab.GetComponent(); + if (prefabObject == null) + { + if (NetworkManager.LogLevel <= LogLevel.Normal) + { + NetworkLog.LogError("Failed to fetch valid player prefab. Ensure PlayerPrefab that is set in NetcodeConfig contains a NetworkObject component."); + } + return; + } + var networkObject = NetworkManager.SpawnManager.GetNetworkObjectToSpawn(prefabObject.GlobalObjectIdHash, ownerId, playerPrefab.transform.position, playerPrefab.transform.rotation); + if (networkObject == null) + { + if (NetworkManager.LogLevel <= LogLevel.Normal) + { + NetworkLog.LogError("Failed to spawn player prefab!"); + } + return; + } + networkObject.IsSceneObject = false; networkObject.NetworkManagerOwner = NetworkManager; networkObject.SpawnAsPlayerObject(ownerId, networkObject.DestroyWithScene); diff --git a/com.unity.netcode.gameobjects/Runtime/Core/FindObjects.cs b/com.unity.netcode.gameobjects/Runtime/Core/FindObjects.cs index 849afe1b88..a283cabe52 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/FindObjects.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/FindObjects.cs @@ -21,17 +21,19 @@ internal static class FindObjects /// /// When true, inactive objects will be included. /// When true, the array returned will be sorted by identifier. - /// Resulst as an of type T + /// Results as an of type T [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T[] ByType(bool includeInactive = false, bool orderByIdentifier = false) where T : Object { var inactive = includeInactive ? UnityEngine.FindObjectsInactive.Include : UnityEngine.FindObjectsInactive.Exclude; #if NGO_FINDOBJECTS_NOSORTING var results = Object.FindObjectsByType(inactive); +#if !NGO_FINDOBJECTS_UNORDERED_IDS if (orderByIdentifier) { Array.Sort(results, (a, b) => a.GetEntityId().CompareTo(b.GetEntityId())); } +#endif #else var results = Object.FindObjectsByType(inactive, orderByIdentifier ? UnityEngine.FindObjectsSortMode.InstanceID : UnityEngine.FindObjectsSortMode.None); #endif diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index bd518b3048..a0badd21de 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -1807,7 +1807,15 @@ internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool pla } } - NetworkManagerOwner.SpawnManager.AuthorityLocalSpawn(this, NetworkManagerOwner.SpawnManager.GetNetworkObjectId(), IsSceneObject.HasValue && IsSceneObject.Value, playerObject, ownerClientId, destroyWithScene); + if (!NetworkManagerOwner.SpawnManager.AuthorityLocalSpawn(this, NetworkManagerOwner.SpawnManager.GetNetworkObjectId(), IsSceneObject.HasValue && IsSceneObject.Value, playerObject, ownerClientId, destroyWithScene)) + { + if (NetworkManagerOwner.LogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"[{name}] Failed to finish spawning!"); + } + ResetOnDespawn(); + return; + } if ((NetworkManagerOwner.DistributedAuthorityMode && NetworkManagerOwner.DAHost) || (!NetworkManagerOwner.DistributedAuthorityMode && NetworkManagerOwner.IsServer)) { @@ -1865,7 +1873,7 @@ public static NetworkObject InstantiateAndSpawn(GameObject networkPrefab, Networ /// /// This invokes . /// - /// The local instance of the NetworkManager connected to an session in progress. + /// The local instance of the NetworkManager connected to a session in progress. /// The owner of the instance (defaults to server). /// Whether the instance will be destroyed when the scene it is located within is unloaded (default is false). /// Whether the instance is a player object or not (default is false). @@ -2161,6 +2169,11 @@ internal void SetNetworkParenting(ulong? latestParent, bool worldPositionStays) m_CachedWorldPositionStays = worldPositionStays; } + internal void ClearNetworkParenting() + { + m_LatestParent = null; + } + /// /// Set the parent of the NetworkObject transform. /// @@ -3304,90 +3317,35 @@ internal static NetworkObject Deserialize(in SerializedObject serializedObject, { var endOfSynchronizationData = reader.Position + serializedObject.SynchronizationDataSize; - byte[] instantiationData = null; - if (serializedObject.HasInstantiationData) - { - reader.ReadValueSafe(out instantiationData); - } - - - // Attempt to create a local NetworkObject - var networkObject = networkManager.SpawnManager.CreateLocalNetworkObject(serializedObject, instantiationData); - - - if (networkObject == null) - { - // Log the error that the NetworkObject failed to construct - if (networkManager.LogLevel <= LogLevel.Normal) - { - NetworkLog.LogError($"Failed to spawn {nameof(NetworkObject)} for Hash {serializedObject.Hash}."); - } - - try - { - // If we failed to load this NetworkObject, then skip past the Network Variable and (if any) synchronization data - reader.Seek(endOfSynchronizationData); - } - catch (Exception ex) - { - Debug.LogException(ex); - } + // Do the SpawnManager parts of the object spawn + var succeeded = networkManager.SpawnManager.NonAuthorityLocalSpawn(in serializedObject, out var networkObject, reader, serializedObject.DestroyWithScene); - // We have nothing left to do here. - return null; - } - - networkObject.NetworkManagerOwner = networkManager; - - // This will get set again when the NetworkObject is spawned locally, but we set it here ahead of spawning - // in order to be able to determine which NetworkVariables the client will be allowed to read. - networkObject.OwnerClientId = serializedObject.OwnerClientId; + // Process any deferred messages once the object is 100% finished spawning + // Ensure this is done whether the spawn succeeds or fails + networkManager.DeferredMessageManager.ProcessTriggers(IDeferredNetworkMessageManager.TriggerType.OnSpawn, networkObject.NetworkObjectId); - // Special Case: Invoke NetworkBehaviour.OnPreSpawn methods here before SynchronizeNetworkBehaviours - networkObject.InvokeBehaviourNetworkPreSpawn(); - - // Process the remaining synchronization data from the buffer - try + // If the SpawnManager spawn doesn't succeed, be sure to clean up + if (!succeeded) { - // Synchronize NetworkBehaviours - var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); - networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId); - // Ensure that the buffer is completely reset if (reader.Position != endOfSynchronizationData) { - Debug.LogWarning($"[Size mismatch] Expected: {endOfSynchronizationData} Currently At: {reader.Position}!"); + if (networkManager.LogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"[{networkObject.name}][Deserialize][Size mismatch] Expected: {endOfSynchronizationData} Currently At: {reader.Position}!"); + } reader.Seek(endOfSynchronizationData); } - } - catch - { - reader.Seek(endOfSynchronizationData); - } - // If we are an in-scene placed NetworkObject and we originally had a parent but when synchronized we are - // being told we do not have a parent, then we want to clear the latest parent so it is not automatically - // "re-parented" to the original parent. This can happen if not unloading the scene and the parenting of - // the in-scene placed Networkobject changes several times over different sessions. - if (serializedObject.IsSceneObject && !serializedObject.HasParent && networkObject.m_LatestParent.HasValue) - { - networkObject.m_LatestParent = null; - } - - // Spawn the NetworkObject - if (networkObject.IsSpawned) - { - if (NetworkManager.Singleton.LogLevel <= LogLevel.Error) + // If the networkObject was created but the spawn failed, the created object needs to be destroyed + if (networkObject != null) { - NetworkLog.LogErrorServer($"[{networkObject.name}] Object-{networkObject.NetworkObjectId} is already spawned!"); + Destroy(networkObject.gameObject); } + return null; } - // Invoke the non-authority local spawn method - // (It also invokes post spawn and handles processing derferred messages) - networkManager.SpawnManager.NonAuthorityLocalSpawn(networkObject, serializedObject, serializedObject.DestroyWithScene); - if (serializedObject.SyncObservers) { foreach (var observer in serializedObject.Observers) diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/DestroyObjectMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/DestroyObjectMessage.cs index 2550c3015f..9bb6828c7f 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/DestroyObjectMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/DestroyObjectMessage.cs @@ -112,7 +112,7 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int } // Client-Server mode we always defer where in distributed authority mode we only defer if it is not a targeted destroy - if (!networkManager.DistributedAuthorityMode || (networkManager.DistributedAuthorityMode && !IsTargetedDestroy)) + if (!networkManager.DistributedAuthorityMode || !networkManager.IsConnectedClient || (networkManager.DistributedAuthorityMode && !IsTargetedDestroy)) { networkManager.DeferredMessageManager.DeferMessage(IDeferredNetworkMessageManager.TriggerType.OnSpawn, NetworkObjectId, reader, ref context, k_Name); } diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs index bf1ed0d734..387d390c27 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs @@ -657,6 +657,12 @@ internal bool ShouldDeferCreateObject() { return false; } + + if (!NetworkManager.IsConnectedClient) + { + return true; + } + var synchronizeEventDetected = false; var loadingEventDetected = false; foreach (var entry in SceneEventDataStore) @@ -1972,6 +1978,7 @@ internal void SynchronizeNetworkObjects(ulong clientId, bool synchronizingServic sceneEventData.ClientSynchronizationMode = ClientSynchronizationMode; sceneEventData.InitializeForSynch(); sceneEventData.TargetClientId = clientId; + sceneEventData.SenderClientId = NetworkManager.LocalClientId; sceneEventData.LoadSceneMode = ClientSynchronizationMode; var activeScene = SceneManager.GetActiveScene(); sceneEventData.SceneEventType = SceneEventType.Synchronize; @@ -2064,13 +2071,16 @@ internal void SynchronizeNetworkObjects(ulong clientId, bool synchronizingServic // Notify the local server that the client has been sent the synchronize event - OnSceneEvent?.Invoke(new SceneEvent() + if (!synchronizingService) { - SceneEventType = sceneEventData.SceneEventType, - ClientId = clientId - }); + OnSceneEvent?.Invoke(new SceneEvent() + { + SceneEventType = SceneEventType.Synchronize, + ClientId = clientId + }); - OnSynchronize?.Invoke(clientId); + OnSynchronize?.Invoke(clientId); + } EndSceneEvent(sceneEventData.SceneEventId); } @@ -2094,18 +2104,6 @@ private void OnClientBeginSync(uint sceneEventId) sceneEventData.NetworkSceneHandle = sceneHandle; sceneEventData.ClientSceneHash = sceneHash; - // If this is the beginning of the synchronization event, then send client a notification that synchronization has begun - if (sceneHash == sceneEventData.SceneHash) - { - OnSceneEvent?.Invoke(new SceneEvent() - { - SceneEventType = SceneEventType.Synchronize, - ClientId = NetworkManager.LocalClientId, - }); - - OnSynchronize?.Invoke(NetworkManager.LocalClientId); - } - // Always check to see if the scene needs to be validated if (!ValidateSceneBeforeLoading(sceneHash, loadSceneMode)) { @@ -2306,6 +2304,19 @@ private void HandleClientSceneEvent(uint sceneEventId) } case SceneEventType.Synchronize: { + if (sceneEventData.IsStartingSynchronization) + { + sceneEventData.IsStartingSynchronization = false; + + OnSceneEvent?.Invoke(new SceneEvent() + { + SceneEventType = SceneEventType.Synchronize, + ClientId = NetworkManager.LocalClientId, + }); + + OnSynchronize?.Invoke(NetworkManager.LocalClientId); + } + if (!sceneEventData.IsDoneWithSynchronization()) { OnClientBeginSync(sceneEventId); diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs index fd3424a79e..a82ce21a73 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs @@ -196,6 +196,7 @@ internal NetworkSceneHandle GetNextSceneSynchronizationHandle() return SceneHandlesToSynchronize.Dequeue(); } + internal bool IsStartingSynchronization; /// /// Client Side: /// Determines if all scenes have been processed during the synchronization process @@ -782,6 +783,7 @@ internal void Deserialize(FastBufferReader reader) LogArray(reader.ToArray(), 0, reader.Length); } CopySceneSynchronizationData(reader); + IsStartingSynchronization = true; break; } case SceneEventType.SynchronizeComplete: @@ -831,6 +833,7 @@ internal void CopySceneSynchronizationData(FastBufferReader reader) ScenesToSynchronize = new Queue(scenesToSynchronize); SceneHandlesToSynchronize = new Queue(sceneHandlesToSynchronize); + // is not packed! reader.ReadValueSafe(out int sizeToCopy); m_InternalBufferSize = sizeToCopy; @@ -916,9 +919,9 @@ internal void ReadClientReSynchronizationData(FastBufferReader reader) var networkObjectIdToNetworkObject = new Dictionary(); foreach (var networkObject in networkObjects) { - if (!networkObjectIdToNetworkObject.ContainsKey(networkObject.NetworkObjectId)) + if (networkObject.IsSpawned) { - networkObjectIdToNetworkObject.Add(networkObject.NetworkObjectId, networkObject); + networkObjectIdToNetworkObject.TryAdd(networkObject.NetworkObjectId, networkObject); } } @@ -940,7 +943,7 @@ internal void ReadClientReSynchronizationData(FastBufferReader reader) { m_NetworkManager.SpawnManager.SpawnedObjectsList.Remove(networkObject); } - NetworkManager.Singleton.PrefabHandler.HandleNetworkPrefabDestroy(networkObject); + m_NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObject); } else { @@ -975,7 +978,7 @@ internal bool ClientNeedsReSynchronization() } /// - /// Server Side: + /// All clients: /// Determines if the client needs to be re-synchronized if during the deserialization /// process the server finds NetworkObjects that the client still thinks are spawned but /// have since been despawned. diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index bf4eacbc1b..073e71f19d 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using System.Text; using UnityEngine; +using Object = UnityEngine.Object; namespace Unity.Netcode { @@ -884,20 +885,29 @@ internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ow /// conditional position in place of the network prefab's default position /// conditional rotation in place of the network prefab's default rotation /// the instance of the - internal NetworkObject InstantiateNetworkPrefab(GameObject networkPrefab, uint prefabGlobalObjectIdHash, Vector3? position, Quaternion? rotation) + internal NetworkObject InstantiateNetworkPrefab([NotNull] GameObject networkPrefab, uint prefabGlobalObjectIdHash, Vector3? position, Quaternion? rotation) { - var networkObject = UnityEngine.Object.Instantiate(networkPrefab).GetComponent(); + var gameObject = Object.Instantiate(networkPrefab); + var networkObject = gameObject.GetComponent(); + if (networkObject == null) + { + if (NetworkManager.LogLevel <= LogLevel.Error) + { + NetworkLog.LogError($"No {nameof(NetworkObject)} found on NetworkPrefab {networkPrefab.name}!"); + } + Object.Destroy(gameObject); + return null; + } networkObject.transform.SetPositionAndRotation(position ?? networkObject.transform.position, rotation ?? networkObject.transform.rotation); networkObject.PrefabGlobalObjectIdHash = prefabGlobalObjectIdHash; return networkObject; } /// - /// Creates a local NetowrkObject to be spawned. + /// Creates a local NetworkObject to be spawned. /// /// - /// For most cases this is client-side only, with the exception of when the server - /// is spawning a player. + /// For most cases this is client-side only, except when the server is spawning a player. /// internal NetworkObject CreateLocalNetworkObject(NetworkObject.SerializedObject serializedObject, byte[] instantiationData = null) { @@ -923,105 +933,110 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SerializedObject s { NetworkLog.LogError($"{nameof(NetworkPrefab)} hash was not found! In-Scene placed {nameof(NetworkObject)} soft synchronization failure for Hash: {globalObjectIdHash}!"); } + + return null; } // Since this NetworkObject is an in-scene placed NetworkObject, if it is disabled then enable it so // NetworkBehaviours will have their OnNetworkSpawn method invoked - if (networkObject != null && !networkObject.gameObject.activeInHierarchy) + if (!networkObject.gameObject.activeInHierarchy) { networkObject.gameObject.SetActive(true); } } - if (networkObject != null) + if (networkObject == null) { - networkObject.DestroyWithScene = serializedObject.DestroyWithScene; - networkObject.NetworkSceneHandle = serializedObject.NetworkSceneHandle; - networkObject.DontDestroyWithOwner = serializedObject.DontDestroyWithOwner; - networkObject.Ownership = (NetworkObject.OwnershipStatus)serializedObject.OwnershipFlags; - - var nonNetworkObjectParent = false; - // SPECIAL CASE FOR IN-SCENE PLACED: (only when the parent has a NetworkObject) - // This is a special case scenario where a late joining client has joined and loaded one or - // more scenes that contain nested in-scene placed NetworkObject children yet the server's - // synchronization information does not indicate the NetworkObject in question has a parent =or= - // the parent has changed. - // For this we will want to remove the parent before spawning and setting the transform values based - // on several possible scenarios. - if (serializedObject.IsSceneObject && networkObject.transform.parent != null) - { - var parentNetworkObject = networkObject.transform.parent.GetComponent(); - - // special case to handle being parented under a GameObject with no NetworkObject - nonNetworkObjectParent = !parentNetworkObject && serializedObject.HasParent; + return null; + } - // If the in-scene placed NetworkObject has a parent NetworkObject... - if (parentNetworkObject) + networkObject.DestroyWithScene = serializedObject.DestroyWithScene; + networkObject.NetworkSceneHandle = serializedObject.NetworkSceneHandle; + networkObject.DontDestroyWithOwner = serializedObject.DontDestroyWithOwner; + networkObject.Ownership = (NetworkObject.OwnershipStatus)serializedObject.OwnershipFlags; + + var nonNetworkObjectParent = false; + // SPECIAL CASE FOR IN-SCENE PLACED: (only when the parent has a NetworkObject) + // This is a special case scenario where a late joining client has joined and loaded one or + // more scenes that contain nested in-scene placed NetworkObject children yet the server's + // synchronization information does not indicate the NetworkObject in question has a parent =or= + // the parent has changed. + // For this we will want to remove the parent before spawning and setting the transform values based + // on several possible scenarios. + if (serializedObject.IsSceneObject && networkObject.transform.parent != null) + { + var parentNetworkObject = networkObject.transform.parent.GetComponent(); + + // special case to handle being parented under a GameObject with no NetworkObject + nonNetworkObjectParent = !parentNetworkObject && serializedObject.HasParent; + + // If the in-scene placed NetworkObject has a parent NetworkObject... + if (parentNetworkObject) + { + // Then remove the parent only if: + // - The authority says we don't have a parent (but locally we do). + // - The auhtority says we have a parent but either of the two are true: + // -- It isn't the same parent. + // -- It was parented using world position stays. + if (!serializedObject.HasParent || (serializedObject.IsLatestParentSet + && (serializedObject.LatestParent.Value != parentNetworkObject.NetworkObjectId || serializedObject.WorldPositionStays))) { - // Then remove the parent only if: - // - The authority says we don't have a parent (but locally we do). - // - The auhtority says we have a parent but either of the two are true: - // -- It isn't the same parent. - // -- It was parented using world position stays. - if (!serializedObject.HasParent || (serializedObject.IsLatestParentSet - && (serializedObject.LatestParent.Value != parentNetworkObject.NetworkObjectId || serializedObject.WorldPositionStays))) - { - // If parenting without notifications then we are temporarily removing the parent to set the transform - // values before reparenting under the current parent. - networkObject.ApplyNetworkParenting(true, true, enableNotification: !serializedObject.HasParent); - } + // If parenting without notifications then we are temporarily removing the parent to set the transform + // values before reparenting under the current parent. + networkObject.ApplyNetworkParenting(true, true, enableNotification: !serializedObject.HasParent); } } + } - // Set the transform only if the sceneObject includes transform information. - if (serializedObject.HasTransform) + // Set the transform only if the sceneObject includes transform information. + if (serializedObject.HasTransform) + { + // If world position stays is true or we have auto object parent synchronization disabled + // then we want to apply the position and rotation values world space relative + if ((worldPositionStays && !nonNetworkObjectParent) || !networkObject.AutoObjectParentSync) { - // If world position stays is true or we have auto object parent synchronization disabled - // then we want to apply the position and rotation values world space relative - if ((worldPositionStays && !nonNetworkObjectParent) || !networkObject.AutoObjectParentSync) - { - networkObject.transform.SetPositionAndRotation(position, rotation); - } - else - { - networkObject.transform.SetLocalPositionAndRotation(position, rotation); - } - - // SPECIAL CASE: - // Since players are created uniquely we don't apply scale because - // the ConnectionApprovalResponse does not currently provide the - // ability to specify scale. So, we just use the default scale of - // the network prefab used to represent the player. - // Note: not doing this would set the player's scale to zero since - // that is the default value of Vector3. - if (!serializedObject.IsPlayerObject) - { - // Since scale is always applied to local space scale, we do the transform - // space logic during serialization such that it works out whether AutoObjectParentSync - // is enabled or not (see NetworkObject.SceneObject) - networkObject.transform.localScale = scale; - } + networkObject.transform.SetPositionAndRotation(position, rotation); + } + else + { + networkObject.transform.SetLocalPositionAndRotation(position, rotation); } - if (serializedObject.HasParent) + // SPECIAL CASE: + // Since players are created uniquely we don't apply scale because + // the ConnectionApprovalResponse does not currently provide the + // ability to specify scale. So, we just use the default scale of + // the network prefab used to represent the player. + // Note: not doing this would set the player's scale to zero since + // that is the default value of Vector3. + if (!serializedObject.IsPlayerObject) { - // Go ahead and set network parenting properties, if the latest parent is not set then pass in null - // (we always want to set worldPositionStays) - ulong? parentId = null; - if (serializedObject.IsLatestParentSet) - { - parentId = parentNetworkId; - } - networkObject.SetNetworkParenting(parentId, worldPositionStays); + // Since scale is always applied to local space scale, we do the transform + // space logic during serialization such that it works out whether AutoObjectParentSync + // is enabled or not (see NetworkObject.SceneObject) + networkObject.transform.localScale = scale; } + } - // Dynamically spawned NetworkObjects that occur during a LoadSceneMode.Single load scene event are migrated into the DDOL - // until the scene is loaded. They are then migrated back into the newly loaded and currently active scene. - if (!serializedObject.IsSceneObject && NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad) + if (serializedObject.HasParent) + { + // Go ahead and set network parenting properties, if the latest parent is not set then pass in null + // (we always want to set worldPositionStays) + ulong? parentId = null; + if (serializedObject.IsLatestParentSet) { - UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); + parentId = parentNetworkId; } + networkObject.SetNetworkParenting(parentId, worldPositionStays); } + + // Dynamically spawned NetworkObjects that occur during a LoadSceneMode.Single load scene event are migrated into the DDOL + // until the scene is loaded. They are then migrated back into the newly loaded and currently active scene. + if (!serializedObject.IsSceneObject && NetworkSceneManager.IsSpawnedObjectsPendingInDontDestroyOnLoad) + { + Object.DontDestroyOnLoad(networkObject.gameObject); + } + return networkObject; } @@ -1037,22 +1052,34 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SerializedObject s /// Server is the only instance that invokes this method. /// /// Distributed Authority: - /// DAHost client and standard DA clients invoke this method. + /// All clients can invoke this method. /// - internal void AuthorityLocalSpawn([NotNull] NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) + internal bool AuthorityLocalSpawn([NotNull] NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) { if (networkObject.IsSpawned) { - Debug.LogError($"{networkObject.name} is already spawned!"); - return; + if (NetworkManager.LogLevel <= LogLevel.Error) + { + NetworkLog.LogError($"Cannot process spawn of {networkObject.name} as it is already spawned!"); + } + return false; } - if (!sceneObject) + if (NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(networkId, out var existingObj)) + { + if (NetworkManager.LogLevel <= LogLevel.Error) + { + NetworkLog.LogError($"Cannot spawn {networkObject.name} with {nameof(networkId)}={networkId} as {existingObj.name} has already been spawned using this id!"); + } + return false; + } + + if (!sceneObject && NetworkManager.LogLevel <= LogLevel.Error) { var networkObjectChildren = networkObject.GetComponentsInChildren(); if (networkObjectChildren.Length > 1) { - Debug.LogError("Spawning NetworkObjects with nested NetworkObjects is only supported for scene objects. Child NetworkObjects will not be spawned over the network!"); + NetworkLog.LogWarning("Spawning NetworkObjects with nested NetworkObjects is only supported for scene objects. Child NetworkObjects will not be spawned over the network!"); } } @@ -1092,7 +1119,7 @@ internal void AuthorityLocalSpawn([NotNull] NetworkObject networkObject, ulong n } // Sanity check to make sure the owner is always included - // Itentionally checking as opposed to just assigning in order to generate notification. + // Intentionally checking as opposed to just assigning in order to generate notification. if (!networkObject.Observers.Contains(ownerClientId)) { Debug.LogError($"Client-{ownerClientId} is the owner of {networkObject.name} but is not an observer! Adding owner, but there is a bug in observer synchronization!"); @@ -1101,12 +1128,22 @@ internal void AuthorityLocalSpawn([NotNull] NetworkObject networkObject, ulong n } } - SpawnNetworkObjectLocallyCommon(networkObject, networkId, sceneObject, playerObject, ownerClientId, destroyWithScene); + if (!SpawnNetworkObjectLocallyCommon(networkObject, networkId, sceneObject, playerObject, ownerClientId, destroyWithScene)) + { + if (NetworkManager.LogLevel <= LogLevel.Error) + { + NetworkLog.LogError($"Failed to spawn {nameof(NetworkObject)} {networkObject.name} with Hash {networkObject.GlobalObjectIdHash}."); + } + + networkObject.ResetOnDespawn(); + return false; + } // When done spawning invoke post spawn networkObject.InvokeBehaviourNetworkPostSpawn(); - // No need to check for deferred messages since this method is used for authority spawning. + networkObject.IsSpawnAuthority = false; + return true; } /// @@ -1117,35 +1154,105 @@ internal void AuthorityLocalSpawn([NotNull] NetworkObject networkObject, ulong n /// /// IMPORTANT: Pre spawn methods need to be invoked from within . /// - internal void NonAuthorityLocalSpawn([NotNull] NetworkObject networkObject, in NetworkObject.SerializedObject serializedObject, bool destroyWithScene) + /// boolean indicating whether the spawn succeeded + internal bool NonAuthorityLocalSpawn(in NetworkObject.SerializedObject serializedObject, out NetworkObject networkObject, FastBufferReader reader, bool destroyWithScene) { + if (SpawnedObjects.ContainsKey(serializedObject.NetworkObjectId)) + { + if (NetworkManager.LogLevel <= LogLevel.Error) + { + NetworkLog.LogWarning($"Trying to spawn a {nameof(NetworkObject)} with a {nameof(NetworkObject.NetworkObjectId)} of {serializedObject.NetworkObjectId} but an object with that id already in the spawned list!"); + } + networkObject = null; + return false; + } + + byte[] instantiationData = null; + if (serializedObject.HasInstantiationData) + { + reader.ReadValueSafe(out instantiationData); + } + + // Attempt to create a local NetworkObject + networkObject = CreateLocalNetworkObject(serializedObject, instantiationData); + + // Log the error that the NetworkObject failed to construct + if (networkObject == null) + { + if (NetworkManager.LogLevel <= LogLevel.Normal) + { + NetworkLog.LogError($"Failed to spawn {nameof(NetworkObject)} for Hash {serializedObject.Hash}."); + } + + return false; + } + + networkObject.NetworkManagerOwner = NetworkManager; + + // This will get set again when the NetworkObject is spawned locally, but we set it here ahead of spawning + // in order to be able to determine which NetworkVariables the client will be allowed to read. + networkObject.OwnerClientId = serializedObject.OwnerClientId; + + // Special Case: Invoke NetworkBehaviour.OnPreSpawn methods here before SynchronizeNetworkBehaviours + networkObject.InvokeBehaviourNetworkPreSpawn(); + + // Process the remaining synchronization data from the buffer + try + { + // Synchronize NetworkBehaviours + var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); + networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, NetworkManager.LocalClientId); + } + catch (Exception ex) + { + Debug.LogException(ex); + // We can continue processing the NetworkObject spawn even if the NetworkBehaviours failed to synchronize + } + if (networkObject.IsSpawned) { - Debug.LogError($"[{networkObject.name}] Object-{networkObject.NetworkObjectId} is already spawned!"); - return; + if (NetworkManager.LogLevel <= LogLevel.Error) + { + NetworkLog.LogErrorServer($"[{networkObject.name}] Object-{networkObject.NetworkObjectId} is already spawned!"); + } + + // Mark the spawn as a success if the object is already spawned + return true; + } + + // If we are an in-scene placed NetworkObject and we originally had a parent but when synchronized we are + // being told we do not have a parent, then we want to clear the latest parent so it is not automatically + // "re-parented" to the original parent. This can happen if not unloading the scene and the parenting of + // the in-scene placed Networkobject changes several times over different sessions. + if (serializedObject.IsSceneObject && !serializedObject.HasParent && networkObject.GetNetworkParenting().HasValue) + { + networkObject.ClearNetworkParenting(); } // Do not invoke Pre spawn here (SynchronizeNetworkBehaviours needs to be invoked prior to this) - SpawnNetworkObjectLocallyCommon(networkObject, serializedObject.NetworkObjectId, serializedObject.IsSceneObject, serializedObject.IsPlayerObject, serializedObject.OwnerClientId, destroyWithScene); + var succeeded = SpawnNetworkObjectLocallyCommon(networkObject, serializedObject.NetworkObjectId, serializedObject.IsSceneObject, serializedObject.IsPlayerObject, serializedObject.OwnerClientId, destroyWithScene); + if (!succeeded) + { + Debug.LogError($"Failed to spawn {nameof(NetworkObject)} for Hash {serializedObject.Hash}."); + return false; + } // It is ok to invoke NetworkBehaviour.OnPostSpawn methods networkObject.InvokeBehaviourNetworkPostSpawn(); - // Process any deferred messages once the object is 100% finished spawning, - NetworkManager.DeferredMessageManager.ProcessTriggers(IDeferredNetworkMessageManager.TriggerType.OnSpawn, networkObject.NetworkObjectId); + return true; } - internal void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) + /// + /// Handles the all the final setup and spawning needed for + /// + /// boolean indicating whether the spawn succeeded. Internal dev note: THIS IS A CATCH FOR OURSELVES. DON'T PULL OUT + internal bool SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) { if (networkObject.NetworkManagerOwner == null) { Debug.LogError("NetworkManagerOwner should not be null!"); - } - - if (SpawnedObjects.ContainsKey(networkId)) - { - Debug.LogWarning($"[{NetworkManager.name}] Trying to spawn {networkObject.name} with a {nameof(NetworkObject.NetworkObjectId)} of {networkId} but it is already in the spawned list!"); - return; + return false; } networkObject.IsSceneObject = sceneObject; @@ -1236,6 +1343,8 @@ internal void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong { networkObject.PrefabGlobalObjectIdHash = networkObject.InScenePlacedSourceGlobalObjectIdHash; } + + return true; } internal Dictionary NetworkObjectsToSynchronizeSceneChanges = new Dictionary(); @@ -1394,7 +1503,7 @@ internal void ServerDestroySpawnedSceneObjects() if (sobj.IsSceneObject != null && sobj.IsSceneObject.Value && sobj.DestroyWithScene && sobj.gameObject.scene != NetworkManager.SceneManager.DontDestroyOnLoadScene) { SpawnedObjectsList.Remove(sobj); - UnityEngine.Object.Destroy(sobj.gameObject); + Object.Destroy(sobj.gameObject); } } } @@ -1478,7 +1587,7 @@ internal void DestroySceneObjects() } else { - UnityEngine.Object.Destroy(networkObjects[i].gameObject); + Object.Destroy(networkObjects[i].gameObject); } } } @@ -1489,23 +1598,31 @@ internal void ServerSpawnSceneObjectsOnStartSweep() { var networkObjects = FindObjects.ByType(orderByIdentifier: true); var networkObjectsToSpawn = new List(); - for (int i = 0; i < networkObjects.Length; i++) + foreach (var networkObject in networkObjects) { - if (networkObjects[i].NetworkManager == NetworkManager) + if (networkObject.NetworkManager != NetworkManager) + { + continue; + } + + // This used to be two loops. + // The first added all NetworkObjects to a list and the second spawned all NetworkObjects in the list. + // Now, a parent will set its children's IsSceneObject value when spawned, so we check for null or for true. + if (networkObject.IsSceneObject == null || (networkObject.IsSceneObject.HasValue && networkObject.IsSceneObject.Value)) { - // This used to be two loops. - // The first added all NetworkObjects to a list and the second spawned all NetworkObjects in the list. - // Now, a parent will set its children's IsSceneObject value when spawned, so we check for null or for true. - if (networkObjects[i].IsSceneObject == null || (networkObjects[i].IsSceneObject.HasValue && networkObjects[i].IsSceneObject.Value)) + var ownerId = networkObject.OwnerClientId; + if (NetworkManager.DistributedAuthorityMode) { - var ownerId = networkObjects[i].OwnerClientId; - if (NetworkManager.DistributedAuthorityMode) - { - ownerId = NetworkManager.LocalClientId; - } + ownerId = NetworkManager.LocalClientId; + } - AuthorityLocalSpawn(networkObjects[i], GetNetworkObjectId(), true, false, ownerId, true); - networkObjectsToSpawn.Add(networkObjects[i]); + if (AuthorityLocalSpawn(networkObject, GetNetworkObjectId(), true, false, ownerId, true)) + { + networkObjectsToSpawn.Add(networkObject); + } + else + { + networkObject.ResetOnDespawn(); } } } @@ -1717,7 +1834,7 @@ internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObjec } else { - UnityEngine.Object.Destroy(gobj); + Object.Destroy(gobj); } } } @@ -1846,8 +1963,8 @@ internal NetworkSpawnManager(NetworkManager networkManager) internal void Shutdown() { - NetworkObjectsToSynchronizeSceneChanges.Clear(); - CleanUpDisposedObjects.Clear(); + NetworkObjectsToSynchronizeSceneChanges?.Clear(); + CleanUpDisposedObjects?.Clear(); } /// diff --git a/com.unity.netcode.gameobjects/Runtime/Unity.Netcode.Runtime.asmdef b/com.unity.netcode.gameobjects/Runtime/Unity.Netcode.Runtime.asmdef index c04d2a6fd4..3d0e9a58bf 100644 --- a/com.unity.netcode.gameobjects/Runtime/Unity.Netcode.Runtime.asmdef +++ b/com.unity.netcode.gameobjects/Runtime/Unity.Netcode.Runtime.asmdef @@ -97,6 +97,11 @@ "name": "Unity", "expression": "6000.5.0a7", "define": "NGO_FINDOBJECTS_NOSORTING" + }, + { + "name": "Unity", + "expression": "6000.6.0a3", + "define": "NGO_FINDOBJECTS_UNORDERED_IDS" } ], "noEngineReferences": false diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs index 9e439ba098..e99843b087 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs @@ -985,6 +985,12 @@ private bool AllPlayerObjectClonesSpawned(NetworkManager joinedClient) return false; } + if (playerObjectRelative.Observers.Count != m_NetworkManagers.Length) + { + m_InternalErrorLog.Append($"Client-{networkManager.LocalClientId} has an incorrect number of observers for Object-{playerObjectRelative.NetworkObjectId}!"); + return false; + } + // Go ahead and create an entry for this new client if (!m_PlayerNetworkObjects[networkManager.LocalClientId].ContainsKey(joinedClient.LocalClientId)) { @@ -1267,7 +1273,7 @@ protected IEnumerator StartServerAndClients() { VerboseDebug($"Entering {nameof(StartServerAndClients)}"); - // DANGO-TODO: Renove this when the Rust server connection sequence is fixed and we don't have to pre-start + // DANGO-TODO: Remove this when the Rust server connection sequence is fixed and we don't have to pre-start // the session owner. if (m_UseCmbService) { @@ -1536,6 +1542,11 @@ protected void ShutdownAndCleanUp() } } + foreach (var networkManager in m_NetworkManagers) + { + networkManager?.Shutdown(); + } + // Cleanup any remaining NetworkObjects DestroySceneNetworkObjects(); @@ -1579,6 +1590,11 @@ protected IEnumerator CoroutineShutdownAndCleanUp() } } + foreach (var networkManager in m_NetworkManagers) + { + networkManager?.Shutdown(); + } + // Allow time for NetworkManagers to fully shutdown yield return k_DefaultTickRate; @@ -1733,7 +1749,7 @@ protected void DestroySceneNetworkObjects() // This can sometimes be null depending upon order of operations // when dealing with parented NetworkObjects. If NetworkObjectB // is a child of NetworkObjectA and NetworkObjectA comes before - // NetworkObjectB in the list of NeworkObjects found, then when + // NetworkObjectB in the list of NetworkObjects found, then when // NetworkObjectA's GameObject is destroyed it will also destroy // NetworkObjectB's GameObject which will destroy NetworkObjectB. // If there is a null entry in the list, this is the most likely diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerEventNotifications.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerEventNotifications.cs index ddccd7c029..b7d2fa953b 100644 --- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerEventNotifications.cs +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerEventNotifications.cs @@ -37,6 +37,7 @@ internal class SceneTestInfo } private List m_ShouldWaitList = new List(); + private List m_ServerReceivedSynchronize = new List(); private List m_ClientsReceivedSynchronize = new List(); public NetworkSceneManagerEventNotifications(HostOrServer hostOrServer) : base(hostOrServer) { } @@ -58,6 +59,7 @@ protected override IEnumerator OnSetup() { m_ScenesLoaded.Clear(); m_CanStartServerOrClients = false; + m_ServerReceivedSynchronize.Clear(); m_ClientsReceivedSynchronize.Clear(); m_ShouldWaitList.Clear(); return base.OnSetup(); @@ -72,6 +74,13 @@ protected override IEnumerator OnTearDown() return base.OnTearDown(); } + protected override void OnServerAndClientsCreated() + { + var authority = GetAuthorityNetworkManager(); + + base.OnServerAndClientsCreated(); + } + protected override IEnumerator OnStartedServerAndClients() { foreach (var manager in m_NetworkManagers) @@ -92,6 +101,12 @@ private void ClientSceneManager_OnSceneEvent(SceneEvent sceneEvent) { switch (sceneEvent.SceneEventType) { + // Validates that we sent the proper number of synchronize events to the clients + case SceneEventType.Synchronize: + { + m_ClientsReceivedSynchronize.Add(sceneEvent.ClientId); + break; + } // Validate that the clients finish synchronization and they used the proper synchronization mode case SceneEventType.SynchronizeComplete: { @@ -107,13 +122,13 @@ private void ClientSceneManager_OnSceneEvent(SceneEvent sceneEvent) private void ServerSceneManager_OnSceneEvent(SceneEvent sceneEvent) { var authority = GetAuthorityNetworkManager(); - VerboseDebug($"[SceneEvent] ClientId:{sceneEvent.ClientId} | EventType: {sceneEvent.SceneEventType}"); + VerboseLog($"[SceneEvent] ClientId:{sceneEvent.ClientId} | EventType: {sceneEvent.SceneEventType}"); switch (sceneEvent.SceneEventType) { // Validates that we sent the proper number of synchronize events to the clients case SceneEventType.Synchronize: { - m_ClientsReceivedSynchronize.Add(sceneEvent.ClientId); + m_ServerReceivedSynchronize.Add(sceneEvent.ClientId); break; } case SceneEventType.Load: @@ -238,8 +253,11 @@ public IEnumerator SceneLoadingAndNotifications([Values] LoadSceneMode loadScene m_CanStartServerOrClients = true; yield return StartServerAndClients(); + yield return WaitForConditionOrTimeOut(() => m_ServerReceivedSynchronize.Count == NumberOfClients); + AssertOnTimeout($"Timed out waiting for the authority to receive synchronization events for all clients! Received: {m_ServerReceivedSynchronize.Count} | Expected: {NumberOfClients}"); yield return WaitForConditionOrTimeOut(() => m_ClientsReceivedSynchronize.Count == NumberOfClients); AssertOnTimeout($"Timed out waiting for all clients to receive synchronization event! Received: {m_ClientsReceivedSynchronize.Count} | Expected: {NumberOfClients}"); + if (loadSceneMode == LoadSceneMode.Single) { m_ClientToTestLoading.SceneManager.OnSceneEvent += SceneManager_OnSceneEvent;