From 770143849580b78667aaa18fb3e546737b15290f Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Mon, 14 Apr 2025 10:15:49 -0500 Subject: [PATCH 1/4] fix This resolves the issue where instantiating, spawning, and parenting a child NetworkObject during a to-be parent's OnNetworkSpawn or OnNetworkPostSpawn would not defer the parenting message properly if the parent had yet to be spawned. --- .../Runtime/Messaging/Messages/ParentSyncMessage.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs index abbe88802e..cbb6a974e4 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs @@ -88,6 +88,14 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int networkManager.DeferredMessageManager.DeferMessage(IDeferredNetworkMessageManager.TriggerType.OnSpawn, NetworkObjectId, reader, ref context); return false; } + + // If the target parent does not exist, then defer this message until it does. + if (LatestParent.HasValue && !networkManager.SpawnManager.SpawnedObjects.ContainsKey(LatestParent.Value)) + { + networkManager.DeferredMessageManager.DeferMessage(IDeferredNetworkMessageManager.TriggerType.OnSpawn, LatestParent.Value, reader, ref context); + return false; + } + return true; } From 13ca162b52d8fe1c52ab06c560a835752c3dfeb8 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Mon, 14 Apr 2025 10:16:47 -0500 Subject: [PATCH 2/4] test test that validates this fix. updates to bring the logging of Vector3 values into v1.x. --- .../IntegrationTestWithApproximation.cs | 10 ++ .../Runtime/ParentingDuringSpawnTests.cs | 160 ++++++++++++++++++ .../Runtime/ParentingDuringSpawnTests.cs.meta | 11 ++ .../NestedNetworkTransformTests.cs | 4 - 4 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/ParentingDuringSpawnTests.cs create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/ParentingDuringSpawnTests.cs.meta diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestWithApproximation.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestWithApproximation.cs index 462fd74f76..bdddcd78ee 100644 --- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestWithApproximation.cs +++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestWithApproximation.cs @@ -8,6 +8,16 @@ public abstract class IntegrationTestWithApproximation : NetcodeIntegrationTest { private const float k_AproximateDeltaVariance = 0.01f; + protected string GetVector3Values(ref Vector3 vector3) + { + return $"({vector3.x:F6},{vector3.y:F6},{vector3.z:F6})"; + } + + protected string GetVector3Values(Vector3 vector3) + { + return GetVector3Values(ref vector3); + } + protected virtual float GetDeltaVarianceThreshold() { return k_AproximateDeltaVariance; diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/ParentingDuringSpawnTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/ParentingDuringSpawnTests.cs new file mode 100644 index 0000000000..c6dcf6cf1a --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/ParentingDuringSpawnTests.cs @@ -0,0 +1,160 @@ +using System.Collections; +using System.Collections.Generic; +using System.Text; +using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + [TestFixture(NetworkSpawnTypes.OnNetworkSpawn)] + [TestFixture(NetworkSpawnTypes.OnNetworkPostSpawn)] + internal class ParentingDuringSpawnTests : IntegrationTestWithApproximation + { + protected override int NumberOfClients => 2; + + public enum NetworkSpawnTypes + { + OnNetworkSpawn, + OnNetworkPostSpawn, + } + + private NetworkSpawnTypes m_NetworkSpawnType; + + private GameObject m_ParentPrefab; + private GameObject m_ChildPrefab; + private NetworkObject m_AuthorityInstance; + private List m_NetworkManagers = new List(); + private StringBuilder m_Errors = new StringBuilder(); + + public class ParentDuringSpawnBehaviour : NetworkBehaviour + { + public GameObject ChildToSpawn; + + public NetworkSpawnTypes NetworkSpawnType; + + public Transform ChildSpawnPoint; + + private void SpawnThenParent() + { + var child = NetworkObject.InstantiateAndSpawn(ChildToSpawn, NetworkManager, position: ChildSpawnPoint.position, rotation: ChildSpawnPoint.rotation); + if (!child.TrySetParent(NetworkObject)) + { + var errorMessage = $"[{ChildToSpawn}] Failed to parent child {child.name} under parent {gameObject.name}!"; + Debug.LogError(errorMessage); + } + } + + public override void OnNetworkSpawn() + { + if (IsServer && NetworkSpawnType == NetworkSpawnTypes.OnNetworkSpawn) + { + SpawnThenParent(); + } + + base.OnNetworkSpawn(); + } + + protected override void OnNetworkPostSpawn() + { + if (IsServer && NetworkSpawnType == NetworkSpawnTypes.OnNetworkPostSpawn) + { + SpawnThenParent(); + } + base.OnNetworkPostSpawn(); + } + } + + public ParentingDuringSpawnTests(NetworkSpawnTypes networkSpawnType) : base() + { + m_NetworkSpawnType = networkSpawnType; + } + + protected override void OnServerAndClientsCreated() + { + m_ParentPrefab = CreateNetworkObjectPrefab("Parent"); + m_ChildPrefab = CreateNetworkObjectPrefab("Child"); + var parentComponet = m_ParentPrefab.AddComponent(); + parentComponet.ChildToSpawn = m_ChildPrefab; + var spawnPoint = new GameObject(); + parentComponet.ChildSpawnPoint = spawnPoint.transform; + parentComponet.ChildSpawnPoint.position = GetRandomVector3(-5.0f, 5.0f); + var rotation = parentComponet.ChildSpawnPoint.rotation; + rotation.eulerAngles = GetRandomVector3(-180.0f, 180.0f); + parentComponet.ChildSpawnPoint.rotation = rotation; + base.OnServerAndClientsCreated(); + } + + private bool NonAuthorityInstancesSpawnedParent() + { + foreach (var networkManager in m_NetworkManagers) + { + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_AuthorityInstance.NetworkObjectId)) + { + return false; + } + } + return true; + } + + private bool NonAuthorityInstancesParentedChild() + { + m_Errors.Clear(); + if (m_AuthorityInstance.transform.childCount == 0) + { + return false; + } + var authorityChildObject = m_AuthorityInstance.transform.GetChild(0).GetComponent(); + + foreach (var networkManager in m_NetworkManagers) + { + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(authorityChildObject.NetworkObjectId)) + { + m_Errors.AppendLine($"{networkManager.name} has not spawned the child {authorityChildObject.name}!"); + return false; + } + var childObject = networkManager.SpawnManager.SpawnedObjects[authorityChildObject.NetworkObjectId]; + + if (childObject.transform.parent == null) + { + m_Errors.AppendLine($"{childObject.name} does not have a parent!"); + return false; + } + + if (!Approximately(authorityChildObject.transform.position, childObject.transform.position)) + { + m_Errors.AppendLine($"{childObject.name} position {GetVector3Values(childObject.transform.position)} does " + + $"not match the authority's position {GetVector3Values(authorityChildObject.transform.position)}!"); + return false; + } + + if (!Approximately(authorityChildObject.transform.rotation, childObject.transform.rotation)) + { + m_Errors.AppendLine($"{childObject.name} rotation {GetVector3Values(childObject.transform.rotation.eulerAngles)} does " + + $"not match the authority's position {GetVector3Values(authorityChildObject.transform.rotation.eulerAngles)}!"); + return false; + } + } + return true; + } + + [UnityTest] + public IEnumerator ParentDuringSpawn() + { + m_NetworkManagers.Clear(); + var authorityNetworkManager = m_ServerNetworkManager; + + m_NetworkManagers.AddRange(m_ClientNetworkManagers); + m_NetworkManagers.Add(m_ServerNetworkManager); + + m_AuthorityInstance = SpawnObject(m_ParentPrefab, authorityNetworkManager).GetComponent(); + + yield return WaitForConditionOrTimeOut(NonAuthorityInstancesSpawnedParent); + AssertOnTimeout($"Not all clients spawned the parent {nameof(NetworkObject)}!"); + + yield return WaitForConditionOrTimeOut(NonAuthorityInstancesParentedChild); + AssertOnTimeout($"Non-Authority instance had a mismatched value: \n {m_Errors}"); + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/ParentingDuringSpawnTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/ParentingDuringSpawnTests.cs.meta new file mode 100644 index 0000000000..0c1096a7e5 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/ParentingDuringSpawnTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3ba96c894d9ac474a8d63b9db28c3ec7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/testproject/Assets/Tests/Runtime/NetworkTransform/NestedNetworkTransformTests.cs b/testproject/Assets/Tests/Runtime/NetworkTransform/NestedNetworkTransformTests.cs index 77d8403d57..ac1a6aa160 100644 --- a/testproject/Assets/Tests/Runtime/NetworkTransform/NestedNetworkTransformTests.cs +++ b/testproject/Assets/Tests/Runtime/NetworkTransform/NestedNetworkTransformTests.cs @@ -181,10 +181,6 @@ protected override float GetDeltaVarianceThreshold() private StringBuilder m_ValidationErrors; - private string GetVector3Values(ref Vector3 vector3) - { - return $"({vector3.x:F6},{vector3.y:F6},{vector3.z:F6})"; - } /// /// Validates all transform instance values match the authority's From dd5dc7b61cf00e362da22cd37db1e03b93ab0303 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Mon, 14 Apr 2025 10:20:47 -0500 Subject: [PATCH 3/4] update Adding changelog entry. --- 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 3708d1b0c2..ea9b77f778 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -16,6 +16,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed +- Fixed issue where during a `NetworkObject`'s spawn if you instantiated, spawned, and parented another network prefab under the currently spawning `NetworkObject` the parenting message would not properly defer until the parent `NetworkObject` was spawned. (#3403) - Fixed issue where in-scene placed `NetworkObjects` could fail to synchronize its transform properly (especially without a `NetworkTransform`) if their parenting changes from the default when the scene is loaded and if the same scene remains loaded between network sessions while the parenting is completely different from the original hierarchy. (#3388) - Fixed an issue in `UnityTransport` where the transport would accept sends on invalid connections, leading to a useless memory allocation and confusing error message. (#3383) - Fixed issue where `NetworkAnimator` would log an error if there was no destination transition information. (#3384) From 8ddeb1616b2f1c8f0371e000a505eaddbc91de48 Mon Sep 17 00:00:00 2001 From: NoelStephensUnity Date: Mon, 14 Apr 2025 10:29:20 -0500 Subject: [PATCH 4/4] style Adding XML API for time being to avoid PVP issues (until we sort through the issue with v1.x tests being public). --- .../Runtime/IntegrationTestWithApproximation.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestWithApproximation.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestWithApproximation.cs index bdddcd78ee..8527d7b458 100644 --- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestWithApproximation.cs +++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestWithApproximation.cs @@ -8,11 +8,21 @@ public abstract class IntegrationTestWithApproximation : NetcodeIntegrationTest { private const float k_AproximateDeltaVariance = 0.01f; + /// + /// Returns a as a formatted string. + /// + /// reference of to return as a formatted string. + /// protected string GetVector3Values(ref Vector3 vector3) { return $"({vector3.x:F6},{vector3.y:F6},{vector3.z:F6})"; } + /// + /// Returns a as a formatted string. + /// + /// to return as a formatted string. + /// protected string GetVector3Values(Vector3 vector3) { return GetVector3Values(ref vector3);