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) 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; } diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestWithApproximation.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestWithApproximation.cs index 462fd74f76..8527d7b458 100644 --- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestWithApproximation.cs +++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/IntegrationTestWithApproximation.cs @@ -8,6 +8,26 @@ 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); + } + 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