diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 93ed4d1732..695e000a45 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -13,6 +13,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. (#3401) ### Changed diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs index 33caa7d114..c2965d4b31 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ParentSyncMessage.cs @@ -6,7 +6,7 @@ internal struct ParentSyncMessage : INetworkMessage { public int Version => 0; - private const string k_Name = "DestroyObjectMessage"; + private const string k_Name = "ParentSyncMessage"; public ulong NetworkObjectId; @@ -87,12 +87,20 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int reader.ReadValueSafe(out Rotation); reader.ReadValueSafe(out Scale); - // If the target NetworkObject does not exist =or= the target latest parent does not exist then defer the message - if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(NetworkObjectId) || (LatestParent.HasValue && !networkManager.SpawnManager.SpawnedObjects.ContainsKey(LatestParent.Value))) + // If the target NetworkObject does not exist then defer this message until it does. + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(NetworkObjectId)) { networkManager.DeferredMessageManager.DeferMessage(IDeferredNetworkMessageManager.TriggerType.OnSpawn, NetworkObjectId, reader, ref context, k_Name); 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, k_Name); + return false; + } + return true; } 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..9623c997a8 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/ParentingDuringSpawnTests.cs @@ -0,0 +1,165 @@ +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(NetworkTopologyTypes.ClientServer, NetworkSpawnTypes.OnNetworkSpawn)] + [TestFixture(NetworkTopologyTypes.ClientServer, NetworkSpawnTypes.OnNetworkPostSpawn)] + [TestFixture(NetworkTopologyTypes.DistributedAuthority, NetworkSpawnTypes.OnNetworkSpawn)] + [TestFixture(NetworkTopologyTypes.DistributedAuthority, 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 (HasAuthority && NetworkSpawnType == NetworkSpawnTypes.OnNetworkSpawn) + { + SpawnThenParent(); + } + + base.OnNetworkSpawn(); + } + + protected override void OnNetworkPostSpawn() + { + if (HasAuthority && NetworkSpawnType == NetworkSpawnTypes.OnNetworkPostSpawn) + { + SpawnThenParent(); + } + base.OnNetworkPostSpawn(); + } + } + + public ParentingDuringSpawnTests(NetworkTopologyTypes networkTopology, NetworkSpawnTypes networkSpawnType) : base(networkTopology) + { + 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_DistributedAuthority ? m_ClientNetworkManagers[0] : m_ServerNetworkManager; + + m_NetworkManagers.AddRange(m_ClientNetworkManagers); + if (!UseCMBService()) + { + 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..abf773ea1c --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/ParentingDuringSpawnTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1d708e3d4d5f1794aa0a924eaa9445d6 \ No newline at end of file