diff --git a/Assets/Mirage/Components/NetworkParent.cs b/Assets/Mirage/Components/NetworkParent.cs new file mode 100644 index 00000000000..d86a5add0c6 --- /dev/null +++ b/Assets/Mirage/Components/NetworkParent.cs @@ -0,0 +1,11 @@ +using UnityEngine; + +namespace Mirage.Components +{ + /// + /// An NetworkBehaviour class to represent a parent object over the network + /// + public class NetworkParent : NetworkBehaviour + { + } +} diff --git a/Assets/Mirage/Components/NetworkParent.cs.meta b/Assets/Mirage/Components/NetworkParent.cs.meta new file mode 100644 index 00000000000..23d9df022d2 --- /dev/null +++ b/Assets/Mirage/Components/NetworkParent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 638d00d097af83c479a5ee7cb8779e1a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirage/Runtime/ClientObjectManager.cs b/Assets/Mirage/Runtime/ClientObjectManager.cs index 8b5ecd974bb..1e11be0fe3a 100644 --- a/Assets/Mirage/Runtime/ClientObjectManager.cs +++ b/Assets/Mirage/Runtime/ClientObjectManager.cs @@ -709,7 +709,14 @@ private NetworkIdentity SpawnPrefab(SpawnMessage msg, SpawnHandler handler) // we need to set position and rotation here incase that their values are used from awake/onenable var pos = msg.SpawnValues.Position ?? prefab.transform.position; var rot = msg.SpawnValues.Rotation ?? prefab.transform.rotation; - return Instantiate(prefab, pos, rot); + var clone = Instantiate(prefab, pos, rot); + + if (msg.SpawnValues.Parent.HasValue && msg.SpawnValues.Parent.Value.TryGet(_client.World, out var parentTransform)) + { + clone.transform.SetParent(parentTransform, false); + } + + return clone; } internal NetworkIdentity SpawnSceneObject(SpawnMessage msg) @@ -724,6 +731,12 @@ internal NetworkIdentity SpawnSceneObject(SpawnMessage msg) throw new SpawnObjectException($"Scene object is null, sceneId={msg.SceneId:X}, NetId={msg.NetId}"); if (logger.LogEnabled()) logger.Log($"[ClientObjectManager] Found scene object for netid:{msg.NetId}, sceneId:{msg.SceneId.Value:X}, obj:{foundSceneObject}"); + + if (msg.SpawnValues.Parent.HasValue && msg.SpawnValues.Parent.Value.TryGet(_client.World, out var parentTransform)) + { + foundSceneObject.transform.SetParent(parentTransform, false); + } + return foundSceneObject; } diff --git a/Assets/Mirage/Runtime/Extensions/ServerObjectManagerExtensions.cs b/Assets/Mirage/Runtime/Extensions/ServerObjectManagerExtensions.cs index b7781f5c8f5..5f403e2f6aa 100644 --- a/Assets/Mirage/Runtime/Extensions/ServerObjectManagerExtensions.cs +++ b/Assets/Mirage/Runtime/Extensions/ServerObjectManagerExtensions.cs @@ -109,6 +109,24 @@ public static void Spawn(this ServerObjectManager som, GameObject obj, INetworkP som.Spawn(identity, owner); } + /// + /// Spawns the with a specific and assigns to be it's owner. + /// This will set the transform parent on the server and ensure the client receives the parent information if is enabled. + /// + /// The object to spawn. + /// The parent for the object. + /// The connection that has authority over the object. + public static void Spawn(this ServerObjectManager som, NetworkIdentity identity, Component parent, INetworkPlayer owner = null) + { + if (parent == null) + throw new ArgumentNullException(nameof(parent)); + + identity.Parent = parent; + identity.transform.SetParent(parent.transform); + + som.Spawn(identity, owner); + } + /// /// Instantiate a prefab an then Spawns it with ServerObjectManager diff --git a/Assets/Mirage/Runtime/Messages.cs b/Assets/Mirage/Runtime/Messages.cs index 73eb74281bb..430941c2e53 100644 --- a/Assets/Mirage/Runtime/Messages.cs +++ b/Assets/Mirage/Runtime/Messages.cs @@ -96,6 +96,7 @@ public struct SpawnValues public Vector3? Scale; public string Name; public bool? SelfActive; + public NetworkReferenceId? Parent; [ThreadStatic] private static StringBuilder builder; @@ -125,6 +126,9 @@ public override string ToString() if (SelfActive.HasValue) Append(ref first, $"SelfActive={SelfActive.Value}"); + if (Parent.HasValue) + Append(ref first, $"Parent={Parent.Value}"); + builder.Append(")"); return builder.ToString(); } @@ -185,4 +189,42 @@ public struct NetworkPongMessage public double ClientTime; public double ServerTime; } + + public struct NetworkReferenceId : IEquatable + { + public uint NetId; + public byte? ComponentIndex; + + public bool TryGet(NetworkWorld world, out Transform transform) + { + if (world.TryGetIdentity(NetId, out var identity)) + { + if (ComponentIndex.HasValue) + { + if (ComponentIndex.Value < identity.NetworkBehaviours.Length) + { + transform = identity.NetworkBehaviours[ComponentIndex.Value].transform; + return true; + } + } + else + { + transform = identity.transform; + return true; + } + } + transform = null; + return false; + } + + public bool Equals(NetworkReferenceId other) + { + return NetId == other.NetId && ComponentIndex == other.ComponentIndex; + } + + public override string ToString() + { + return ComponentIndex.HasValue ? $"[NetId:{NetId}, Comp:{ComponentIndex.Value}]" : $"[NetId:{NetId}]"; + } + } } diff --git a/Assets/Mirage/Runtime/NetworkIdentity.cs b/Assets/Mirage/Runtime/NetworkIdentity.cs index 0c86a2dbf8f..a072ca39301 100644 --- a/Assets/Mirage/Runtime/NetworkIdentity.cs +++ b/Assets/Mirage/Runtime/NetworkIdentity.cs @@ -224,6 +224,13 @@ public void ClearSceneId() [Tooltip("Reference to Server set after the object is spawned. Used when debugging to see which server this object belongs to.")] public ServerObjectManager ServerObjectManager; + /// + /// Explicit parent override for this object. Used by . + /// Can be a NetworkIdentity or a NetworkBehaviour. + /// + [Tooltip("Explicit parent override for this object. Used by SpawnParentingMode.Manual.")] + public Component Parent; + /// /// The NetworkClient associated with this NetworkIdentity. /// @@ -982,13 +989,6 @@ internal void SetServerValues(NetworkServer networkServer, ServerObjectManager s internal void SetClientValues(ClientObjectManager clientObjectManager, SpawnMessage msg) { - var spawnValues = msg.SpawnValues; - if (spawnValues.Position.HasValue) transform.localPosition = spawnValues.Position.Value; - if (spawnValues.Rotation.HasValue) transform.localRotation = spawnValues.Rotation.Value; - if (spawnValues.Scale.HasValue) transform.localScale = spawnValues.Scale.Value; - if (!string.IsNullOrEmpty(spawnValues.Name)) gameObject.name = spawnValues.Name; - if (spawnValues.SelfActive.HasValue) gameObject.SetActive(spawnValues.SelfActive.Value); - NetId = msg.NetId; HasAuthority = msg.IsOwner; ClientObjectManager = clientObjectManager; @@ -1000,6 +1000,15 @@ internal void SetClientValues(ClientObjectManager clientObjectManager, SpawnMess SyncVarSender = Client.SyncVarSender; } + var spawnValues = msg.SpawnValues; + + + if (spawnValues.Position.HasValue) transform.localPosition = spawnValues.Position.Value; + if (spawnValues.Rotation.HasValue) transform.localRotation = spawnValues.Rotation.Value; + if (spawnValues.Scale.HasValue) transform.localScale = spawnValues.Scale.Value; + if (!string.IsNullOrEmpty(spawnValues.Name)) gameObject.name = spawnValues.Name; + if (spawnValues.SelfActive.HasValue) gameObject.SetActive(spawnValues.SelfActive.Value); + foreach (var behaviour in NetworkBehaviours) behaviour.InitializeSyncObjects(); } diff --git a/Assets/Mirage/Runtime/NetworkSpawnSettings.cs b/Assets/Mirage/Runtime/NetworkSpawnSettings.cs index 739c6e17ceb..fdc5ea2c83e 100644 --- a/Assets/Mirage/Runtime/NetworkSpawnSettings.cs +++ b/Assets/Mirage/Runtime/NetworkSpawnSettings.cs @@ -11,15 +11,18 @@ public struct NetworkSpawnSettings public bool SendScale; public bool SendName; public SyncActiveOption SendActive; + public SpawnParentingMode SendParent; - public NetworkSpawnSettings(bool sendPosition, bool sendRotation, bool sendScale, bool sendName, SyncActiveOption sendActive) : this() + public NetworkSpawnSettings(bool sendPosition, bool sendRotation, bool sendScale, bool sendName, SyncActiveOption sendActive, SpawnParentingMode sendParent) : this() { SendPosition = sendPosition; SendRotation = sendRotation; SendScale = sendScale; SendName = sendName; SendActive = sendActive; + SendParent = sendParent; } + public NetworkSpawnSettings(bool sendPosition, bool sendRotation, bool sendScale) : this() { SendPosition = sendPosition; @@ -32,7 +35,8 @@ public NetworkSpawnSettings(bool sendPosition, bool sendRotation, bool sendScale sendRotation: true, sendScale: false, sendName: false, - sendActive: SyncActiveOption.ForceEnable); + sendActive: SyncActiveOption.ForceEnable, + sendParent: SpawnParentingMode.None); } @@ -53,4 +57,22 @@ public enum SyncActiveOption /// ForceEnable, } + + public enum SpawnParentingMode + { + /// + /// Don't synchronize parent-child relationship. + /// + None, + + /// + /// Automatically detect parent NetworkIdentity in the transform hierarchy. + /// + Auto, + + /// + /// Manually specify the parent NetworkIdentity via the field. + /// + Manual, + } } diff --git a/Assets/Mirage/Runtime/ServerObjectManager.cs b/Assets/Mirage/Runtime/ServerObjectManager.cs index e687add51d5..e961e9247b8 100644 --- a/Assets/Mirage/Runtime/ServerObjectManager.cs +++ b/Assets/Mirage/Runtime/ServerObjectManager.cs @@ -562,6 +562,32 @@ private SpawnValues CreateSpawnValues(NetworkIdentity identity) break; } + if (settings.SendParent != SpawnParentingMode.None) + { + NetworkIdentity parentIdentity = null; + NetworkBehaviour parentBehaviour = null; + + if (settings.SendParent == SpawnParentingMode.Manual) + { + if (identity.Parent is NetworkIdentity id) parentIdentity = id; + else if (identity.Parent is NetworkBehaviour b) { parentBehaviour = b; parentIdentity = b.Identity; } + } + else if (settings.SendParent == SpawnParentingMode.Auto) + { + parentBehaviour = identity.transform.parent?.GetComponentInParent(); + parentIdentity = identity.transform.parent?.GetComponentInParent(); + } + + if (parentIdentity != null && parentIdentity.NetId != 0) + { + values.Parent = new NetworkReferenceId + { + NetId = parentIdentity.NetId, + ComponentIndex = parentBehaviour != null ? (byte?)parentBehaviour.ComponentIndex : null + }; + } + } + return values; } diff --git a/Assets/Tests/Runtime/ClientServer/ClientObjectManagerParentingTest.cs b/Assets/Tests/Runtime/ClientServer/ClientObjectManagerParentingTest.cs new file mode 100644 index 00000000000..bfac79cd75f --- /dev/null +++ b/Assets/Tests/Runtime/ClientServer/ClientObjectManagerParentingTest.cs @@ -0,0 +1,110 @@ +using System.Collections; +using Mirage.Tests.Runtime; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Mirage.Tests.Runtime.ClientServer +{ + public class ClientObjectManagerParentingTest : ClientServerSetup + { + [UnityTest] + public IEnumerator AutoParentingWorks() + { + var parent = InstantiateForTest(_characterPrefab); + serverObjectManager.Spawn(parent); + + var child = InstantiateForTest(_characterPrefab); + child.SpawnSettings = new NetworkSpawnSettings + { + SendPosition = true, + SendRotation = true, + SendParent = SpawnParentingMode.Auto + }; + child.transform.SetParent(parent.transform); + child.transform.localPosition = new Vector3(1, 2, 3); + serverObjectManager.Spawn(child); + + // Wait for spawn messages to be processed + yield return null; + yield return null; + + var clientParent = _remoteClients[0].Get(parent); + var clientChild = _remoteClients[0].Get(child); + + Assert.That(clientChild.transform.parent, Is.EqualTo(clientParent.transform)); + Assert.That(clientChild.transform.localPosition, Is.EqualTo(new Vector3(1, 2, 3))); + } + + [UnityTest] + public IEnumerator ManualParentingWorks() + { + var parent = InstantiateForTest(_characterPrefab); + serverObjectManager.Spawn(parent); + + var child = InstantiateForTest(_characterPrefab); + child.SpawnSettings = new NetworkSpawnSettings + { + SendPosition = true, + SendParent = SpawnParentingMode.Manual + }; + child.Parent = parent; + child.transform.localPosition = new Vector3(4, 5, 6); + serverObjectManager.Spawn(child); + + yield return null; + yield return null; + + var clientParent = _remoteClients[0].Get(parent); + var clientChild = _remoteClients[0].Get(child); + + Assert.That(clientChild.transform.parent, Is.EqualTo(clientParent.transform)); + Assert.That(clientChild.transform.localPosition, Is.EqualTo(new Vector3(4, 5, 6))); + } + + [UnityTest] + public IEnumerator SpawnWithParentIdentityOverload() + { + var parent = InstantiateForTest(_characterPrefab); + serverObjectManager.Spawn(parent); + + var child = InstantiateForTest(_characterPrefab); + child.SpawnSettings = new NetworkSpawnSettings { SendParent = SpawnParentingMode.Manual }; + serverObjectManager.Spawn(child, parent); + + yield return null; + yield return null; + + var clientParent = _remoteClients[0].Get(parent); + var clientChild = _remoteClients[0].Get(child); + + Assert.That(child.transform.parent, Is.EqualTo(parent.transform), "Should be parented on server"); + Assert.That(clientChild.transform.parent, Is.EqualTo(clientParent.transform), "Should be parented on client"); + } + + [UnityTest] + public IEnumerator AutoParentingFindsHighestIdentity() + { + var grandParent = InstantiateForTest(_characterPrefab); + serverObjectManager.Spawn(grandParent); + + var parentWithoutIdentity = new GameObject("ParentNoNI").transform; + parentWithoutIdentity.SetParent(grandParent.transform); + + var child = InstantiateForTest(_characterPrefab); + child.SpawnSettings = new NetworkSpawnSettings { SendParent = SpawnParentingMode.Auto }; + child.transform.SetParent(parentWithoutIdentity); + serverObjectManager.Spawn(child); + + yield return null; + yield return null; + + var clientGrandParent = _remoteClients[0].Get(grandParent); + var clientChild = _remoteClients[0].Get(child); + + Assert.That(clientChild.transform.parent, Is.EqualTo(clientGrandParent.transform)); + + Object.DestroyImmediate(parentWithoutIdentity.gameObject); + } + } +} diff --git a/Assets/Tests/Runtime/ClientServer/ClientObjectManagerParentingTest.cs.meta b/Assets/Tests/Runtime/ClientServer/ClientObjectManagerParentingTest.cs.meta new file mode 100644 index 00000000000..65620ac6162 --- /dev/null +++ b/Assets/Tests/Runtime/ClientServer/ClientObjectManagerParentingTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8352a17bfb0afc741993b8aa5b1b413d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/doc/docs/guides/game-objects/pickup-drop-child.md b/doc/docs/guides/game-objects/pickup-drop-child.md index 2a8daf06f23..5094054f3cb 100644 --- a/doc/docs/guides/game-objects/pickup-drop-child.md +++ b/doc/docs/guides/game-objects/pickup-drop-child.md @@ -5,8 +5,8 @@ sidebar_position: 9 Frequently the question comes up about how to handle objects that are attached as children of the player prefab that all clients need to know about and synchronize, such as which weapon is equipped, picking up networked scene objects, and players dropping objects into the scene. -:::caution -Mirage cannot support multiple Network Identity components within an object hierarchy. Since the character object must have a Network Identity, none of its descendant objects can have one. +:::info +Mirage supports synchronizing parent-child relationships between objects that each have their own `NetworkIdentity`. This is useful when the child object needs to be a first-class networked object with its own `NetworkBehaviour`s, `SyncVar`s, or RPCs. ::: ## Child Objects @@ -244,4 +244,33 @@ Since the SceneObject(Clone) is networked, we can pass it directly through to `C For this entire example, the only prefab that needs to be registered with Network Manager besides the Player is the SceneObject prefab. -![Screenshot of inspector](/img/guides/game-objects/child-objects3.png) +## Networked Child Objects + +If you need a child object to have its own networking logic (like its own SyncVars or RPCs), you can use Mirage's **Spawn Parenting** feature. This allows you to parent one `NetworkIdentity` to another and have that relationship synchronized to all clients. + +### 1. Configure the Child Prefab +On the child prefab's `NetworkIdentity` component: +1. Set **Send Parent** to `Auto` (to automatically find the parent NI in the hierarchy) or `Manual` (to explicitly set it). +2. Configure **Send Position/Rotation** as needed (usually enabled for children). + +### 2. Spawning and Parenting +When spawning the child on the server, you can use the `Spawn` extension method that takes a parent: + +```cs +[ServerRpc] +public void CmdEquipNetworkedItem(GameObject itemPrefab) +{ + // 1. Instantiate the item + GameObject itemGo = Instantiate(itemPrefab); + NetworkIdentity itemIdentity = itemGo.GetComponent(); + + // 2. Spawn and parent it to the player's 'RightHand' + // This will set the transform parent on the server and sync it to clients + ServerObjectManager.Spawn(itemIdentity, this.Identity, Owner); + + // 3. Move to the specific attach point (optional, depends on your setup) + itemGo.transform.SetParent(rightHand.transform, false); +} +``` + +This approach is more powerful than the art-swapping approach for complex items, but it comes with the overhead of an additional networked object. diff --git a/doc/docs/guides/game-objects/spawn-object-custom.md b/doc/docs/guides/game-objects/spawn-object-custom.md index 9a4f9f4a6fa..06fc3665dca 100644 --- a/doc/docs/guides/game-objects/spawn-object-custom.md +++ b/doc/docs/guides/game-objects/spawn-object-custom.md @@ -62,7 +62,9 @@ The spawn functions themselves are implemented with the delegate signature. Here ``` cs public NetworkIdentity SpawnCoin(SpawnMessage msg) { - return Instantiate(m_CoinPrefab, msg.position, msg.rotation); + var pos = msg.SpawnValues.Position ?? m_CoinPrefab.transform.position; + var rot = msg.SpawnValues.Rotation ?? m_CoinPrefab.transform.rotation; + return Instantiate(m_CoinPrefab, pos, rot); } public void UnSpawnCoin(NetworkIdentity spawned) { diff --git a/doc/docs/guides/game-objects/spawn-object-pooling.md b/doc/docs/guides/game-objects/spawn-object-pooling.md index 9784ed69f9b..1d610dcab8c 100644 --- a/doc/docs/guides/game-objects/spawn-object-pooling.md +++ b/doc/docs/guides/game-objects/spawn-object-pooling.md @@ -37,7 +37,9 @@ namespace Mirage.Examples // used by clientObjectManager.RegisterPrefab NetworkIdentity SpawnHandler(SpawnMessage msg) { - return GetFromPool(msg.position, msg.rotation); + var pos = msg.SpawnValues.Position ?? prefab.transform.position; + var rot = msg.SpawnValues.Rotation ?? prefab.transform.rotation; + return GetFromPool(pos, rot); } // used by clientObjectManager.RegisterPrefab diff --git a/doc/docs/guides/game-objects/spawn-object.md b/doc/docs/guides/game-objects/spawn-object.md index a0e2861941a..f0f124dc7bd 100644 --- a/doc/docs/guides/game-objects/spawn-object.md +++ b/doc/docs/guides/game-objects/spawn-object.md @@ -155,8 +155,51 @@ void SpawnTrees() Attach the `Tree` script to the `treePrefab` script created earlier to see this in action. +## Network Parenting + +Mirage supports synchronizing the parent-child hierarchy during the spawn process. This allows you to spawn an object and ensure it is correctly parented on all clients immediately. + +There are three modes for network parenting, defined in `SpawnParentingMode`: + +- **None**: No parenting information is sent. The object will spawn at the root of the scene (unless manually parented by a custom spawn handler). +- **Auto**: Mirage will automatically look up the transform hierarchy for the nearest `NetworkIdentity` and use it as the parent. +- **Manual**: You explicitly set the parent using the `NetworkIdentity.Parent` field or by using the `Spawn` overload that takes a parent. + +### Spawning with a Parent + +To spawn an object with a specific parent, you can use the extension method provided in `ServerObjectManagerExtensions`: + +```cs +public GameObject childPrefab; +public NetworkIdentity parentIdentity; + +void SpawnChild() +{ + GameObject childGo = Instantiate(childPrefab); + NetworkIdentity childIdentity = childGo.GetComponent(); + + // Set parenting mode to Manual if you want to explicitly specify the parent + childIdentity.SpawnSettings.SendParent = SpawnParentingMode.Manual; + + // Spawn with parent + ServerObjectManager.Spawn(childIdentity, parentIdentity); +} +``` + +### Network Spawn Settings + +The `NetworkSpawnSettings` on the `NetworkIdentity` component allows you to configure what data is sent during spawning. + +- **Send Position/Rotation/Scale**: Whether to synchronize the transform values. +- **Send Name**: Whether to synchronize the game object's name. +- **Send Active**: How to handle the active state of the game object. +- **Send Parent**: The `SpawnParentingMode` to use for this object. + +![Spawn Settings](/img/guides/game-objects/spawn-settings.png) + ### Constraints - A NetworkIdentity must be on the root game object of a spawnable Prefab. Without this, the Network Manager can’t register the Prefab. +- When using `SpawnParentingMode.Auto`, the parent `NetworkIdentity` must already be spawned and visible to the client receiving the spawn message. ## Game Object Creation Flow