From 373fab056983206de0e33d1b49cfcdeda1db03b3 Mon Sep 17 00:00:00 2001 From: Gareth Fultz Date: Wed, 26 Jul 2023 23:01:05 -0400 Subject: [PATCH 01/10] feat(nested_network_objects): Added nested NetworkObject support Introduces the concept of Dependent NetworkObjects. Dependent NetworkObjects cannot outlive the NetworkObject they are dependent on. When spawning a prefab with nested NetworkObjects, all nested NetworkObjects are dependent on the root NetworkObject. This means that nested NetworkObjects can always be spawned/synchronized by spawning the root NetworkObject (and despawning any NetworkObjects that are no longer spawned). Resolves #2637 --- .../Runtime/Core/NetworkObject.cs | 260 +++++++++++++++--- .../Messages/ConnectionApprovedMessage.cs | 3 + .../Runtime/SceneManagement/SceneEventData.cs | 2 +- .../Runtime/Spawning/NetworkSpawnManager.cs | 45 ++- 4 files changed, 261 insertions(+), 49 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 81542e3318..2737039aa8 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,21 @@ public sealed class NetworkObject : MonoBehaviour [SerializeField] internal uint GlobalObjectIdHash; + // TODO: Remove + //[HideInInspector] + [SerializeField] + internal NetworkObject DependentNetworkObject = null; + + //[HideInInspector] + [SerializeField] + internal List DependingNetworkObjects = new List(); + + /// + /// Whether this NetworkObject is dependent on another NetworkObject + /// + public bool IsDependent => DependentNetworkObject != null; + + /// /// Gets the Prefab Hash Id of this object if the object is registerd as a prefab otherwise it returns 0 /// @@ -43,6 +61,8 @@ public uint PrefabIdHash private void OnValidate() { GenerateGlobalObjectIdHash(); + + CheckDependency(); } internal void GenerateGlobalObjectIdHash() @@ -62,6 +82,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 + DependentNetworkObject = null; + GetComponentsInChildren(DependingNetworkObjects); + DependingNetworkObjects.Remove(this); + + // Have the parent register its children as dependent + foreach (var obj in DependingNetworkObjects) + { + obj.DependentNetworkObject = this; + obj.DependingNetworkObjects.Clear(); + } + } + } + else + { + // In-scene placed NetworkObjects cannot be dependent + DependentNetworkObject = null; + DependingNetworkObjects.Clear(); + } + } #endif // UNITY_EDITOR /// @@ -582,8 +633,18 @@ private void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool play throw new NotServerException($"Only server can spawn {nameof(NetworkObject)}s"); } + if (DependentNetworkObject != null) + { + throw new NotServerException($"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 < DependingNetworkObjects.Count; i++) + { + NetworkManager.SpawnManager.SpawnNetworkObjectLocally(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 +1018,12 @@ 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) + var isIndependent = DependentNetworkObject == null; + if (transform.parent != null && !removeParent && !m_LatestParent.HasValue && (isInScenePlaced || !isIndependent)) { var parentNetworkObject = transform.parent.GetComponent(); @@ -976,8 +1038,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 onjects. + else if (!parentNetworkObject.IsSpawned) { OrphanChildren.Add(this); return false; @@ -1198,6 +1261,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 +1274,7 @@ internal NetworkBehaviour GetNetworkBehaviourAtOrderIndex(ushort index) } NetworkLog.LogInfo(currentKnownChildren.ToString()); } + return null; } @@ -1268,6 +1333,34 @@ public bool DestroyWithScene set => ByteUtility.SetBit(ref m_BitField, 6, value); } + public struct DependingObjectData : 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 DependingObjectData[] DependingObjects; + //If(Metadata.HasParent) public ulong ParentObjectId; @@ -1294,6 +1387,7 @@ public struct TransformData : INetworkSerializeByMemcpy public void Serialize(FastBufferWriter writer) { + Debug.Log($"Serialize {OwnerObject.gameObject}"); writer.WriteValueSafe(m_BitField); writer.WriteValueSafe(Hash); BytePacker.WriteValueBitPacked(writer, NetworkObjectId); @@ -1308,15 +1402,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); @@ -1326,9 +1429,17 @@ public void Serialize(FastBufferWriter writer) // scene handle that the NetworkObject resides in. writer.WriteValue(OwnerObject.GetSceneOriginHandle()); - // Synchronize NetworkVariables and NetworkBehaviours - var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer)); - OwnerObject.SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId); + { // Synchronize NetworkVariables and NetworkBehaviours + var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer)); + OwnerObject.SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId); + } + + // Synchronize NetworkVariables and NetworkBehaviours of depending objects + for (int i = 0; i < dependingCount; i++) + { + var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer)); + OwnerObject.DependingNetworkObjects[i].SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId); + } } public void Deserialize(FastBufferReader reader) @@ -1348,9 +1459,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 +1472,12 @@ public void Deserialize(FastBufferReader reader) throw new OverflowException("Could not deserialize SceneObject: Reading past the end of the buffer"); } + DependingObjects = new DependingObjectData[dependingCount]; + for (int i = 0; i < dependingCount; i++) + { + reader.ReadValue(out DependingObjects[i]); + } + if (HasTransform) { reader.ReadValue(out Transform); @@ -1463,31 +1583,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 +1650,50 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId) }; } + SceneObject.DependingObjectData[] DependingObjects = new SceneObject.DependingObjectData[DependingNetworkObjects.Count]; + for (int i = 0; i < DependingNetworkObjects.Count; i++) + { + if (DependingNetworkObjects[i] == null || !DependingNetworkObjects[i].IsSpawned) + { + DependingObjects[i] = new SceneObject.DependingObjectData + { + IsSpawned = false, + }; + } + else + { + DependingObjects[i] = new SceneObject.DependingObjectData + { + IsSpawned = true, + NetworkObjectId = DependingNetworkObjects[i].NetworkObjectId, + OwnerClientId = DependingNetworkObjects[i].OwnerClientId, + }; + + { // Set parentage info + NetworkObject parentNetworkObject = null; + + if (!AlwaysReplicateAsRoot && DependingNetworkObjects[i].transform.parent != null) + { + parentNetworkObject = DependingNetworkObjects[i].transform.parent.GetComponent(); + } + + if (parentNetworkObject != null) + { + DependingObjects[i].HasParent = true; + DependingObjects[i].ParentObjectId = parentNetworkObject.NetworkObjectId; + var latestParent = 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,16 +1733,32 @@ 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; + { + // 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; + + // Synchronize NetworkBehaviours + var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); + networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId); - // Synchronize NetworkBehaviours - var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); - networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId); + // Spawn the NetworkObject + networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, sceneObject.DestroyWithScene); + } - // Spawn the NetworkObject - networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, sceneObject.DestroyWithScene); + // Repeat Steps for depending NetworkObjects + for (int i = 0; i < networkObject.DependingNetworkObjects.Count; i++) + { + var dependingObj = networkObject.DependingNetworkObjects[i]; + var dependingObjData = sceneObject.DependingObjects[i]; + + dependingObj.OwnerClientId = sceneObject.OwnerClientId; + + var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); + 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..4583c9a00c 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -473,6 +473,36 @@ 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, + for (int i = 0; i < sceneObject.DependingObjects.Length; i++) + { + var childData = sceneObject.DependingObjects[i]; + var childNetworkObject = networkObject.DependingNetworkObjects[i]; + + if (childData.IsSpawned) + { + childNetworkObject.DestroyWithScene = sceneObject.DestroyWithScene; + childNetworkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle; + + 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 +520,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 +841,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) { From 6852bd33207bac6b821f38d378e809d07a199717 Mon Sep 17 00:00:00 2001 From: Gareth Fultz Date: Thu, 27 Jul 2023 00:27:15 -0400 Subject: [PATCH 02/10] update Cleaned up code to be more specific, remove a log statement, hide serialized dependent NetworkObject fields in editor, and correct typos. --- .../Runtime/Core/NetworkObject.cs | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 2737039aa8..5bdd8d89b1 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -20,19 +20,24 @@ public sealed class NetworkObject : MonoBehaviour [SerializeField] internal uint GlobalObjectIdHash; - // TODO: Remove - //[HideInInspector] + [HideInInspector] [SerializeField] - internal NetworkObject DependentNetworkObject = null; + private NetworkObject m_DependentNetworkObject = null; - //[HideInInspector] + [HideInInspector] [SerializeField] internal List DependingNetworkObjects = new List(); /// - /// Whether this NetworkObject is dependent on another NetworkObject + /// 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 bool IsDependent => DependentNetworkObject != null; + public NetworkObject DependentNetworkObject { get => m_DependentNetworkObject; internal set => m_DependentNetworkObject = value; } /// @@ -633,9 +638,9 @@ private void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool play throw new NotServerException($"Only server can spawn {nameof(NetworkObject)}s"); } - if (DependentNetworkObject != null) + if (IsDependent) { - throw new NotServerException($"Cannot spawn {nameof(NetworkObject)}s that are dependent on other {nameof(NetworkObject)}s"); + 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); @@ -1022,8 +1027,7 @@ internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpa // 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; - var isIndependent = DependentNetworkObject == null; - if (transform.parent != null && !removeParent && !m_LatestParent.HasValue && (isInScenePlaced || !isIndependent)) + if (transform.parent != null && !removeParent && !m_LatestParent.HasValue && (isInScenePlaced || IsDependent)) { var parentNetworkObject = transform.parent.GetComponent(); @@ -1039,7 +1043,7 @@ internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpa return true; } // If the parent still isn't spawned add this to the orphaned children and return false. Should only occur - // with in-scene placed onjects. + // with in-scene placed objects. else if (!parentNetworkObject.IsSpawned) { OrphanChildren.Add(this); @@ -1333,7 +1337,7 @@ public bool DestroyWithScene set => ByteUtility.SetBit(ref m_BitField, 6, value); } - public struct DependingObjectData : INetworkSerializeByMemcpy + public struct DependingSceneObject : INetworkSerializeByMemcpy { private byte m_BitField; @@ -1359,7 +1363,7 @@ public bool IsLatestParentSet } } - public DependingObjectData[] DependingObjects; + public DependingSceneObject[] DependingObjects; //If(Metadata.HasParent) public ulong ParentObjectId; @@ -1387,7 +1391,6 @@ public struct TransformData : INetworkSerializeByMemcpy public void Serialize(FastBufferWriter writer) { - Debug.Log($"Serialize {OwnerObject.gameObject}"); writer.WriteValueSafe(m_BitField); writer.WriteValueSafe(Hash); BytePacker.WriteValueBitPacked(writer, NetworkObjectId); @@ -1406,7 +1409,7 @@ public void Serialize(FastBufferWriter writer) writer.WriteValueSafe(dependingCount); var writeSize = 0; - writeSize += dependingCount * FastBufferWriter.GetWriteSize(); // Each Depending Object + writeSize += dependingCount * FastBufferWriter.GetWriteSize(); // Each Depending Object writeSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0; // Transform writeSize += FastBufferWriter.GetWriteSize(); // NetworkSceneHandle @@ -1462,7 +1465,7 @@ public void Deserialize(FastBufferReader reader) reader.ReadValueSafe(out int dependingCount); var readSize = 0; - readSize += dependingCount * FastBufferWriter.GetWriteSize(); // Each Depending Object + readSize += dependingCount * FastBufferWriter.GetWriteSize(); // Each Depending Object readSize += HasTransform ? FastBufferWriter.GetWriteSize() : 0; // Transform readSize += FastBufferWriter.GetWriteSize(); // NetworkSceneHandle @@ -1472,7 +1475,7 @@ public void Deserialize(FastBufferReader reader) throw new OverflowException("Could not deserialize SceneObject: Reading past the end of the buffer"); } - DependingObjects = new DependingObjectData[dependingCount]; + DependingObjects = new DependingSceneObject[dependingCount]; for (int i = 0; i < dependingCount; i++) { reader.ReadValue(out DependingObjects[i]); @@ -1650,19 +1653,19 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId) }; } - SceneObject.DependingObjectData[] DependingObjects = new SceneObject.DependingObjectData[DependingNetworkObjects.Count]; + SceneObject.DependingSceneObject[] DependingObjects = new SceneObject.DependingSceneObject[DependingNetworkObjects.Count]; for (int i = 0; i < DependingNetworkObjects.Count; i++) { if (DependingNetworkObjects[i] == null || !DependingNetworkObjects[i].IsSpawned) { - DependingObjects[i] = new SceneObject.DependingObjectData + DependingObjects[i] = new SceneObject.DependingSceneObject { IsSpawned = false, }; } else { - DependingObjects[i] = new SceneObject.DependingObjectData + DependingObjects[i] = new SceneObject.DependingSceneObject { IsSpawned = true, NetworkObjectId = DependingNetworkObjects[i].NetworkObjectId, From 4c91b2a7cf451a65a6999bdd4edf01935b12d6bb Mon Sep 17 00:00:00 2001 From: Gareth Fultz Date: Fri, 28 Jul 2023 19:35:37 -0400 Subject: [PATCH 03/10] update Removed unnecessary serialization code --- .../Runtime/Core/NetworkObject.cs | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 5bdd8d89b1..ba59dab506 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -1432,15 +1432,12 @@ public void Serialize(FastBufferWriter writer) // scene handle that the NetworkObject resides in. writer.WriteValue(OwnerObject.GetSceneOriginHandle()); - { // Synchronize NetworkVariables and NetworkBehaviours - var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer)); - OwnerObject.SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId); - } + // Synchronize NetworkVariables and NetworkBehaviours + var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer)); + OwnerObject.SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId); - // Synchronize NetworkVariables and NetworkBehaviours of depending objects for (int i = 0; i < dependingCount; i++) { - var bufferSerializer = new BufferSerializer(new BufferSerializerWriter(writer)); OwnerObject.DependingNetworkObjects[i].SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId); } } @@ -1736,18 +1733,18 @@ 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; - // Synchronize NetworkBehaviours - var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); - networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId); + // 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; + + // Synchronize NetworkBehaviours + var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); + networkObject.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId); + + // Spawn the NetworkObject + networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, sceneObject.DestroyWithScene); - // Spawn the NetworkObject - networkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, sceneObject, sceneObject.DestroyWithScene); - } // Repeat Steps for depending NetworkObjects for (int i = 0; i < networkObject.DependingNetworkObjects.Count; i++) @@ -1757,7 +1754,6 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf dependingObj.OwnerClientId = sceneObject.OwnerClientId; - var bufferSerializer = new BufferSerializer(new BufferSerializerReader(reader)); dependingObj.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId); networkManager.SpawnManager.SpawnNetworkObjectLocally(dependingObj, dependingObjData.NetworkObjectId, sceneObject.IsSceneObject, false, dependingObjData.OwnerClientId, sceneObject.DestroyWithScene); From 1c682fc279a551ec2f4a3861541af61b6e2ec610 Mon Sep 17 00:00:00 2001 From: Gareth Fultz Date: Fri, 28 Jul 2023 19:46:13 -0400 Subject: [PATCH 04/10] update added public property to see what NetworkObjects are depending on a NetworkObject --- .../Runtime/Core/NetworkObject.cs | 50 +++++++++++-------- .../Runtime/Spawning/NetworkSpawnManager.cs | 3 +- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index ba59dab506..7c87c95fb5 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -26,7 +26,7 @@ public sealed class NetworkObject : MonoBehaviour [HideInInspector] [SerializeField] - internal List DependingNetworkObjects = new List(); + private List m_DependingNetworkObjects = new List(); /// /// Whether this NetworkObject is dependent on another NetworkObject. @@ -39,6 +39,12 @@ public sealed class NetworkObject : MonoBehaviour /// 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 @@ -99,23 +105,23 @@ internal void CheckDependency() if (parent == null) { // Find nested/dependent NetworkObjects - DependentNetworkObject = null; - GetComponentsInChildren(DependingNetworkObjects); - DependingNetworkObjects.Remove(this); + m_DependentNetworkObject = null; + GetComponentsInChildren(m_DependingNetworkObjects); + m_DependingNetworkObjects.Remove(this); // Have the parent register its children as dependent - foreach (var obj in DependingNetworkObjects) + foreach (var obj in m_DependingNetworkObjects) { - obj.DependentNetworkObject = this; - obj.DependingNetworkObjects.Clear(); + obj.m_DependentNetworkObject = this; + obj.m_DependingNetworkObjects.Clear(); } } } else { // In-scene placed NetworkObjects cannot be dependent - DependentNetworkObject = null; - DependingNetworkObjects.Clear(); + m_DependentNetworkObject = null; + m_DependingNetworkObjects.Clear(); } } #endif // UNITY_EDITOR @@ -645,9 +651,9 @@ private void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool play NetworkManager.SpawnManager.SpawnNetworkObjectLocally(this, NetworkManager.SpawnManager.GetNetworkObjectId(), IsSceneObject.HasValue && IsSceneObject.Value, playerObject, ownerClientId, destroyWithScene); - for (int i = 0; i < DependingNetworkObjects.Count; i++) + for (int i = 0; i < m_DependingNetworkObjects.Count; i++) { - NetworkManager.SpawnManager.SpawnNetworkObjectLocally(DependingNetworkObjects[i], NetworkManager.SpawnManager.GetNetworkObjectId(), IsSceneObject.HasValue && IsSceneObject.Value, false, ownerClientId, destroyWithScene); + 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++) @@ -1438,7 +1444,7 @@ public void Serialize(FastBufferWriter writer) for (int i = 0; i < dependingCount; i++) { - OwnerObject.DependingNetworkObjects[i].SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId); + OwnerObject.m_DependingNetworkObjects[i].SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId); } } @@ -1650,10 +1656,10 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId) }; } - SceneObject.DependingSceneObject[] DependingObjects = new SceneObject.DependingSceneObject[DependingNetworkObjects.Count]; - for (int i = 0; i < DependingNetworkObjects.Count; i++) + SceneObject.DependingSceneObject[] DependingObjects = new SceneObject.DependingSceneObject[m_DependingNetworkObjects.Count]; + for (int i = 0; i < m_DependingNetworkObjects.Count; i++) { - if (DependingNetworkObjects[i] == null || !DependingNetworkObjects[i].IsSpawned) + if (m_DependingNetworkObjects[i] == null || !m_DependingNetworkObjects[i].IsSpawned) { DependingObjects[i] = new SceneObject.DependingSceneObject { @@ -1665,23 +1671,23 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId) DependingObjects[i] = new SceneObject.DependingSceneObject { IsSpawned = true, - NetworkObjectId = DependingNetworkObjects[i].NetworkObjectId, - OwnerClientId = DependingNetworkObjects[i].OwnerClientId, + NetworkObjectId = m_DependingNetworkObjects[i].NetworkObjectId, + OwnerClientId = m_DependingNetworkObjects[i].OwnerClientId, }; { // Set parentage info NetworkObject parentNetworkObject = null; - if (!AlwaysReplicateAsRoot && DependingNetworkObjects[i].transform.parent != null) + if (!AlwaysReplicateAsRoot && m_DependingNetworkObjects[i].transform.parent != null) { - parentNetworkObject = DependingNetworkObjects[i].transform.parent.GetComponent(); + parentNetworkObject = m_DependingNetworkObjects[i].transform.parent.GetComponent(); } if (parentNetworkObject != null) { DependingObjects[i].HasParent = true; DependingObjects[i].ParentObjectId = parentNetworkObject.NetworkObjectId; - var latestParent = DependingNetworkObjects[i].GetNetworkParenting(); + var latestParent = m_DependingNetworkObjects[i].GetNetworkParenting(); var isLatestParentSet = latestParent != null && latestParent.HasValue; DependingObjects[i].IsLatestParentSet = isLatestParentSet; if (isLatestParentSet) @@ -1747,9 +1753,9 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf // Repeat Steps for depending NetworkObjects - for (int i = 0; i < networkObject.DependingNetworkObjects.Count; i++) + for (int i = 0; i < networkObject.m_DependingNetworkObjects.Count; i++) { - var dependingObj = networkObject.DependingNetworkObjects[i]; + var dependingObj = networkObject.m_DependingNetworkObjects[i]; var dependingObjData = sceneObject.DependingObjects[i]; dependingObj.OwnerClientId = sceneObject.OwnerClientId; diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 4583c9a00c..e7cf16bb7f 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -475,10 +475,11 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO } // Hook up NetworkObjects that depend on this NetworkObject. Usually used for nested NetworkObjects in prefabs, + var DependingNetworkObjects = networkObject.DependingNetworkObjects; for (int i = 0; i < sceneObject.DependingObjects.Length; i++) { var childData = sceneObject.DependingObjects[i]; - var childNetworkObject = networkObject.DependingNetworkObjects[i]; + var childNetworkObject = DependingNetworkObjects[i]; if (childData.IsSpawned) { From 9a50e42069786069ff81a8d91884da5e8b6a3f5d Mon Sep 17 00:00:00 2001 From: Gareth Fultz Date: Fri, 28 Jul 2023 21:42:23 -0400 Subject: [PATCH 05/10] fix Fixed a bug throwing errors when spawning prefabs without children --- .../Runtime/Spawning/NetworkSpawnManager.cs | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index e7cf16bb7f..0baaa34866 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -475,33 +475,36 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO } // Hook up NetworkObjects that depend on this NetworkObject. Usually used for nested NetworkObjects in prefabs, - var DependingNetworkObjects = networkObject.DependingNetworkObjects; - for (int i = 0; i < sceneObject.DependingObjects.Length; i++) + if (sceneObject.DependingObjects != null) { - var childData = sceneObject.DependingObjects[i]; - var childNetworkObject = DependingNetworkObjects[i]; - - if (childData.IsSpawned) + var DependingNetworkObjects = networkObject.DependingNetworkObjects; + for (int i = 0; i < sceneObject.DependingObjects.Length; i++) { - childNetworkObject.DestroyWithScene = sceneObject.DestroyWithScene; - childNetworkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle; + var childData = sceneObject.DependingObjects[i]; + var childNetworkObject = DependingNetworkObjects[i]; - if (childData.HasParent) + if (childData.IsSpawned) { - // 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) + childNetworkObject.DestroyWithScene = sceneObject.DestroyWithScene; + childNetworkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle; + + if (childData.HasParent) { - parentId = childData.HasParent ? childData.ParentObjectId : default; + // 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); } - childNetworkObject.SetNetworkParenting(parentId, true); } - } - else - { - // Remove unspawned child NetworkObjects - GameObject.Destroy(networkObject.DependingNetworkObjects[i].gameObject); + else + { + // Remove unspawned child NetworkObjects + GameObject.Destroy(networkObject.DependingNetworkObjects[i].gameObject); + } } } } From 8b7d26677afc915fc47dfbec2c6d5ea8c59d0fa2 Mon Sep 17 00:00:00 2001 From: Gareth Fultz Date: Sat, 29 Jul 2023 12:56:14 -0400 Subject: [PATCH 06/10] fix Fixed a bug where dependent network objects wouldn't be reparented to root on late-joining clients --- .../Runtime/Core/NetworkObject.cs | 9 ++++++++- .../Runtime/Spawning/NetworkSpawnManager.cs | 6 ++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 7c87c95fb5..993bc7a8ef 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -1678,9 +1678,16 @@ internal SceneObject GetMessageSceneObject(ulong targetClientId) { // Set parentage info NetworkObject parentNetworkObject = null; - if (!AlwaysReplicateAsRoot && m_DependingNetworkObjects[i].transform.parent != 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) diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 0baaa34866..6e9158e684 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -488,6 +488,12 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO 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 From 59eeac40145e879f2c709f2989073e65f09105db Mon Sep 17 00:00:00 2001 From: Gareth Fultz Date: Sun, 30 Jul 2023 13:12:11 -0400 Subject: [PATCH 07/10] fix Fixed a bug where player prefabs could not have nested NetworkObjects --- .../Runtime/Connection/NetworkConnectionManager.cs | 5 +++++ 1 file changed, 5 insertions(+) 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); } From a23deb60b87d62877e100caa868e9747c568ce5c Mon Sep 17 00:00:00 2001 From: Gareth Fultz Date: Sun, 30 Jul 2023 16:06:34 -0400 Subject: [PATCH 08/10] fix Fixed a bug where despawned NetworkObjects would attempt to be serialized --- .../Runtime/Core/NetworkObject.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 993bc7a8ef..b4dc1ca918 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -1444,7 +1444,10 @@ public void Serialize(FastBufferWriter writer) for (int i = 0; i < dependingCount; i++) { - OwnerObject.m_DependingNetworkObjects[i].SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId); + if (DependingObjects[i].IsSpawned) + { + OwnerObject.m_DependingNetworkObjects[i].SynchronizeNetworkBehaviours(ref bufferSerializer, TargetClientId); + } } } @@ -1765,11 +1768,14 @@ internal static NetworkObject AddSceneObject(in SceneObject sceneObject, FastBuf var dependingObj = networkObject.m_DependingNetworkObjects[i]; var dependingObjData = sceneObject.DependingObjects[i]; - dependingObj.OwnerClientId = sceneObject.OwnerClientId; + if (dependingObjData.IsSpawned) + { + dependingObj.OwnerClientId = sceneObject.OwnerClientId; - dependingObj.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId); + dependingObj.SynchronizeNetworkBehaviours(ref bufferSerializer, networkManager.LocalClientId); - networkManager.SpawnManager.SpawnNetworkObjectLocally(dependingObj, dependingObjData.NetworkObjectId, sceneObject.IsSceneObject, false, dependingObjData.OwnerClientId, sceneObject.DestroyWithScene); + networkManager.SpawnManager.SpawnNetworkObjectLocally(dependingObj, dependingObjData.NetworkObjectId, sceneObject.IsSceneObject, false, dependingObjData.OwnerClientId, sceneObject.DestroyWithScene); + } } return networkObject; From e30bb4d4063a669c15bd6fcaa5120206868cff98 Mon Sep 17 00:00:00 2001 From: Gareth Fultz Date: Sun, 30 Jul 2023 16:45:52 -0400 Subject: [PATCH 09/10] test Validates that nested network objects work properly. --- .../Runtime/NetcodeIntegrationTestHelpers.cs | 46 ++++-- .../NetworkObjectDependencyTests.cs | 146 ++++++++++++++++++ .../NetworkObjectDependencyTests.cs.meta | 11 ++ 3 files changed, 193 insertions(+), 10 deletions(-) create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectDependencyTests.cs create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectDependencyTests.cs.meta 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: From 858f728b87069c28f70bb54074db323ae0e0e914 Mon Sep 17 00:00:00 2001 From: Gareth Fultz Date: Sun, 30 Jul 2023 16:53:54 -0400 Subject: [PATCH 10/10] changelog Updated changelog to include nested NetworkObjects --- com.unity.netcode.gameobjects/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) 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)