From 373fab056983206de0e33d1b49cfcdeda1db03b3 Mon Sep 17 00:00:00 2001 From: Gareth Fultz Date: Wed, 26 Jul 2023 23:01:05 -0400 Subject: [PATCH] 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) {