From ba2cb5fddf87f634ca68515ba99171c54629e149 Mon Sep 17 00:00:00 2001 From: Emma Date: Thu, 2 Jul 2026 18:44:20 -0400 Subject: [PATCH 1/3] fix: NullReferenceExceptions in spawn path --- .../Connection/NetworkConnectionManager.cs | 2 +- .../Runtime/Core/NetworkObject.cs | 28 ++- .../Runtime/Messaging/ILPPMessageProvider.cs | 1 + .../Messages/ConnectionApprovedMessage.cs | 4 +- .../Messaging/Messages/CreateObjectMessage.cs | 59 ++--- .../Runtime/SceneManagement/SceneEventData.cs | 21 +- .../Runtime/Spawning/NetworkSpawnManager.cs | 10 +- .../NetworkObjectSynchronizationTests.cs | 204 +++++++----------- ...etworkPrefabHandlerSynchronizationTests.cs | 88 ++++++++ ...kPrefabHandlerSynchronizationTests.cs.meta | 3 + 10 files changed, 234 insertions(+), 186 deletions(-) create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerSynchronizationTests.cs create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerSynchronizationTests.cs.meta diff --git a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs index cc44bd8783..dd22801412 100644 --- a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs @@ -1218,7 +1218,7 @@ internal void ApprovedPlayerSpawn(ulong clientId, uint playerPrefabHash) var message = new CreateObjectMessage { - ObjectInfo = ConnectedClients[clientId].PlayerObject.Serialize(clientPair.Key), + ObjectInfo = ConnectedClients[clientId].PlayerObject.SerializeSpawnedObject(clientPair.Key), IncludesSerializedObject = true, }; diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 3423c05a07..3e99519993 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Text; @@ -3277,6 +3278,7 @@ public void Deserialize(FastBufferReader reader) var readSize = 0; readSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0; readSize += FastBufferWriter.GetWriteSize(); + readSize += FastBufferWriter.GetWriteSize(); // Try to begin reading the remaining bytes if (!reader.TryBeginRead(readSize)) @@ -3295,7 +3297,7 @@ public void Deserialize(FastBufferReader reader) // Read the size of the remaining synchronization data // This data will be read in AddSceneObject() - reader.ReadValueSafe(out SynchronizationDataSize); + reader.ReadValue(out SynchronizationDataSize); } } @@ -3369,7 +3371,12 @@ internal void SynchronizeNetworkBehaviours(ref BufferSerializer serializer } } - internal SerializedObject Serialize(ulong targetClientId = NetworkManager.ServerClientId, bool syncObservers = false) + /// + /// Creates a on an authority client. + /// Used to synchronize state to a non-authority client. + /// + /// This function is the authority mirror of + internal SerializedObject SerializeSpawnedObject(ulong targetClientId = NetworkManager.ServerClientId, bool syncObservers = false) { var obj = new SerializedObject { @@ -3444,15 +3451,17 @@ internal SerializedObject Serialize(ulong targetClientId = NetworkManager.Server } /// - /// Used to deserialize a serialized which occurs - /// when the client is approved or during a scene transition + /// Does a non-authority local spawn of a given . + /// This occurs when the client is approved, a new object is spawned by an authority, or during a scene transition. /// - /// Deserialized scene object data - /// FastBufferReader for the NetworkVariable data - /// NetworkManager instance + /// This function is the non-authority mirror of + /// Deserialized data received from the authority for this + /// FastBufferReader for any additional data sent with this object on spawn. + /// NetworkManager instance. /// will be true if invoked by CreateObjectMessage /// The deserialized NetworkObject or null if deserialization failed - internal static NetworkObject Deserialize(in SerializedObject serializedObject, FastBufferReader reader, NetworkManager networkManager, bool invokedByMessage = false) + [return: MaybeNull] + internal static NetworkObject DeserializeAndSpawnObject(in SerializedObject serializedObject, FastBufferReader reader, NetworkManager networkManager, bool invokedByMessage = false) { var endOfSynchronizationData = reader.Position + serializedObject.SynchronizationDataSize; @@ -3479,7 +3488,8 @@ internal static NetworkObject Deserialize(in SerializedObject serializedObject, { if (networkManager.LogLevel <= LogLevel.Normal) { - NetworkLog.LogWarning($"[{networkObject.name}][Deserialize][{nameof(NetworkBehaviour)}Synchronization][Size mismatch] Expected: {endOfSynchronizationData} Currently At: {reader.Position}!"); + var networkObjectName = networkObject != null ? networkObject.name : "null"; + NetworkLog.LogWarning($"[{networkObjectName}][Deserialize][{nameof(NetworkBehaviour)}Synchronization][Size mismatch] Expected: {endOfSynchronizationData} Currently At: {reader.Position}!"); } reader.Seek(endOfSynchronizationData); } diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/ILPPMessageProvider.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/ILPPMessageProvider.cs index 21413a4fae..8d8379dc05 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/ILPPMessageProvider.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/ILPPMessageProvider.cs @@ -148,6 +148,7 @@ internal static Dictionary GetMessageTypesMap() [InitializeOnLoadMethod] public static void NotifyOnPlayStateChange() { + EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; EditorApplication.playModeStateChanged += OnPlayModeStateChanged; } diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs index 17669a335f..2a7eee8be7 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs @@ -160,7 +160,7 @@ public void Serialize(FastBufferWriter writer, int targetVersion) { sobj.AddObserver(OwnerClientId); // In distributed authority mode, we send the currently known observers of each NetworkObject to the client being synchronized. - var serializedObject = sobj.Serialize(OwnerClientId, IsDistributedAuthority); + var serializedObject = sobj.SerializeSpawnedObject(OwnerClientId, IsDistributedAuthority); serializedObject.Serialize(writer); ++sceneObjectCount; } @@ -344,7 +344,7 @@ public void Handle(ref NetworkContext context) { var serializedObject = new NetworkObject.SerializedObject(); serializedObject.Deserialize(m_ReceivedSceneObjectData); - NetworkObject.Deserialize(serializedObject, m_ReceivedSceneObjectData, networkManager); + NetworkObject.DeserializeAndSpawnObject(serializedObject, m_ReceivedSceneObjectData, networkManager); } if (networkManager.DistributedAuthorityMode && networkManager.AutoSpawnPlayerPrefabClientSide) diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs index b8f8af78fb..a5f3039251 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Runtime.CompilerServices; +using Unity.Netcode.Logging; namespace Unity.Netcode { @@ -171,7 +172,12 @@ internal static void CreateObject(ref NetworkManager networkManager, ulong sende { if (!networkManager.DistributedAuthorityMode) { - networkObject = NetworkObject.Deserialize(serializedObject, networkVariableData, networkManager); + networkObject = NetworkObject.DeserializeAndSpawnObject(serializedObject, networkVariableData, networkManager); + if (networkObject == null) + { + networkManager.Log.ErrorServer(new Context(LogLevel.Developer, $"Failed to deserialize {nameof(NetworkObject)}.").AddInfo(nameof(NetworkObject.GlobalObjectIdHash), serializedObject.Hash).AddInfo(nameof(NetworkObject.NetworkObjectId), serializedObject.NetworkObjectId)); + return; + } } else { @@ -179,25 +185,27 @@ internal static void CreateObject(ref NetworkManager networkManager, ulong sende var hasNewObserverIdList = newObserverIds != null && newObserverIds.Length > 0; // Depending upon visibility of the NetworkObject and the client in question, it could be that // this client already has visibility of this NetworkObject - if (networkManager.SpawnManager.SpawnedObjects.ContainsKey(serializedObject.NetworkObjectId)) + if (networkManager.SpawnManager.SpawnedObjects.TryGetValue(serializedObject.NetworkObjectId, out networkObject)) { - // If so, then just get the local instance - networkObject = networkManager.SpawnManager.SpawnedObjects[serializedObject.NetworkObjectId]; - // This should not happen, logging error just in case if (hasNewObserverIdList && newObserverIds.Contains(networkManager.LocalClientId)) { NetworkLog.LogErrorServer($"[{nameof(CreateObjectMessage)}][Duplicate-Broadcast] Detected duplicated object creation for {serializedObject.NetworkObjectId}!"); } - else // Trap to make sure the owner is not receiving any messages it sent - if (networkManager.CMBServiceConnection && networkManager.LocalClientId == networkObject.OwnerClientId) - { - NetworkLog.LogWarning($"[{nameof(CreateObjectMessage)}][Client-{networkManager.LocalClientId}][Duplicate-CreateObjectMessage][Client Is Owner] Detected duplicated object creation for {networkObject.name}-{serializedObject.NetworkObjectId}!"); - } + // Trap to make sure the owner is not receiving any messages it sent + else if (networkManager.CMBServiceConnection && networkManager.LocalClientId == networkObject.OwnerClientId) + { + NetworkLog.LogWarning($"[{nameof(CreateObjectMessage)}][Client-{networkManager.LocalClientId}][Duplicate-CreateObjectMessage][Client Is Owner] Detected duplicated object creation for {networkObject.name}-{serializedObject.NetworkObjectId}!"); + } } else { - networkObject = NetworkObject.Deserialize(serializedObject, networkVariableData, networkManager, true); + networkObject = NetworkObject.DeserializeAndSpawnObject(serializedObject, networkVariableData, networkManager, true); + if (networkObject == null) + { + networkManager.Log.ErrorServer(new Context(LogLevel.Developer, $"Failed to deserialize {nameof(NetworkObject)}.").AddInfo(nameof(NetworkObject.GlobalObjectIdHash), serializedObject.Hash).AddInfo(nameof(NetworkObject.NetworkObjectId), serializedObject.NetworkObjectId)); + return; + } } // DA - NGO CMB SERVICE NOTES: @@ -214,27 +222,6 @@ internal static void CreateObject(ref NetworkManager networkManager, ulong sende // Mock CMB Service and forward to all clients if (networkManager.DAHost) { - // DA - NGO CMB SERVICE NOTES: - // (*** See above notes fist ***) - // If it is a player object freshly spawning and one or more clients all connect at the exact same time (i.e. received on effectively - // the same frame), then we need to check the observers list to make sure all players are visible upon first spawning. At a later date, - // for area of interest we will need to have some form of follow up "observer update" message to cull out players not within each - // player's AOI. - if (networkObject.IsPlayerObject && hasNewObserverIdList && clientList.Count != observerIds.Length) - { - // For same-frame newly spawned players that might not be aware of all other players, update the player's observer - // list. - observerIds = clientList.ToArray(); - } - - var createObjectMessage = new CreateObjectMessage() - { - ObjectInfo = serializedObject, - m_ReceivedNetworkVariableData = networkVariableData, - ObserverIds = hasObserverIdList ? observerIds : null, - NetworkObjectId = networkObject.NetworkObjectId, - IncludesSerializedObject = true, - }; foreach (var clientId in clientList) { // DA - NGO CMB SERVICE NOTES: @@ -250,16 +237,12 @@ internal static void CreateObject(ref NetworkManager networkManager, ulong sende // If this included a list of new observers and the targeted clientId is one of the observers, then send the serialized data. // Otherwise, the targeted clientId has already has visibility (i.e. it is already spawned) and so just send the updated // observers list to that client's instance. - createObjectMessage.IncludesSerializedObject = hasNewObserverIdList && newObserverIds.Contains(clientId); - networkManager.SpawnManager.SendSpawnCallForObject(clientId, networkObject); } } } - if (networkObject != null) - { - networkManager.NetworkMetrics.TrackObjectSpawnReceived(senderId, networkObject, messageSize); - } + + networkManager.NetworkMetrics.TrackObjectSpawnReceived(senderId, networkObject, messageSize); } catch (System.Exception ex) { diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs index 4840944117..2575bd6bfd 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs @@ -622,7 +622,7 @@ private void WriteSceneSynchronizationData(FastBufferWriter writer) { var noStart = writer.Position; // In distributed authority mode, we send the currently known observers of each NetworkObject to the client being synchronized. - var serializedObject = networkObject.Serialize(TargetClientId, distributedAuthority); + var serializedObject = networkObject.SerializeSpawnedObject(TargetClientId, distributedAuthority); serializedObject.Serialize(writer); var noStop = writer.Position; @@ -698,7 +698,7 @@ private void SerializeScenePlacedObjects(FastBufferWriter writer) foreach (var objectToSync in m_NetworkObjectsSync) { // Serialize the NetworkObject - var serializedObject = objectToSync.Serialize(TargetClientId, distributedAuthority); + var serializedObject = objectToSync.SerializeSpawnedObject(TargetClientId, distributedAuthority); serializedObject.Serialize(writer); numberOfObjects++; } @@ -875,9 +875,9 @@ internal void DeserializeScenePlacedObjects() m_NetworkManager.SceneManager.SetTheSceneBeingSynchronized(serializedObject.NetworkSceneHandle); } - var networkObject = NetworkObject.Deserialize(serializedObject, m_InternalBuffer, m_NetworkManager); + var networkObject = NetworkObject.DeserializeAndSpawnObject(serializedObject, m_InternalBuffer, m_NetworkManager); - if (serializedObject.IsSceneObject) + if (serializedObject.IsSceneObject && networkObject != null) { sceneObjects.Add(networkObject); } @@ -1101,7 +1101,11 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) { m_NetworkManager.SceneManager.SetTheSceneBeingSynchronized(serializedObject.NetworkSceneHandle); } - var spawnedNetworkObject = NetworkObject.Deserialize(serializedObject, m_InternalBuffer, networkManager); + var spawnedNetworkObject = NetworkObject.DeserializeAndSpawnObject(serializedObject, m_InternalBuffer, networkManager); + if (spawnedNetworkObject == null) + { + continue; + } var noStop = m_InternalBuffer.Position; if (EnableSerializationLogs) @@ -1110,12 +1114,9 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) LogArray(m_InternalBuffer.ToArray(), noStart, noStop, builder); } // If we failed to deserialize the NetworkObject then don't add null to the list - if (spawnedNetworkObject != null) + if (!m_NetworkObjectsSync.Contains(spawnedNetworkObject)) { - if (!m_NetworkObjectsSync.Contains(spawnedNetworkObject)) - { - m_NetworkObjectsSync.Add(spawnedNetworkObject); - } + m_NetworkObjectsSync.Add(spawnedNetworkObject); } } if (EnableSerializationLogs) diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 38711e155c..2f8ae30d03 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -1165,13 +1165,13 @@ internal bool AuthorityLocalSpawn([NotNull] NetworkObject networkObject, ulong n /// /// Only spawn non-authority instances should invoke this. /// This is invoked to instantiate an authority spawned , and - /// is only invoked by: + /// is only invoked by: /// /// - /// IMPORTANT: Pre spawn methods need to be invoked from within . + /// IMPORTANT: Pre spawn methods need to be invoked from within . /// /// boolean indicating whether the spawn succeeded - internal bool NonAuthorityLocalSpawn(in NetworkObject.SerializedObject serializedObject, out NetworkObject networkObject, FastBufferReader reader, bool destroyWithScene) + internal bool NonAuthorityLocalSpawn(in NetworkObject.SerializedObject serializedObject, [MaybeNullWhen(false)] out NetworkObject networkObject, FastBufferReader reader, bool destroyWithScene) { if (SpawnedObjects.ContainsKey(serializedObject.NetworkObjectId)) { @@ -1368,7 +1368,7 @@ internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject } var message = new CreateObjectMessage { - ObjectInfo = networkObject.Serialize(clientId, NetworkManager.DistributedAuthorityMode), + ObjectInfo = networkObject.SerializeSpawnedObject(clientId, NetworkManager.DistributedAuthorityMode), IncludesSerializedObject = true, UpdateObservers = NetworkManager.DistributedAuthorityMode, ObserverIds = NetworkManager.DistributedAuthorityMode ? networkObject.Observers.ToArray() : null, @@ -1394,7 +1394,7 @@ internal void SendSpawnCallForObserverUpdate(ulong[] newObservers, NetworkObject var message = new CreateObjectMessage { - ObjectInfo = networkObject.Serialize(), + ObjectInfo = networkObject.SerializeSpawnedObject(), ObserverIds = networkObject.Observers.ToArray(), NewObserverIds = newObservers.ToArray(), IncludesSerializedObject = true, diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectSynchronizationTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectSynchronizationTests.cs index 6224c6c627..7535e4ffce 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectSynchronizationTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectSynchronizationTests.cs @@ -8,7 +8,6 @@ namespace Unity.Netcode.RuntimeTests { - [UnityPlatform(exclude = new[] { RuntimePlatform.IPhonePlayer })] // Ignored test tracked in MTT-14172 [TestFixture(VariableLengthSafety.DisableNetVarSafety, HostOrServer.DAHost)] [TestFixture(VariableLengthSafety.DisableNetVarSafety, HostOrServer.Host)] @@ -28,12 +27,6 @@ internal class NetworkObjectSynchronizationTests : NetcodeIntegrationTest private LogLevel m_CurrentLogLevel; - // TODO: [CmbServiceTests] Adapt to run with the service - protected override bool UseCMBService() - { - return false; - } - public enum VariableLengthSafety { DisableNetVarSafety, @@ -57,15 +50,15 @@ protected override void OnCreatePlayerPrefab() protected override void OnServerAndClientsCreated() { - + var authority = GetAuthorityNetworkManager(); // Set the NetworkVariable Safety Check setting - m_ServerNetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety = m_VariableLengthSafety == VariableLengthSafety.EnabledNetVarSafety; + authority.NetworkConfig.EnsureNetworkVariableLengthSafety = m_VariableLengthSafety == VariableLengthSafety.EnabledNetVarSafety; // Ignore the errors generated during this test (they are expected) - m_ServerNetworkManager.LogLevel = LogLevel.Nothing; + authority.LogLevel = LogLevel.Nothing; // Disable forcing the same prefabs to avoid failed connections - m_ServerNetworkManager.NetworkConfig.ForceSamePrefabs = false; + authority.NetworkConfig.ForceSamePrefabs = false; // Create the valid network prefab m_NetworkPrefab = CreateNetworkObjectPrefab("ValidObject"); @@ -104,8 +97,9 @@ protected override void OnNewClientCreated(NetworkManager networkManager) public IEnumerator NetworkObjectDeserializationFailure() { m_CurrentLogLevel = LogLevel.Nothing; - var validSpawnedNetworkObjects = new List(); + var authoritySpawnedNetworkObjects = new List(); NetworkBehaviourWithNetworkVariables.ResetSpawnCount(); + var authority = GetAuthorityNetworkManager(); // Spawn NetworkObjects on the server side with half of them being the // invalid network prefabs to simulate NetworkObject synchronization failure @@ -113,47 +107,29 @@ public IEnumerator NetworkObjectDeserializationFailure() { if (i % 2 == 0) { - SpawnObject(m_InValidNetworkPrefab, m_ServerNetworkManager); + SpawnObject(m_InValidNetworkPrefab, authority); } else { // Keep track of the prefabs that should successfully spawn on the client side - validSpawnedNetworkObjects.Add(SpawnObject(m_NetworkPrefab, m_ServerNetworkManager)); + var instance = SpawnObject(m_NetworkPrefab, authority); + authoritySpawnedNetworkObjects.Add(instance.GetComponent()); } } // Assure the server-side spawned all NetworkObjects - yield return WaitForConditionOrTimeOut(() => NetworkBehaviourWithNetworkVariables.ServerSpawnCount == k_NumberToSpawn); + yield return WaitForConditionOrTimeOut(() => NetworkBehaviourWithNetworkVariables.AuthoritySpawnCount == k_NumberToSpawn); // Now spawn and connect a client that will fail to spawn half of the NetworkObjects spawned - yield return CreateAndStartNewClient(); + var newClient = CreateNewClient(); + yield return StartClient(newClient); if (m_UseHost) { - var delayCounter = 0; - while (m_ClientNetworkManagers.Length == 0) - { - delayCounter++; - Assert.True(delayCounter < 30, "TimeOut waiting for client to spawn!"); - yield return s_DefaultWaitForTick; - } - delayCounter = 0; - while (!m_PlayerNetworkObjects[m_ServerNetworkManager.LocalClientId].ContainsKey(m_ClientNetworkManagers[0].LocalClientId)) - { - delayCounter++; - if (delayCounter >= 30) - { - VerboseDebug("Trap!"); - } - Assert.True(delayCounter < 30, "TimeOut waiting for client to spawn!"); - yield return s_DefaultWaitForTick; - } - - - var serverSideClientPlayerComponent = m_PlayerNetworkObjects[m_ServerNetworkManager.LocalClientId][m_ClientNetworkManagers[0].LocalClientId].GetComponent(); - var serverSideHostPlayerComponent = m_ServerNetworkManager.LocalClient.PlayerObject.GetComponent(); - var clientSidePlayerComponent = m_ClientNetworkManagers[0].LocalClient.PlayerObject.GetComponent(); - var clientSideHostPlayerComponent = m_PlayerNetworkObjects[m_ClientNetworkManagers[0].LocalClientId][m_ServerNetworkManager.LocalClientId].GetComponent(); + var serverSideClientPlayerComponent = m_PlayerNetworkObjects[authority.LocalClientId][newClient.LocalClientId].GetComponent(); + var serverSideHostPlayerComponent = authority.LocalClient.PlayerObject.GetComponent(); + var clientSidePlayerComponent = newClient.LocalClient.PlayerObject.GetComponent(); + var clientSideHostPlayerComponent = m_PlayerNetworkObjects[newClient.LocalClientId][authority.LocalClientId].GetComponent(); var modeText = m_DistributedAuthority ? "owner" : "server"; // Validate that the client side player values match the server side value of the client's player Assert.IsTrue(serverSideClientPlayerComponent.NetworkVariableData1.Value == clientSidePlayerComponent.NetworkVariableData1.Value, @@ -192,16 +168,15 @@ public IEnumerator NetworkObjectDeserializationFailure() else { // Spawn and connect another client when running as a server - yield return CreateAndStartNewClient(); - yield return WaitForConditionOrTimeOut(() => m_PlayerNetworkObjects[2].Count > 1); - AssertOnTimeout($"Timed out waiting for second client to have access to the first client's cloned player object!"); + var secondClient = CreateNewClient(); + yield return StartClient(secondClient); - var clientSide1PlayerComponent = m_ClientNetworkManagers[0].LocalClient.PlayerObject.GetComponent(); - var clientSide2Player1Clone = m_PlayerNetworkObjects[2][clientSide1PlayerComponent.OwnerClientId].GetComponent(); + var clientSide1PlayerComponent = newClient.LocalClient.PlayerObject.GetComponent(); + var clientSide2Player1Clone = m_PlayerNetworkObjects[secondClient.LocalClientId][clientSide1PlayerComponent.OwnerClientId].GetComponent(); var clientOneId = clientSide1PlayerComponent.OwnerClientId; - var clientSide2PlayerComponent = m_ClientNetworkManagers[1].LocalClient.PlayerObject.GetComponent(); - var clientSide1Player2Clone = m_PlayerNetworkObjects[1][clientSide2PlayerComponent.OwnerClientId].GetComponent(); + var clientSide2PlayerComponent = secondClient.LocalClient.PlayerObject.GetComponent(); + var clientSide1Player2Clone = m_PlayerNetworkObjects[newClient.LocalClientId][clientSide2PlayerComponent.OwnerClientId].GetComponent(); var clientTwoId = clientSide2PlayerComponent.OwnerClientId; // Validate that client one's 2nd and 4th NetworkVariables for the local and clone instances match and the other two do not @@ -244,75 +219,57 @@ public IEnumerator NetworkObjectDeserializationFailure() } } - // DANGO-TODO: This scenario is only possible to do if we add a DA-Server to mock the CMB Service or we integrate the CMB Service AND we have updated NetworkVariable permissions - // to only allow the service to write. For now, we will skip this validation for distributed authority - if (!m_DistributedAuthority) + // Now validate all of the NetworkVariable values match to assure everything synchronized properly + foreach (var spawnedObject in authoritySpawnedNetworkObjects) { - // Now validate all of the NetworkVariable values match to assure everything synchronized properly - foreach (var spawnedObject in validSpawnedNetworkObjects) + foreach (var networkManager in m_NetworkManagers) { - foreach (var clientNetworkManager in m_ClientNetworkManagers) + if (networkManager == authority) { - //Validate that the connected client has spawned all of the instances that shouldn't have failed. - var clientSideNetworkObjects = s_GlobalNetworkObjects[clientNetworkManager.LocalClientId]; + continue; + } + //Validate that the connected client has spawned all of the instances that shouldn't have failed. + var clientSideNetworkObjects = s_GlobalNetworkObjects[networkManager.LocalClientId]; - Assert.IsTrue(NetworkBehaviourWithNetworkVariables.ClientSpawnCount[clientNetworkManager.LocalClientId] == validSpawnedNetworkObjects.Count, $"Client-{clientNetworkManager.LocalClientId} spawned " + - $"({NetworkBehaviourWithNetworkVariables.ClientSpawnCount}) {nameof(NetworkObject)}s but the expected number of {nameof(NetworkObject)}s should have been ({validSpawnedNetworkObjects.Count})!"); + Assert.IsTrue(NetworkBehaviourWithNetworkVariables.NonAuthoritySpawnCount[networkManager.LocalClientId] == authoritySpawnedNetworkObjects.Count, $"Client-{networkManager.LocalClientId} spawned " + + $"({NetworkBehaviourWithNetworkVariables.NonAuthoritySpawnCount}) {nameof(NetworkObject)}s but the expected number of {nameof(NetworkObject)}s should have been ({authoritySpawnedNetworkObjects.Count})!"); - var spawnedNetworkObject = spawnedObject.GetComponent(); - Assert.IsTrue(clientSideNetworkObjects.ContainsKey(spawnedNetworkObject.NetworkObjectId), $"Failed to find valid spawned {nameof(NetworkObject)} on the client-side with a " + - $"{nameof(NetworkObject.NetworkObjectId)} of {spawnedNetworkObject.NetworkObjectId}"); + Assert.IsTrue(clientSideNetworkObjects.ContainsKey(spawnedObject.NetworkObjectId), $"Failed to find valid spawned {nameof(NetworkObject)} on the client-side with a " + + $"{nameof(NetworkObject.NetworkObjectId)} of {spawnedObject.NetworkObjectId}"); - var clientSideObject = clientSideNetworkObjects[spawnedNetworkObject.NetworkObjectId]; - Assert.IsTrue(clientSideObject.NetworkManager == clientNetworkManager, $"Client-side object {clientSideObject}'s {nameof(NetworkManager)} is not valid!"); + var clientSideObject = clientSideNetworkObjects[spawnedObject.NetworkObjectId]; + Assert.IsTrue(clientSideObject.NetworkManager == networkManager, $"Client-side object {clientSideObject}'s {nameof(NetworkManager)} is not valid!"); - ValidateNetworkBehaviourWithNetworkVariables(spawnedNetworkObject, clientSideObject); - } + ValidateNetworkBehaviourWithNetworkVariables(spawnedObject, clientSideObject); } } } - private void ValidateNetworkBehaviourWithNetworkVariables(NetworkObject serverSideNetworkObject, NetworkObject clientSideNetworkObject) + private void ValidateNetworkBehaviourWithNetworkVariables(NetworkObject authorityNetworkObject, NetworkObject nonAuthorityNetworkObject) { - var serverSideComponent = serverSideNetworkObject.GetComponent(); - var clientSideComponent = clientSideNetworkObject.GetComponent(); + var authorityComponent = authorityNetworkObject.GetComponent(); + var nonAuthorityComponent = nonAuthorityNetworkObject.GetComponent(); string netVarName1 = nameof(NetworkBehaviourWithNetworkVariables.NetworkVariableData1); string netVarName2 = nameof(NetworkBehaviourWithNetworkVariables.NetworkVariableData1); string netVarName3 = nameof(NetworkBehaviourWithNetworkVariables.NetworkVariableData1); string netVarName4 = nameof(NetworkBehaviourWithNetworkVariables.NetworkVariableData1); - Assert.IsTrue(serverSideComponent.NetworkVariableData1.Count == clientSideComponent.NetworkVariableData1.Count, $"[{serverSideComponent.name}:{netVarName1}] Server side {nameof(NetworkList)} " + - $"count ({serverSideComponent.NetworkVariableData1.Count}) does not match the client side {nameof(NetworkList)} count ({clientSideComponent.NetworkVariableData1.Count})!"); + Assert.IsTrue(authorityComponent.NetworkVariableData1.Count == nonAuthorityComponent.NetworkVariableData1.Count, $"[{authorityComponent.name}:{netVarName1}] Server side {nameof(NetworkList)} " + + $"count ({authorityComponent.NetworkVariableData1.Count}) does not match the client side {nameof(NetworkList)} count ({nonAuthorityComponent.NetworkVariableData1.Count})!"); - for (int i = 0; i < serverSideComponent.NetworkVariableData1.Count; i++) + for (int i = 0; i < authorityComponent.NetworkVariableData1.Count; i++) { - Assert.IsTrue(serverSideComponent.NetworkVariableData1[i] == clientSideComponent.NetworkVariableData1[i], $"[{serverSideComponent.name}:{netVarName1}][Index:{i}] Server side instance value " + - $"({serverSideComponent.NetworkVariableData1[i]}) does not match the client side instance value ({clientSideComponent.NetworkVariableData1[i]})!"); + Assert.IsTrue(authorityComponent.NetworkVariableData1[i] == nonAuthorityComponent.NetworkVariableData1[i], $"[{authorityComponent.name}:{netVarName1}][Index:{i}] Server side instance value " + + $"({authorityComponent.NetworkVariableData1[i]}) does not match the client side instance value ({nonAuthorityComponent.NetworkVariableData1[i]})!"); } - Assert.IsTrue(serverSideComponent.NetworkVariableData2.Value == clientSideComponent.NetworkVariableData2.Value, $"[{serverSideComponent.name}:{netVarName2}] Server side instance value ({serverSideComponent.NetworkVariableData2.Value}) " + - $"does not match the client side instance value ({clientSideComponent.NetworkVariableData2.Value})!"); - Assert.IsTrue(serverSideComponent.NetworkVariableData3.Value == clientSideComponent.NetworkVariableData3.Value, $"[{serverSideComponent.name}:{netVarName3}] Server side instance value ({serverSideComponent.NetworkVariableData3.Value}) " + - $"does not match the client side instance value ({clientSideComponent.NetworkVariableData3.Value})!"); - Assert.IsTrue(serverSideComponent.NetworkVariableData4.Value == clientSideComponent.NetworkVariableData4.Value, $"[{serverSideComponent.name}:{netVarName4}] Server side instance value ({serverSideComponent.NetworkVariableData4.Value}) " + - $"does not match the client side instance value ({clientSideComponent.NetworkVariableData4.Value})!"); - } - - - private bool ClientSpawnedNetworkObjects(List spawnedObjectList) - { - var clientSideNetworkObjects = s_GlobalNetworkObjects[m_ClientNetworkManagers[0].LocalClientId]; - - foreach (var spawnedObject in spawnedObjectList) - { - var serverSideSpawnedNetworkObject = spawnedObject.GetComponent(); - if (!clientSideNetworkObjects.ContainsKey(serverSideSpawnedNetworkObject.NetworkObjectId)) - { - return false; - } - } - return true; + Assert.IsTrue(authorityComponent.NetworkVariableData2.Value == nonAuthorityComponent.NetworkVariableData2.Value, $"[{authorityComponent.name}:{netVarName2}] Server side instance value ({authorityComponent.NetworkVariableData2.Value}) " + + $"does not match the client side instance value ({nonAuthorityComponent.NetworkVariableData2.Value})!"); + Assert.IsTrue(authorityComponent.NetworkVariableData3.Value == nonAuthorityComponent.NetworkVariableData3.Value, $"[{authorityComponent.name}:{netVarName3}] Server side instance value ({authorityComponent.NetworkVariableData3.Value}) " + + $"does not match the client side instance value ({nonAuthorityComponent.NetworkVariableData3.Value})!"); + Assert.IsTrue(authorityComponent.NetworkVariableData4.Value == nonAuthorityComponent.NetworkVariableData4.Value, $"[{authorityComponent.name}:{netVarName4}] Server side instance value ({authorityComponent.NetworkVariableData4.Value}) " + + $"does not match the client side instance value ({nonAuthorityComponent.NetworkVariableData4.Value})!"); } /// @@ -322,7 +279,8 @@ private bool ClientSpawnedNetworkObjects(List spawnedObjectList) [UnityTest] public IEnumerator NetworkBehaviourSynchronization() { - m_ServerNetworkManager.LogLevel = LogLevel.Normal; + var authority = GetAuthorityNetworkManager(); + authority.LogLevel = LogLevel.Normal; m_CurrentLogLevel = LogLevel.Normal; NetworkBehaviourSynchronizeFailureComponent.ResetBehaviour(); @@ -331,28 +289,29 @@ public IEnumerator NetworkBehaviourSynchronization() // Spawn 11 more NetworkObjects where there should be 4 of each failure type for (int i = 0; i < numberOfObjectsToSpawn; i++) { - var synchronizationObject = SpawnObject(m_SynchronizationPrefab, m_ServerNetworkManager); + var synchronizationObject = SpawnObject(m_SynchronizationPrefab, authority); var synchronizationBehaviour = synchronizationObject.GetComponent(); synchronizationBehaviour.AssignNextFailureType(); spawnedObjectList.Add(synchronizationObject); } // Now spawn and connect a client that will fail to spawn half of the NetworkObjects spawned - yield return CreateAndStartNewClient(); + var newClient = CreateNewClient(); + yield return StartClient(newClient); // Validate that when a NetworkBehaviour fails to synchronize and is skipped over it does not // impact the rest of the NetworkBehaviours. - var clientSideNetworkObjects = s_GlobalNetworkObjects[m_ClientNetworkManagers[0].LocalClientId]; - yield return WaitForConditionOrTimeOut(() => ClientSpawnedNetworkObjects(spawnedObjectList)); + var clientSideNetworkObjects = s_GlobalNetworkObjects[newClient.LocalClientId]; + yield return WaitForSpawnedOnAllOrTimeOut(clientSideNetworkObjects.Values); AssertOnTimeout($"Timed out waiting for newly joined client to spawn all NetworkObjects!"); foreach (var spawnedObject in spawnedObjectList) { - var serverSideSpawnedNetworkObject = spawnedObject.GetComponent(); - var clientSideObject = clientSideNetworkObjects[serverSideSpawnedNetworkObject.NetworkObjectId]; - var clientSideSpawnedNetworkObject = clientSideObject.GetComponent(); + var authorityObject = spawnedObject.GetComponent(); + var nonAuthorityObject = clientSideNetworkObjects[authorityObject.NetworkObjectId]; + var clientSideSpawnedNetworkObject = nonAuthorityObject.GetComponent(); - ValidateNetworkBehaviourWithNetworkVariables(serverSideSpawnedNetworkObject, clientSideSpawnedNetworkObject); + ValidateNetworkBehaviourWithNetworkVariables(authorityObject, clientSideSpawnedNetworkObject); } } @@ -362,12 +321,14 @@ public IEnumerator NetworkBehaviourSynchronization() [UnityTest] public IEnumerator NetworkBehaviourOnSynchronize() { - var serverSideInstance = SpawnObject(m_OnSynchronizePrefab, m_ServerNetworkManager).GetComponent(); + var authority = GetAuthorityNetworkManager(); + var serverSideInstance = SpawnObject(m_OnSynchronizePrefab, authority).GetComponent(); // Now spawn and connect a client that will have custom serialized data applied during the client synchronization process. - yield return CreateAndStartNewClient(); + var newClient = CreateNewClient(); + yield return StartClient(newClient); - var clientSideNetworkObjects = s_GlobalNetworkObjects[m_ClientNetworkManagers[0].LocalClientId]; + var clientSideNetworkObjects = s_GlobalNetworkObjects[newClient.LocalClientId]; var clientSideInstance = clientSideNetworkObjects[serverSideInstance.NetworkObjectId].GetComponent(); // Validate the values match @@ -386,13 +347,13 @@ public IEnumerator NetworkBehaviourOnSynchronize() /// internal class NetworkBehaviourWithNetworkVariables : NetworkBehaviour { - public static int ServerSpawnCount { get; internal set; } - public static readonly Dictionary ClientSpawnCount = new Dictionary(); + public static int AuthoritySpawnCount { get; internal set; } + public static readonly Dictionary NonAuthoritySpawnCount = new Dictionary(); public static void ResetSpawnCount() { - ServerSpawnCount = 0; - ClientSpawnCount.Clear(); + AuthoritySpawnCount = 0; + NonAuthoritySpawnCount.Clear(); } private const uint k_MinDataBlocks = 1; @@ -424,15 +385,15 @@ public override void OnNetworkSpawn() { if (IsServer) { - ServerSpawnCount++; + AuthoritySpawnCount++; } else { - if (!ClientSpawnCount.ContainsKey(NetworkManager.LocalClientId)) + if (!NonAuthoritySpawnCount.ContainsKey(NetworkManager.LocalClientId)) { - ClientSpawnCount.Add(NetworkManager.LocalClientId, 0); + NonAuthoritySpawnCount.Add(NetworkManager.LocalClientId, 0); } - ClientSpawnCount[NetworkManager.LocalClientId]++; + NonAuthoritySpawnCount[NetworkManager.LocalClientId]++; } base.OnNetworkSpawn(); @@ -496,8 +457,8 @@ public override void OnNetworkSpawn() internal class NetworkBehaviourSynchronizeFailureComponent : NetworkBehaviour { public static int NumberOfFailureTypes { get; internal set; } - public static int ServerSpawnCount { get; internal set; } - public static int ClientSpawnCount { get; internal set; } + public static int AuthoritySpawnCount { get; internal set; } + public static int NonAuthoritySpawnCount { get; internal set; } private static FailureTypes s_FailureType = FailureTypes.None; @@ -513,8 +474,8 @@ public enum FailureTypes public static void ResetBehaviour() { - ServerSpawnCount = 0; - ClientSpawnCount = 0; + AuthoritySpawnCount = 0; + NonAuthoritySpawnCount = 0; s_FailureType = FailureTypes.None; NumberOfFailureTypes = System.Enum.GetValues(typeof(FailureTypes)).Length; } @@ -595,6 +556,7 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade } case FailureTypes.DontReadAnything: { + Debug.Log("Don't read anything is being run"); // Don't read anything break; } @@ -641,14 +603,14 @@ private void Awake() public override void OnNetworkSpawn() { - if (IsServer) + if (HasAuthority) { - ServerSpawnCount++; + AuthoritySpawnCount++; m_MyCustomData.GenerateData((ushort)Random.Range(1, 512)); } else { - ClientSpawnCount++; + NonAuthoritySpawnCount++; } base.OnNetworkSpawn(); diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerSynchronizationTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerSynchronizationTests.cs new file mode 100644 index 0000000000..d135f29b91 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerSynchronizationTests.cs @@ -0,0 +1,88 @@ +using System.Collections; +using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.DAHost)] + internal class NetworkPrefabHandlerSynchronizationTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 1; + + public NetworkPrefabHandlerSynchronizationTests(HostOrServer hostOrServer) : base(hostOrServer) { } + + private GameObject m_ValidPrefab; + private GameObject m_ClientSideValidPrefab; + private GameObject m_ClientSideExceptionPrefab; + + protected override void OnServerAndClientsCreated() + { + m_ValidPrefab = CreateNetworkObjectPrefab("ValidPrefab"); + m_ClientSideValidPrefab = CreateNetworkObjectPrefab("ClientSideValidPrefab"); + m_ClientSideExceptionPrefab = CreateNetworkObjectPrefab("ClientSideExceptionPrefab"); + base.OnServerAndClientsCreated(); + } + + [UnityTest] + public IEnumerator NetworkPrefabHandlerSpawnAndSynchronizeTests() + { + var nonAuthority = GetNonAuthorityNetworkManager(); + + var networkObjectToSpawnOnClient = m_ClientSideValidPrefab.GetComponent(); + nonAuthority.PrefabHandler.AddHandler(m_ClientSideExceptionPrefab, new NetworkPrefabExceptionThrower()); + nonAuthority.PrefabHandler.AddHandler(m_ValidPrefab, new NetworkPrefabInstanceHandler(networkObjectToSpawnOnClient)); + + var authority = GetAuthorityNetworkManager(); + + // Spawn the invalid object first. + var exceptionObject = SpawnObject(m_ClientSideExceptionPrefab, authority).GetComponent(); + + // Check the invalid object spawns on the authority, expect an error from non-authority. + LogAssert.Expect(LogType.Exception, "Exception: exception while instantiating"); + LogAssert.Expect(LogType.Error, $"[Netcode] [GlobalObjectIdHash={exceptionObject.GlobalObjectIdHash}] Failed to spawn NetworkObject!"); + yield return WaitForConditionOrTimeOut(() => exceptionObject.IsSpawned); + AssertOnTimeout("Failed to spawn object on authority!"); + + // Now spawn a valid object + var validObject = SpawnObject(m_ValidPrefab, authority).GetComponent(); + + // The valid object should spawn as expected + yield return WaitForSpawnedOnAllOrTimeOut(validObject); + AssertOnTimeout("Failed to spawn valid prefab on all clients!"); + + // Create a new client and register the same PrefabHandlers on the client + var newClient = CreateNewClient(); + newClient.PrefabHandler.AddHandler(m_ClientSideExceptionPrefab, new NetworkPrefabExceptionThrower()); + newClient.PrefabHandler.AddHandler(m_ValidPrefab, new NetworkPrefabInstanceHandler(networkObjectToSpawnOnClient)); + + // Expect assertions fromt the new client + LogAssert.Expect(LogType.Exception, "Exception: exception while instantiating"); + LogAssert.Expect(LogType.Error, $"[Netcode] [GlobalObjectIdHash={exceptionObject.GlobalObjectIdHash}] Failed to spawn NetworkObject!"); + + // Start and synchronize the new client + yield return StartClient(newClient); + + // Validate the valid prefab spawned on all clients without issue + var expectedAuthorityHash = m_ValidPrefab.GetComponent().GlobalObjectIdHash; + var expectedNonAuthorityHash = m_ClientSideValidPrefab.GetComponent().GlobalObjectIdHash; + foreach (var networkManager in m_NetworkManagers) + { + Assert.True(networkManager.SpawnManager.SpawnedObjects.TryGetValue(validObject.NetworkObjectId, out NetworkObject spawnedObject), $"Client-{networkManager.LocalClientId} failed to spawn version of valid object!"); + + if (spawnedObject.HasAuthority) + { + Assert.That(spawnedObject.GlobalObjectIdHash, Is.EqualTo(expectedAuthorityHash), "NetworkObject spawned with unexpected GlobalObjectIdHash!"); + Assert.That(networkManager.SpawnManager.SpawnedObjects.ContainsKey(exceptionObject.NetworkObjectId), Is.True, "Authority missing spawned NetworkObject!"); + } + else + { + Assert.That(spawnedObject.GlobalObjectIdHash, Is.EqualTo(expectedNonAuthorityHash), "NetworkObject spawned with unexpected GlobalObjectIdHash!"); + Assert.That(networkManager.SpawnManager.SpawnedObjects.ContainsKey(exceptionObject.NetworkObjectId), Is.False, "Non authority should not have spawned exception object!"); + } + } + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerSynchronizationTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerSynchronizationTests.cs.meta new file mode 100644 index 0000000000..a0c417d419 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerSynchronizationTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 63f7e166459749bfbbf88e5b82ab917d +timeCreated: 1783026731 \ No newline at end of file From 7767c223ffabd170f7317a78fa6bc112ab0cfa2b Mon Sep 17 00:00:00 2001 From: Emma Date: Thu, 2 Jul 2026 19:04:48 -0400 Subject: [PATCH 2/3] Update NetcodeIntegrationTest --- .../Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs index f680b24338..11c092a8a9 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs @@ -1987,7 +1987,7 @@ protected IEnumerator WaitForSpawnedOnAllOrTimeOut(GameObject gameObject, Timeou /// The list of s to wait for. /// An optional to control the timeout period. If null, the default timeout is used. /// An for use in Unity coroutines. - protected IEnumerator WaitForSpawnedOnAllOrTimeOut(List networkObjects, TimeoutHelper timeOutHelper = null) + protected IEnumerator WaitForSpawnedOnAllOrTimeOut(ICollection networkObjects, TimeoutHelper timeOutHelper = null) { bool ValidateObjectsSpawnedOnAllClients(StringBuilder errorLog) { From 56654dbf3d2f7ed99ae03f09634f69dbad0a5944 Mon Sep 17 00:00:00 2001 From: Emma Date: Thu, 2 Jul 2026 19:05:51 -0400 Subject: [PATCH 3/3] Update CHANGELOG.md --- com.unity.netcode.gameobjects/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 0c5e47d1a0..5374068ba0 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -22,6 +22,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed +- Issue where a NullReferenceException was thrown when a non-authority failed to spawn a NetworkObject. (#4067) - Issue when FastBufferReader is attempting to read a string and all or a portion of the character count has already been read by user script, it could read a character length that results in a negative byte length which could result in an editor crash. (#4052) - Issue where NetworkRigidbodyBase was not applying rotation correctly when using Rigidbody2D. (#4012) - Issue where NetworkRigidbodyBase was always checking the 3D rigid body's interpolation mode when determining if it is kinematic and needs to put the rigid body to sleep and then switch to interpolation. (#4012)