diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index a226fda36e..49f6f55cd9 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -10,6 +10,8 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Added +- Added the ability to spawn prefabs with nested NetworkObjects + ### Fixed - Fixed issue where `NetworkClient.OwnedObjects` was not returning any owned objects due to the `NetworkClient.IsConnected` not being properly set. (#2631) diff --git a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs index ccbe7756bf..4f1baa3865 100644 --- a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs @@ -614,6 +614,11 @@ internal void HandleConnectionApproval(ulong ownerClientId, NetworkManager.Conne ownerClientId, destroyWithScene: false); + foreach (var dependingNetworkObject in networkObject.DependingNetworkObjects) + { + NetworkManager.SpawnManager.SpawnNetworkObjectLocally(dependingNetworkObject, NetworkManager.SpawnManager.GetNetworkObjectId(), false, false, ownerClientId, false); + } + client.AssignPlayerObject(ref networkObject); } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 81542e3318..b4dc1ca918 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -3,6 +3,9 @@ using System.Runtime.CompilerServices; using UnityEngine; using UnityEngine.SceneManagement; +#if UNITY_EDITOR +using UnityEditor; +#endif namespace Unity.Netcode { @@ -17,6 +20,32 @@ public sealed class NetworkObject : MonoBehaviour [SerializeField] internal uint GlobalObjectIdHash; + [HideInInspector] + [SerializeField] + private NetworkObject m_DependentNetworkObject = null; + + [HideInInspector] + [SerializeField] + private List m_DependingNetworkObjects = new List(); + + /// + /// Whether this NetworkObject is dependent on another NetworkObject. + /// + public bool IsDependent => m_DependentNetworkObject != null; + + /// + /// Gets the NetworkObject that this NetworkObject is dependent on. + /// When the dependent NetworkObject is despawned, this NetworkObject will be despawned as well. + /// + public NetworkObject DependentNetworkObject { get => m_DependentNetworkObject; internal set => m_DependentNetworkObject = value; } + + /// + /// Gets the NetworkObjects that depend on this NetworkObject. + /// When the this NetworkObject is despawned, all the depending NetworkObjects will be despawned as well. + /// Child NetworkObjects in a prefab are dependent on the root NetworkObject. + /// + public List DependingNetworkObjects { get => new List(m_DependingNetworkObjects); internal set => m_DependingNetworkObjects = value; } + /// /// Gets the Prefab Hash Id of this object if the object is registerd as a prefab otherwise it returns 0 /// @@ -43,6 +72,8 @@ public uint PrefabIdHash private void OnValidate() { GenerateGlobalObjectIdHash(); + + CheckDependency(); } internal void GenerateGlobalObjectIdHash() @@ -62,6 +93,37 @@ internal void GenerateGlobalObjectIdHash() var globalObjectIdString = UnityEditor.GlobalObjectId.GetGlobalObjectIdSlow(this).ToString(); GlobalObjectIdHash = XXHash.Hash32(globalObjectIdString); } + + internal void CheckDependency() + { + if (PrefabUtility.IsPartOfPrefabAsset(this)) + { + NetworkObject parent = transform.parent != null + ? transform.parent.GetComponentInParent(true) + : null; + + if (parent == null) + { + // Find nested/dependent NetworkObjects + m_DependentNetworkObject = null; + GetComponentsInChildren(m_DependingNetworkObjects); + m_DependingNetworkObjects.Remove(this); + + // Have the parent register its children as dependent + foreach (var obj in m_DependingNetworkObjects) + { + obj.m_DependentNetworkObject = this; + obj.m_DependingNetworkObjects.Clear(); + } + } + } + else + { + // In-scene placed NetworkObjects cannot be dependent + m_DependentNetworkObject = null; + m_DependingNetworkObjects.Clear(); + } + } #endif // UNITY_EDITOR /// @@ -582,8 +644,18 @@ private void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool play throw new NotServerException($"Only server can spawn {nameof(NetworkObject)}s"); } + if (IsDependent) + { + throw new InvalidOperationException($"Cannot spawn {nameof(NetworkObject)}s that are dependent on other {nameof(NetworkObject)}s"); + } + NetworkManager.SpawnManager.SpawnNetworkObjectLocally(this, NetworkManager.SpawnManager.GetNetworkObjectId(), IsSceneObject.HasValue && IsSceneObject.Value, playerObject, ownerClientId, destroyWithScene); + for (int i = 0; i < m_DependingNetworkObjects.Count; i++) + { + NetworkManager.SpawnManager.SpawnNetworkObjectLocally(m_DependingNetworkObjects[i], NetworkManager.SpawnManager.GetNetworkObjectId(), IsSceneObject.HasValue && IsSceneObject.Value, false, ownerClientId, destroyWithScene); + } + for (int i = 0; i < NetworkManager.ConnectedClientsList.Count; i++) { if (Observers.Contains(NetworkManager.ConnectedClientsList[i].ClientId)) @@ -957,11 +1029,11 @@ internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpa return false; } - // Handle the first in-scene placed NetworkObject parenting scenarios. Once the m_LatestParent + // Handles scenarios where parentage has been predetermined. Once the m_LatestParent // has been set, this will not be entered into again (i.e. the later code will be invoked and // users will get notifications when the parent changes). var isInScenePlaced = IsSceneObject.HasValue && IsSceneObject.Value; - if (transform.parent != null && !removeParent && !m_LatestParent.HasValue && isInScenePlaced) + if (transform.parent != null && !removeParent && !m_LatestParent.HasValue && (isInScenePlaced || IsDependent)) { var parentNetworkObject = transform.parent.GetComponent(); @@ -976,8 +1048,9 @@ internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpa m_CachedWorldPositionStays = false; return true; } - else // If the parent still isn't spawned add this to the orphaned children and return false - if (!parentNetworkObject.IsSpawned) + // If the parent still isn't spawned add this to the orphaned children and return false. Should only occur + // with in-scene placed objects. + else if (!parentNetworkObject.IsSpawned) { OrphanChildren.Add(this); return false; @@ -1198,6 +1271,7 @@ internal NetworkBehaviour GetNetworkBehaviourAtOrderIndex(ushort index) { NetworkLog.LogError($"{nameof(NetworkBehaviour)} index {index} was out of bounds for {name}. NetworkBehaviours must be the same, and in the same order, between server and client."); } + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { var currentKnownChildren = new System.Text.StringBuilder(); @@ -1210,6 +1284,7 @@ internal NetworkBehaviour GetNetworkBehaviourAtOrderIndex(ushort index) } NetworkLog.LogInfo(currentKnownChildren.ToString()); } + return null; } @@ -1268,6 +1343,34 @@ public bool DestroyWithScene set => ByteUtility.SetBit(ref m_BitField, 6, value); } + public struct DependingSceneObject : INetworkSerializeByMemcpy + { + private byte m_BitField; + + public ulong OwnerClientId; + public ulong NetworkObjectId; + public ulong ParentObjectId; + public ulong? LatestParent; + + public bool IsSpawned + { + get => ByteUtility.GetBit(m_BitField, 0); + set => ByteUtility.SetBit(ref m_BitField, 0, value); + } + public bool HasParent + { + get => ByteUtility.GetBit(m_BitField, 1); + set => ByteUtility.SetBit(ref m_BitField, 1, value); + } + public bool IsLatestParentSet + { + get => ByteUtility.GetBit(m_BitField, 2); + set => ByteUtility.SetBit(ref m_BitField, 2, value); + } + } + + public DependingSceneObject[] DependingObjects; + //If(Metadata.HasParent) public ulong ParentObjectId; @@ -1308,15 +1411,24 @@ public void Serialize(FastBufferWriter writer) } } + int dependingCount = DependingObjects?.Length ?? 0; + writer.WriteValueSafe(dependingCount); + var writeSize = 0; - writeSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0; - writeSize += FastBufferWriter.GetWriteSize(); + writeSize += dependingCount * FastBufferWriter.GetWriteSize(); // Each Depending Object + writeSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0; // Transform + writeSize += FastBufferWriter.GetWriteSize(); // NetworkSceneHandle if (!writer.TryBeginWrite(writeSize)) { throw new OverflowException("Could not serialize SceneObject: Out of buffer space."); } + for (int i = 0; i < dependingCount; i++) + { + writer.WriteValue(DependingObjects[i]); + } + if (HasTransform) { writer.WriteValue(Transform); @@ -1329,6 +1441,14 @@ public void Serialize(FastBufferWriter writer) // Synchronize NetworkVariables and NetworkBehaviours var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer)); OwnerObject.SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId); + + for (int i = 0; i < dependingCount; i++) + { + if (DependingObjects[i].IsSpawned) + { + OwnerObject.m_DependingNetworkObjects[i].SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId); + } + } } public void Deserialize(FastBufferReader reader) @@ -1348,9 +1468,12 @@ public void Deserialize(FastBufferReader reader) } } + reader.ReadValueSafe(out int dependingCount); + var readSize = 0; - readSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0; - readSize += FastBufferWriter.GetWriteSize(); + readSize += dependingCount * FastBufferWriter.GetWriteSize(); // Each Depending Object + readSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0; // Transform + readSize += FastBufferWriter.GetWriteSize(); // NetworkSceneHandle // Try to begin reading the remaining bytes if (!reader.TryBeginRead(readSize)) @@ -1358,6 +1481,12 @@ public void Deserialize(FastBufferReader reader) throw new OverflowException("Could not deserialize SceneObject: Reading past the end of the buffer"); } + DependingObjects = new DependingSceneObject[dependingCount]; + for (int i = 0; i < dependingCount; i++) + { + reader.ReadValue(out DependingObjects[i]); + } + if (HasTransform) { reader.ReadValue(out Transform); @@ -1463,31 +1592,33 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId) TargetClientId = targetClientId }; - NetworkObject parentNetworkObject = null; - - if (!AlwaysReplicateAsRoot && transform.parent != null) { - parentNetworkObject = transform.parent.GetComponent(); - // In-scene placed NetworkObjects parented under GameObjects with no NetworkObject - // should set the has parent flag and preserve the world position stays value - if (parentNetworkObject == null && obj.IsSceneObject) + NetworkObject parentNetworkObject = null; + + if (!AlwaysReplicateAsRoot && transform.parent != null) { - obj.HasParent = true; - obj.WorldPositionStays = m_CachedWorldPositionStays; + parentNetworkObject = transform.parent.GetComponent(); + // In-scene placed NetworkObjects parented under GameObjects with no NetworkObject + // should set the has parent flag and preserve the world position stays value + if (parentNetworkObject == null && obj.IsSceneObject) + { + obj.HasParent = true; + obj.WorldPositionStays = m_CachedWorldPositionStays; + } } - } - if (parentNetworkObject != null) - { - obj.HasParent = true; - obj.ParentObjectId = parentNetworkObject.NetworkObjectId; - obj.WorldPositionStays = m_CachedWorldPositionStays; - var latestParent = GetNetworkParenting(); - var isLatestParentSet = latestParent != null && latestParent.HasValue; - obj.IsLatestParentSet = isLatestParentSet; - if (isLatestParentSet) + if (parentNetworkObject != null) { - obj.LatestParent = latestParent.Value; + obj.HasParent = true; + obj.ParentObjectId = parentNetworkObject.NetworkObjectId; + obj.WorldPositionStays = m_CachedWorldPositionStays; + var latestParent = GetNetworkParenting(); + var isLatestParentSet = latestParent != null && latestParent.HasValue; + obj.IsLatestParentSet = isLatestParentSet; + if (isLatestParentSet) + { + obj.LatestParent = latestParent.Value; + } } } @@ -1528,6 +1659,57 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId) }; } + SceneObject.DependingSceneObject[] DependingObjects = new SceneObject.DependingSceneObject[m_DependingNetworkObjects.Count]; + for (int i = 0; i < m_DependingNetworkObjects.Count; i++) + { + if (m_DependingNetworkObjects[i] == null || !m_DependingNetworkObjects[i].IsSpawned) + { + DependingObjects[i] = new SceneObject.DependingSceneObject + { + IsSpawned = false, + }; + } + else + { + DependingObjects[i] = new SceneObject.DependingSceneObject + { + IsSpawned = true, + NetworkObjectId = m_DependingNetworkObjects[i].NetworkObjectId, + OwnerClientId = m_DependingNetworkObjects[i].OwnerClientId, + }; + + { // Set parentage info + NetworkObject parentNetworkObject = null; + + if (!m_DependingNetworkObjects[i].AlwaysReplicateAsRoot && m_DependingNetworkObjects[i].transform.parent != null) + { + parentNetworkObject = m_DependingNetworkObjects[i].transform.parent.GetComponent(); + + // If a dependent NetworkObject has a parent but not a network parent, the + // HasParent flag needs to be set. + if (parentNetworkObject == null) + { + DependingObjects[i].HasParent = true; + } + } + + if (parentNetworkObject != null) + { + DependingObjects[i].HasParent = true; + DependingObjects[i].ParentObjectId = parentNetworkObject.NetworkObjectId; + var latestParent = m_DependingNetworkObjects[i].GetNetworkParenting(); + var isLatestParentSet = latestParent != null && latestParent.HasValue; + DependingObjects[i].IsLatestParentSet = isLatestParentSet; + if (isLatestParentSet) + { + DependingObjects[i].LatestParent = latestParent.Value; + } + } + } + } + } + obj.DependingObjects = DependingObjects; + return obj; } @@ -1567,6 +1749,7 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf return null; } + // 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 = sceneObject.OwnerClientId; @@ -1578,6 +1761,23 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf // Spawn the NetworkObject networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, sceneObject.DestroyWithScene); + + // Repeat Steps for depending NetworkObjects + for (int i = 0; i < networkObject.m_DependingNetworkObjects.Count; i++) + { + var dependingObj = networkObject.m_DependingNetworkObjects[i]; + var dependingObjData = sceneObject.DependingObjects[i]; + + if (dependingObjData.IsSpawned) + { + dependingObj.OwnerClientId = sceneObject.OwnerClientId; + + dependingObj.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId); + + networkManager.SpawnManager.SpawnNetworkObjectLocally(dependingObj, dependingObjData.NetworkObjectId, sceneObject.IsSceneObject, false, dependingObjData.OwnerClientId, sceneObject.DestroyWithScene); + } + } + return networkObject; } diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs index 648573ed3a..d0f6ddb53c 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs @@ -45,6 +45,9 @@ public void Serialize(FastBufferWriter writer, int targetVersion) // Serialize NetworkVariable data foreach (var sobj in SpawnedObjectsList) { + // Depending Network Objects will be spawned by their Dependent Network Object + if (sobj.DependentNetworkObject != null) { continue; } + if (sobj.CheckObjectVisibility == null || sobj.CheckObjectVisibility(OwnerClientId)) { sobj.Observers.Add(OwnerClientId); diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs index 351e49dc0c..a88d1041af 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs @@ -250,7 +250,7 @@ internal void AddSpawnedNetworkObjects() m_NetworkObjectsSync.Clear(); foreach (var sobj in m_NetworkManager.SpawnManager.SpawnedObjectsList) { - if (sobj.Observers.Contains(TargetClientId)) + if (sobj.Observers.Contains(TargetClientId) && !sobj.IsDependent) { m_NetworkObjectsSync.Add(sobj); } diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index c7ddc60a4a..6e9158e684 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -473,6 +473,46 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO { UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); } + + // Hook up NetworkObjects that depend on this NetworkObject. Usually used for nested NetworkObjects in prefabs, + if (sceneObject.DependingObjects != null) + { + var DependingNetworkObjects = networkObject.DependingNetworkObjects; + for (int i = 0; i < sceneObject.DependingObjects.Length; i++) + { + var childData = sceneObject.DependingObjects[i]; + var childNetworkObject = DependingNetworkObjects[i]; + + if (childData.IsSpawned) + { + childNetworkObject.DestroyWithScene = sceneObject.DestroyWithScene; + childNetworkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle; + + // If a dependent NetworkObject does not have the HasParent flag set, it needs to be unparented + if (!sceneObject.HasParent && childNetworkObject.transform.parent != null) + { + childNetworkObject.ApplyNetworkParenting(true, true); + } + + if (childData.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 (childData.IsLatestParentSet) + { + parentId = childData.HasParent ? childData.ParentObjectId : default; + } + childNetworkObject.SetNetworkParenting(parentId, true); + } + } + else + { + // Remove unspawned child NetworkObjects + GameObject.Destroy(networkObject.DependingNetworkObjects[i].gameObject); + } + } + } } return networkObject; } @@ -490,15 +530,6 @@ internal void SpawnNetworkObjectLocally(NetworkObject networkObject, ulong netwo throw new SpawnStateException("Object is already spawned"); } - if (!sceneObject) - { - 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!"); - } - } - SpawnNetworkObjectLocallyCommon(networkObject, networkId, sceneObject, playerObject, ownerClientId, destroyWithScene); } @@ -820,6 +851,12 @@ internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObjec // and only attempt to remove the child's parent on the server-side if (!NetworkManager.ShutdownInProgress && NetworkManager.IsServer) { + // Destroy GameObjects that depend on the despawned GameObject + foreach (var dependingNetworkObject in networkObject.DependingNetworkObjects) + { + dependingNetworkObject.Despawn(); + } + // Move child NetworkObjects to the root when parent NetworkObject is destroyed foreach (var spawnedNetObj in SpawnedObjectsList) { diff --git a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs index e98590d5eb..baaff77add 100644 --- a/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs +++ b/com.unity.netcode.gameobjects/TestHelpers/Runtime/NetcodeIntegrationTestHelpers.cs @@ -485,6 +485,20 @@ public static void RegisterNetcodeIntegrationTest(bool registered) } + private static void TryAddObjectNameIdentifier(NetworkObject networkObject) + { + // To avoid issues with integration tests that forget to clean up, + // this feature only works with NetcodeIntegrationTest derived classes + if (IsNetcodeIntegrationTestRunning) + { + if (networkObject.GetComponent() == null && networkObject.GetComponentInChildren() == null) + { + // Add the object identifier component + networkObject.gameObject.AddComponent(); + } + } + } + /// /// Normally we would only allow player prefabs to be set to a prefab. Not runtime created objects. /// In order to prevent having a Resource folder full of a TON of prefabs that we have to maintain, @@ -511,16 +525,7 @@ public static void MakeNetworkObjectTestPrefab(NetworkObject networkObject, uint // Prevent object from being snapped up as a scene object networkObject.IsSceneObject = false; - // To avoid issues with integration tests that forget to clean up, - // this feature only works with NetcodeIntegrationTest derived classes - if (IsNetcodeIntegrationTestRunning) - { - if (networkObject.GetComponent() == null && networkObject.GetComponentInChildren() == null) - { - // Add the object identifier component - networkObject.gameObject.AddComponent(); - } - } + TryAddObjectNameIdentifier(networkObject); } public static GameObject CreateNetworkObjectPrefab(string baseName, NetworkManager server, params NetworkManager[] clients) @@ -553,6 +558,27 @@ void AddNetworkPrefab(NetworkConfig config, NetworkPrefab prefab) return gameObject; } + public static GameObject AddNetworkObjectChildToPrefab(NetworkObject prefab, string baseName) + { + var gameObject = new GameObject + { + name = baseName + }; + gameObject.transform.parent = prefab.transform; + var networkObject = gameObject.AddComponent(); + + + var currentDependingNetworkObject = prefab.DependingNetworkObjects; + currentDependingNetworkObject.Add(networkObject); + prefab.DependingNetworkObjects = currentDependingNetworkObject; + + networkObject.DependentNetworkObject = prefab; + networkObject.IsSceneObject = false; + + TryAddObjectNameIdentifier(networkObject); + return gameObject; + } + // We use GameObject instead of SceneObject to be able to keep hierarchy public static void MarkAsSceneObjectRoot(GameObject networkObjectRoot, NetworkManager server, NetworkManager[] clients) { diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectDependencyTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectDependencyTests.cs new file mode 100644 index 0000000000..9ac82154de --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectDependencyTests.cs @@ -0,0 +1,146 @@ +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; +using Object = UnityEngine.Object; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// Tests ensuring that dependent s are functioning properly. Expected behavior: + /// - + /// + public class NetworkObjectDependencyTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 1; + + private GameObject m_PrefabToSpawn; + + protected override void OnCreatePlayerPrefab() + { + NetworkObject playerNetworkObject = m_PlayerPrefab.GetComponent(); + GameObject childObject = NetcodeIntegrationTestHelpers.AddNetworkObjectChildToPrefab(playerNetworkObject, "child"); + } + + protected override void OnServerAndClientsCreated() + { + m_PrefabToSpawn = CreateNetworkObjectPrefab("PrefabWithChildNetworkObject"); + NetcodeIntegrationTestHelpers.AddNetworkObjectChildToPrefab(m_PrefabToSpawn.GetComponent(), "child"); + } + + protected override void OnNewClientCreated(NetworkManager networkManager) + { + var networkPrefab = new NetworkPrefab() { Prefab = m_PrefabToSpawn }; + networkManager.NetworkConfig.Prefabs.Add(networkPrefab); + } + + + /// + /// Tests that depending on a player objects will be synchronized. + /// + [UnityTest] + public IEnumerator TestPlayerDependingObjects() + { + // This is the *SERVER VERSION* of the *CLIENT PLAYER* + var serverClientPlayerResult = new NetcodeIntegrationTestHelpers.ResultWrapper(); + yield return NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation(x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId, m_ServerNetworkManager, serverClientPlayerResult); + + // This is the *CLIENT VERSION* of the *CLIENT PLAYER* + var clientClientPlayerResult = new NetcodeIntegrationTestHelpers.ResultWrapper(); + yield return NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation(x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId, m_ClientNetworkManagers[0], clientClientPlayerResult); + + Assert.IsNotNull(serverClientPlayerResult.Result.gameObject); + Assert.IsNotNull(clientClientPlayerResult.Result.gameObject); + + var serverClientPlayerChild = serverClientPlayerResult.Result.transform.GetChild(0)?.GetComponent(); + var clientClientPlayerChild = clientClientPlayerResult.Result.transform.GetChild(0)?.GetComponent(); + + Assert.IsNotNull(serverClientPlayerChild); + Assert.IsNotNull(clientClientPlayerChild); + + Assert.IsTrue(serverClientPlayerChild.NetworkObjectId == clientClientPlayerChild.NetworkObjectId); // They should have the same NetworkObjectId + Assert.IsTrue(serverClientPlayerChild.NetworkObjectId > default(ulong)); // and that id should have been set + } + + /// + /// Tests that depending s can be reparented + /// and that the reparenting will be synchronized to late-joining clients. + /// + [UnityTest] + public IEnumerator TestDependingObjectReparenting() + { + var serverDependingInstance = SpawnObject(m_PrefabToSpawn, m_ServerNetworkManager).transform.GetChild(0)?.GetComponent(); + Assert.IsNotNull(serverDependingInstance); // Sanity check + + yield return NetcodeIntegrationTestHelpers.WaitForMessageOfTypeHandled(m_ClientNetworkManagers[0]); + + serverDependingInstance.transform.parent = null; + + yield return NetcodeIntegrationTestHelpers.WaitForMessageOfTypeHandled(m_ClientNetworkManagers[0]); + + var clientDepending1Instance = s_GlobalNetworkObjects[m_ClientNetworkManagers[0].LocalClientId][serverDependingInstance.NetworkObjectId]; + Assert.IsNull(clientDepending1Instance.transform.parent); // Make sure the client instance was reparented + + yield return CreateAndStartNewClient(); + + var clientDepending2Instance = s_GlobalNetworkObjects[m_ClientNetworkManagers[1].LocalClientId][serverDependingInstance.NetworkObjectId]; + Assert.IsNull(clientDepending2Instance.transform.parent); // Make sure the late-joining client instance was reparented + } + + /// + /// Tests that depending s can be deleted, + /// and that those deletions will be synchronized across both connected + /// and late-joining clients. + /// + [UnityTest] + public IEnumerator TestDependingObjectDeletion() + { + var serverDependentInstance = SpawnObject(m_PrefabToSpawn, m_ServerNetworkManager).GetComponent(); + var serverDependingInstance = serverDependentInstance.transform.GetChild(0)?.GetComponent(); + Assert.IsNotNull(serverDependingInstance); // Sanity check + + yield return NetcodeIntegrationTestHelpers.WaitForMessageOfTypeHandled(m_ClientNetworkManagers[0]); + + var clientDepending1Instance = s_GlobalNetworkObjects[m_ClientNetworkManagers[0].LocalClientId][serverDependingInstance.NetworkObjectId]; + Object.Destroy(serverDependingInstance.gameObject); + + yield return NetcodeIntegrationTestHelpers.WaitForMessageOfTypeHandled(m_ClientNetworkManagers[0]); + + Assert.IsTrue(clientDepending1Instance == null, "Dependent NetworkObject was not destroyed on connected client."); + + yield return CreateAndStartNewClient(); + + Assert.IsTrue( + !s_GlobalNetworkObjects[m_ClientNetworkManagers[1].LocalClientId].ContainsKey(serverDependingInstance.NetworkObjectId) || + s_GlobalNetworkObjects[m_ClientNetworkManagers[1].LocalClientId][serverDependingInstance.NetworkObjectId] == null, + "Dependent NetworkObject was not destroyed on late-joining client."); + } + + /// + /// Tests that deleting s also deletes any + /// s that are dependent on the deleted one. + /// + [UnityTest] + public IEnumerator TestDependentObjectDeletion() + { + var serverDependentInstance = SpawnObject(m_PrefabToSpawn, m_ServerNetworkManager).GetComponent(); + Assert.IsTrue(serverDependentInstance.DependingNetworkObjects.Count > 0); // Make sure the prefab has a dependent NetworkObject + var serverDependingInstance = serverDependentInstance.DependingNetworkObjects[0]; + + yield return NetcodeIntegrationTestHelpers.WaitForMessageOfTypeHandled(m_ClientNetworkManagers[0]); + + var clientDependentInstance = s_GlobalNetworkObjects[m_ClientNetworkManagers[0].LocalClientId][serverDependentInstance.NetworkObjectId]; + var clientDependingInstance = clientDependentInstance.DependingNetworkObjects[0]; + Object.Destroy(serverDependentInstance.gameObject); + + yield return NetcodeIntegrationTestHelpers.WaitForMessageOfTypeHandled(m_ClientNetworkManagers[0]); // Wait for parent deleting + + Assert.IsTrue(serverDependentInstance == null, "Dependent NetworkObject was not destroyed on host."); + Assert.IsTrue(serverDependingInstance == null, "Depending NetworkObject was not destroyed on host."); + Assert.IsTrue(clientDependentInstance == null, "Dependent NetworkObject was not destroyed on connected client."); + Assert.IsTrue(clientDependingInstance == null, "Depending NetworkObject was not destroyed on connected client."); + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectDependencyTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectDependencyTests.cs.meta new file mode 100644 index 0000000000..5615b3ecd8 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectDependencyTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 454b226302b5d784f9b15b10c3d516ad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: