Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Assets/Mirage/Components/NetworkParent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using UnityEngine;

namespace Mirage.Components
{
/// <summary>
/// An NetworkBehaviour class to represent a parent object over the network
/// </summary>
public class NetworkParent : NetworkBehaviour
{
}
}
11 changes: 11 additions & 0 deletions Assets/Mirage/Components/NetworkParent.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 14 additions & 1 deletion Assets/Mirage/Runtime/ClientObjectManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
}

Expand Down
18 changes: 18 additions & 0 deletions Assets/Mirage/Runtime/Extensions/ServerObjectManagerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,24 @@ public static void Spawn(this ServerObjectManager som, GameObject obj, INetworkP
som.Spawn(identity, owner);
}

/// <summary>
/// Spawns the <paramref name="identity"/> with a specific <paramref name="parent"/> and assigns <paramref name="owner"/> to be it's owner.
/// <para>This will set the transform parent on the server and ensure the client receives the parent information if <see cref="NetworkSpawnSettings.SendParent"/> is enabled.</para>
/// </summary>
/// <param name="identity">The object to spawn.</param>
/// <param name="parent">The parent for the object.</param>
/// <param name="owner">The connection that has authority over the object.</param>
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);
}


/// <summary>
/// Instantiate a prefab an then Spawns it with ServerObjectManager
Expand Down
42 changes: 42 additions & 0 deletions Assets/Mirage/Runtime/Messages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ public struct SpawnValues
public Vector3? Scale;
public string Name;
public bool? SelfActive;
public NetworkReferenceId? Parent;

[ThreadStatic]
private static StringBuilder builder;
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -185,4 +189,42 @@ public struct NetworkPongMessage
public double ClientTime;
public double ServerTime;
}

public struct NetworkReferenceId : IEquatable<NetworkReferenceId>
{
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}]";
}
}
}
23 changes: 16 additions & 7 deletions Assets/Mirage/Runtime/NetworkIdentity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// Explicit parent override for this object. Used by <see cref="SpawnParentingMode.Manual"/>.
/// Can be a NetworkIdentity or a NetworkBehaviour.
/// </summary>
[Tooltip("Explicit parent override for this object. Used by SpawnParentingMode.Manual.")]
public Component Parent;

/// <summary>
/// The NetworkClient associated with this NetworkIdentity.
/// </summary>
Expand Down Expand Up @@ -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;
Expand All @@ -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();
}
Expand Down
26 changes: 24 additions & 2 deletions Assets/Mirage/Runtime/NetworkSpawnSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}


Expand All @@ -53,4 +57,22 @@ public enum SyncActiveOption
/// </summary>
ForceEnable,
}

public enum SpawnParentingMode
{
/// <summary>
/// Don't synchronize parent-child relationship.
/// </summary>
None,

/// <summary>
/// Automatically detect parent NetworkIdentity in the transform hierarchy.
/// </summary>
Auto,

/// <summary>
/// Manually specify the parent NetworkIdentity via the <see cref="NetworkIdentity.Parent"/> field.
/// </summary>
Manual,
}
}
26 changes: 26 additions & 0 deletions Assets/Mirage/Runtime/ServerObjectManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<NetworkBehaviour>();
parentIdentity = identity.transform.parent?.GetComponentInParent<NetworkIdentity>();
}

if (parentIdentity != null && parentIdentity.NetId != 0)
{
values.Parent = new NetworkReferenceId
{
NetId = parentIdentity.NetId,
ComponentIndex = parentBehaviour != null ? (byte?)parentBehaviour.ComponentIndex : null
};
}
}

return values;
}

Expand Down
110 changes: 110 additions & 0 deletions Assets/Tests/Runtime/ClientServer/ClientObjectManagerParentingTest.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading