diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkBehaviourReference.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkBehaviourReference.cs
new file mode 100644
index 0000000000..596831bf8b
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkBehaviourReference.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Runtime.CompilerServices;
+
+namespace Unity.Netcode
+{
+ ///
+ /// A helper struct for serializing s over the network. Can be used in RPCs and .
+ /// Note: network ids get recycled by the NetworkManager after a while. So a reference pointing to
+ ///
+ public struct NetworkBehaviourReference : INetworkSerializable, IEquatable
+ {
+ private NetworkObjectReference m_NetworkObjectReference;
+ private ushort m_NetworkBehaviourId;
+
+ ///
+ /// Creates a new instance of the struct.
+ ///
+ /// The to reference.
+ ///
+ public NetworkBehaviourReference(NetworkBehaviour networkBehaviour)
+ {
+ if (networkBehaviour == null)
+ {
+ throw new ArgumentNullException(nameof(networkBehaviour));
+ }
+ if (networkBehaviour.NetworkObject == null)
+ {
+ throw new ArgumentException($"Cannot create {nameof(NetworkBehaviourReference)} from {nameof(NetworkBehaviour)} without a {nameof(NetworkObject)}.");
+ }
+
+ m_NetworkObjectReference = networkBehaviour.NetworkObject;
+ m_NetworkBehaviourId = networkBehaviour.NetworkBehaviourId;
+ }
+
+ ///
+ /// Tries to get the referenced by this reference.
+ ///
+ /// The which was found. Null if the corresponding was not found.
+ /// The networkmanager. Uses to resolve if null.
+ /// True if the was found; False if the was not found. This can happen if the corresponding has not been spawned yet. you can try getting the reference at a later point in time.
+ public bool TryGet(out NetworkBehaviour networkBehaviour, NetworkManager networkManager = null)
+ {
+ networkBehaviour = GetInternal(this, null);
+ return networkBehaviour != null;
+ }
+
+ ///
+ /// Tries to get the referenced by this reference.
+ ///
+ /// The which was found. Null if the corresponding was not found.
+ /// The networkmanager. Uses to resolve if null.
+ /// The type of the networkBehaviour for convenience.
+ /// True if the was found; False if the was not found. This can happen if the corresponding has not been spawned yet. you can try getting the reference at a later point in time.
+ public bool TryGet(out T networkBehaviour, NetworkManager networkManager = null) where T : NetworkBehaviour
+ {
+ networkBehaviour = (T)GetInternal(this, null);
+ return networkBehaviour != null;
+ }
+
+ ///
+ public void NetworkSerialize(NetworkSerializer serializer)
+ {
+ m_NetworkObjectReference.NetworkSerialize(serializer);
+ serializer.Serialize(ref m_NetworkBehaviourId);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static NetworkBehaviour GetInternal(NetworkBehaviourReference networkBehaviourRef, NetworkManager networkManager = null)
+ {
+ if (networkBehaviourRef.m_NetworkObjectReference.TryGet(out NetworkObject networkObject, networkManager))
+ {
+ return networkObject.GetNetworkBehaviourAtOrderIndex(networkBehaviourRef.m_NetworkBehaviourId);
+ }
+
+ return null;
+ }
+
+ ///
+ public bool Equals(NetworkBehaviourReference other)
+ {
+ return m_NetworkObjectReference.Equals(other.m_NetworkObjectReference) && m_NetworkBehaviourId == other.m_NetworkBehaviourId;
+ }
+
+ ///
+ public override bool Equals(object obj)
+ {
+ return obj is NetworkBehaviourReference other && Equals(other);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (m_NetworkObjectReference.GetHashCode() * 397) ^ m_NetworkBehaviourId.GetHashCode();
+ }
+ }
+
+ public static implicit operator NetworkBehaviour(NetworkBehaviourReference networkBehaviourRef) => GetInternal(networkBehaviourRef);
+
+ public static implicit operator NetworkBehaviourReference(NetworkBehaviour networkBehaviour) => new NetworkBehaviourReference(networkBehaviour);
+ }
+}
diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkBehaviourReference.cs.meta b/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkBehaviourReference.cs.meta
new file mode 100644
index 0000000000..a65b9efd02
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkBehaviourReference.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0a9cea52f48ea70499020aebe4073ba8
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkObjectReference.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkObjectReference.cs
new file mode 100644
index 0000000000..783749ec32
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkObjectReference.cs
@@ -0,0 +1,131 @@
+using System;
+using System.Runtime.CompilerServices;
+using UnityEngine;
+
+namespace Unity.Netcode
+{
+ ///
+ /// A helper struct for serializing s over the network. Can be used in RPCs and .
+ ///
+ public struct NetworkObjectReference : INetworkSerializable, IEquatable
+ {
+ private ulong m_NetworkObjectId;
+
+ ///
+ /// The of the referenced .
+ ///
+ public ulong NetworkObjectId
+ {
+ get => m_NetworkObjectId;
+ internal set => m_NetworkObjectId = value;
+ }
+
+ ///
+ /// Creates a new instance of the struct.
+ ///
+ /// The to reference.
+ ///
+ ///
+ public NetworkObjectReference(NetworkObject networkObject)
+ {
+ if (networkObject == null)
+ {
+ throw new ArgumentNullException(nameof(networkObject));
+ }
+
+ if (networkObject.IsSpawned == false)
+ {
+ throw new ArgumentException($"{nameof(NetworkObjectReference)} can only be created from spawned {nameof(NetworkObject)}s.");
+ }
+
+ m_NetworkObjectId = networkObject.NetworkObjectId;
+ }
+
+ ///
+ /// Creates a new instance of the struct.
+ ///
+ /// The GameObject from which the component will be referenced.
+ ///
+ ///
+ public NetworkObjectReference(GameObject gameObject)
+ {
+ if (gameObject == null)
+ {
+ throw new ArgumentNullException(nameof(gameObject));
+ }
+
+ var networkObject = gameObject.GetComponent();
+
+ if (networkObject == null)
+ {
+ throw new ArgumentException($"Cannot create {nameof(NetworkObjectReference)} from {nameof(GameObject)} without a {nameof(NetworkObject)} component.");
+ }
+
+ if (networkObject.IsSpawned == false)
+ {
+ throw new ArgumentException($"{nameof(NetworkObjectReference)} can only be created from spawned {nameof(NetworkObject)}s.");
+ }
+
+ m_NetworkObjectId = networkObject.NetworkObjectId;
+ }
+
+ ///
+ /// Tries to get the referenced by this reference.
+ ///
+ /// The which was found. Null if no object was found.
+ /// The networkmanager. Uses to resolve if null.
+ /// True if the was found; False if the was not found. This can happen if the has not been spawned yet. you can try getting the reference at a later point in time.
+ public bool TryGet(out NetworkObject networkObject, NetworkManager networkManager = null)
+ {
+ networkObject = Resolve(this, networkManager);
+ return networkObject != null;
+ }
+
+ ///
+ public void NetworkSerialize(NetworkSerializer serializer)
+ {
+ serializer.Serialize(ref m_NetworkObjectId);
+ }
+
+ ///
+ /// Resolves the corresponding for this reference.
+ ///
+ /// The reference.
+ /// The networkmanager. Uses to resolve if null.
+ /// The resolves . Returns null if the networkobject was not found
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static NetworkObject Resolve(NetworkObjectReference networkObjectRef, NetworkManager networkManager = null)
+ {
+ networkManager = networkManager != null ? networkManager : NetworkManager.Singleton;
+ networkManager.SpawnManager.SpawnedObjects.TryGetValue(networkObjectRef.m_NetworkObjectId, out NetworkObject networkObject);
+
+ return networkObject;
+ }
+
+ ///
+ public bool Equals(NetworkObjectReference other)
+ {
+ return m_NetworkObjectId == other.m_NetworkObjectId;
+ }
+
+ ///
+ public override bool Equals(object obj)
+ {
+ return obj is NetworkObjectReference other && Equals(other);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return m_NetworkObjectId.GetHashCode();
+ }
+
+ public static implicit operator NetworkObject(NetworkObjectReference networkObjectRef) => Resolve(networkObjectRef);
+
+ public static implicit operator NetworkObjectReference(NetworkObject networkObject) => new NetworkObjectReference(networkObject);
+
+ public static implicit operator GameObject(NetworkObjectReference networkObjectRef) => Resolve(networkObjectRef).gameObject;
+
+ public static implicit operator NetworkObjectReference(GameObject gameObject) => new NetworkObjectReference(gameObject);
+ }
+}
diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkObjectReference.cs.meta b/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkObjectReference.cs.meta
new file mode 100644
index 0000000000..fecff26098
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkObjectReference.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 463f3b530aad5d849964ee157646818e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Serialization.meta b/com.unity.netcode.gameobjects/Tests/Runtime/Serialization.meta
new file mode 100644
index 0000000000..1f1dc0f8b1
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/Serialization.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 90d51da7691e302498265bba08c43636
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs
new file mode 100644
index 0000000000..bcc7119a00
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs
@@ -0,0 +1,151 @@
+using System;
+using System.Collections;
+using NUnit.Framework;
+using UnityEngine;
+using UnityEngine.TestTools;
+
+namespace Unity.Netcode.RuntimeTests.Serialization
+{
+ ///
+ /// Unit tests to test:
+ /// - Serializing NetworkObject to NetworkObjectReference
+ /// - Deserializing NetworkObjectReference to NetworkObject
+ /// - Implicit operators of NetworkObjectReference
+ ///
+ public class NetworkBehaviourReferenceTests : IDisposable
+ {
+ private class TestNetworkBehaviour : NetworkBehaviour
+ {
+ public NetworkVariable TestVariable = new NetworkVariable();
+
+ public TestNetworkBehaviour RpcReceivedBehaviour;
+
+ [ServerRpc]
+ public void SendReferenceServerRpc(NetworkBehaviourReference value)
+ {
+ RpcReceivedBehaviour = (TestNetworkBehaviour)value;
+ }
+ }
+
+ [UnityTest]
+ public IEnumerator TestRpc()
+ {
+ using var networkObjectContext = UnityObjectContext.CreateNetworkObject();
+ var testNetworkBehaviour = networkObjectContext.Object.gameObject.AddComponent();
+ networkObjectContext.Object.Spawn();
+
+ using var otherObjectContext = UnityObjectContext.CreateNetworkObject();
+ otherObjectContext.Object.Spawn();
+
+ testNetworkBehaviour.SendReferenceServerRpc(new NetworkBehaviourReference(testNetworkBehaviour));
+
+ // wait for rpc completion
+ float t = 0;
+ while (testNetworkBehaviour.RpcReceivedBehaviour == null)
+ {
+ t += Time.deltaTime;
+ if (t > 5f)
+ {
+ new AssertionException("RPC with NetworkBehaviour reference hasn't been received");
+ }
+
+ yield return null;
+ }
+
+ // validate
+ Assert.AreEqual(testNetworkBehaviour, testNetworkBehaviour.RpcReceivedBehaviour);
+ }
+
+ [UnityTest]
+ public IEnumerator TestRpcImplicitNetworkBehaviour()
+ {
+ using var networkObjectContext = UnityObjectContext.CreateNetworkObject();
+ var testNetworkBehaviour = networkObjectContext.Object.gameObject.AddComponent();
+ networkObjectContext.Object.Spawn();
+
+ using var otherObjectContext = UnityObjectContext.CreateNetworkObject();
+ otherObjectContext.Object.Spawn();
+
+ testNetworkBehaviour.SendReferenceServerRpc(testNetworkBehaviour);
+
+ // wait for rpc completion
+ float t = 0;
+ while (testNetworkBehaviour.RpcReceivedBehaviour == null)
+ {
+ t += Time.deltaTime;
+ if (t > 5f)
+ {
+ new AssertionException("RPC with NetworkBehaviour reference hasn't been received");
+ }
+
+ yield return null;
+ }
+
+ // validate
+ Assert.AreEqual(testNetworkBehaviour, testNetworkBehaviour.RpcReceivedBehaviour);
+ }
+
+ [Test]
+ public void TestNetworkVariable()
+ {
+ using var networkObjectContext = UnityObjectContext.CreateNetworkObject();
+ var testNetworkBehaviour = networkObjectContext.Object.gameObject.AddComponent();
+ networkObjectContext.Object.Spawn();
+
+ using var otherObjectContext = UnityObjectContext.CreateNetworkObject();
+ otherObjectContext.Object.Spawn();
+
+ // check default value is null
+ Assert.IsNull((NetworkBehaviour)testNetworkBehaviour.TestVariable.Value);
+
+ testNetworkBehaviour.TestVariable.Value = testNetworkBehaviour;
+
+ Assert.AreEqual((NetworkBehaviour)testNetworkBehaviour.TestVariable.Value, testNetworkBehaviour);
+ }
+
+ [Test]
+ public void FailSerializeNonSpawnedNetworkObject()
+ {
+ using var networkObjectContext = UnityObjectContext.CreateNetworkObject();
+ var component = networkObjectContext.Object.gameObject.AddComponent();
+
+ Assert.Throws(() =>
+ {
+ NetworkBehaviourReference outReference = component;
+ });
+ }
+
+ [Test]
+ public void FailSerializeGameObjectWithoutNetworkObject()
+ {
+ using var gameObjectContext = UnityObjectContext.CreateGameObject();
+ var component = gameObjectContext.Object.gameObject.AddComponent();
+
+ Assert.Throws(() =>
+ {
+ NetworkBehaviourReference outReference = component;
+ });
+ }
+
+ [Test]
+ public void FailSerializeNullBehaviour()
+ {
+ Assert.Throws(() =>
+ {
+ NetworkBehaviourReference outReference = null;
+ });
+ }
+
+ public void Dispose()
+ {
+ //Stop, shutdown, and destroy
+ NetworkManagerHelper.ShutdownNetworkManager();
+ }
+
+ public NetworkBehaviourReferenceTests()
+ {
+ //Create, instantiate, and host
+ NetworkManagerHelper.StartNetworkManager(out _);
+ }
+ }
+}
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs.meta
new file mode 100644
index 0000000000..6ae11f3c32
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0fca807d195f9fc49a400cfce86b085d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs
new file mode 100644
index 0000000000..4d2c3b3cf4
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs
@@ -0,0 +1,335 @@
+using System;
+using System.Collections;
+using NUnit.Framework;
+using UnityEngine;
+using UnityEngine.TestTools;
+using Object = UnityEngine.Object;
+
+namespace Unity.Netcode.RuntimeTests.Serialization
+{
+ ///
+ /// Unit tests to test:
+ /// - Serializing NetworkObject to NetworkObjectReference
+ /// - Deserializing NetworkObjectReference to NetworkObject
+ /// - Implicit operators of NetworkObjectReference
+ ///
+ public class NetworkObjectReferenceTests : IDisposable
+ {
+ private class TestNetworkBehaviour : NetworkBehaviour
+ {
+ public NetworkVariable TestVariable = new NetworkVariable();
+
+ public NetworkObject RpcReceivedNetworkObject;
+
+ public GameObject RpcReceivedGameObject;
+
+ [ServerRpc]
+ public void SendReferenceServerRpc(NetworkObjectReference value)
+ {
+ RpcReceivedGameObject = value;
+ RpcReceivedNetworkObject = value;
+ }
+ }
+
+ [Test]
+ public void TestSerializeNetworkObject()
+ {
+ using var networkObjectContext = UnityObjectContext.CreateNetworkObject();
+ networkObjectContext.Object.Spawn();
+ using var outStream = PooledNetworkBuffer.Get();
+ using var outWriter = PooledNetworkWriter.Get(outStream);
+ using var inStream = PooledNetworkBuffer.Get();
+ using var inReader = PooledNetworkReader.Get(inStream);
+
+ // serialize
+ var outSerializer = new NetworkSerializer(outWriter);
+ NetworkObjectReference outReference = networkObjectContext.Object;
+ outReference.NetworkSerialize(outSerializer);
+
+ // deserialize
+ NetworkObjectReference inReference = default;
+ inStream.Write(outStream.ToArray());
+ inStream.Position = 0;
+ var inSerializer = new NetworkSerializer(inReader);
+ inReference.NetworkSerialize(inSerializer);
+
+ // validate
+ Assert.NotNull((NetworkObject)inReference);
+ Assert.AreEqual(inReference.NetworkObjectId, networkObjectContext.Object.NetworkObjectId);
+ Assert.AreEqual(outReference, inReference);
+ Assert.AreEqual(networkObjectContext.Object, (NetworkObject)inReference);
+ }
+
+ [Test]
+ public void TestSerializeGameObject()
+ {
+ using var networkObjectContext = UnityObjectContext.CreateNetworkObject();
+ networkObjectContext.Object.Spawn();
+ using var outStream = PooledNetworkBuffer.Get();
+ using var outWriter = PooledNetworkWriter.Get(outStream);
+ using var inStream = PooledNetworkBuffer.Get();
+ using var inReader = PooledNetworkReader.Get(inStream);
+
+ // serialize
+ var outSerializer = new NetworkSerializer(outWriter);
+ NetworkObjectReference outReference = networkObjectContext.Object.gameObject;
+ outReference.NetworkSerialize(outSerializer);
+
+ // deserialize
+ NetworkObjectReference inReference = default;
+ inStream.Write(outStream.ToArray());
+ inStream.Position = 0;
+ var inSerializer = new NetworkSerializer(inReader);
+ inReference.NetworkSerialize(inSerializer);
+ GameObject gameObject = inReference;
+
+ // validate
+ Assert.AreEqual(outReference, inReference);
+ Assert.AreEqual(networkObjectContext.Object.gameObject, gameObject);
+ }
+
+ [Test]
+ public void TestTryGet()
+ {
+ using var networkObjectContext = UnityObjectContext.CreateNetworkObject();
+ networkObjectContext.Object.Spawn();
+
+ NetworkObjectReference networkObjectReference = networkObjectContext.Object;
+
+ Assert.True(networkObjectReference.TryGet(out NetworkObject networkObject));
+ Assert.NotNull(networkObject);
+ networkObjectReference.TryGet(out NetworkObject result);
+ Assert.AreEqual(networkObject, result);
+ }
+
+ [UnityTest]
+ public IEnumerator TestRpc()
+ {
+ using var networkObjectContext = UnityObjectContext.CreateNetworkObject();
+ var testNetworkBehaviour = networkObjectContext.Object.gameObject.AddComponent();
+ networkObjectContext.Object.Spawn();
+
+ using var otherObjectContext = UnityObjectContext.CreateNetworkObject();
+ otherObjectContext.Object.Spawn();
+
+ testNetworkBehaviour.SendReferenceServerRpc(new NetworkObjectReference(otherObjectContext.Object));
+
+ // wait for rpc completion
+ float t = 0;
+ while (testNetworkBehaviour.RpcReceivedGameObject == null)
+ {
+ t += Time.deltaTime;
+ if (t > 5f)
+ {
+ new AssertionException("RPC with NetworkBehaviour reference hasn't been received");
+ }
+
+ yield return null;
+ }
+
+ // validate
+ Assert.AreEqual(otherObjectContext.Object, testNetworkBehaviour.RpcReceivedNetworkObject);
+ Assert.AreEqual(otherObjectContext.Object.gameObject, testNetworkBehaviour.RpcReceivedGameObject);
+ }
+
+ [UnityTest]
+ public IEnumerator TestRpcImplicitNetworkObject()
+ {
+ using var networkObjectContext = UnityObjectContext.CreateNetworkObject();
+ var testNetworkBehaviour = networkObjectContext.Object.gameObject.AddComponent();
+ networkObjectContext.Object.Spawn();
+
+ using var otherObjectContext = UnityObjectContext.CreateNetworkObject();
+ otherObjectContext.Object.Spawn();
+
+ testNetworkBehaviour.SendReferenceServerRpc(otherObjectContext.Object);
+
+ // wait for rpc completion
+ float t = 0;
+ while (testNetworkBehaviour.RpcReceivedGameObject == null)
+ {
+ t += Time.deltaTime;
+ if (t > 5f)
+ {
+ new AssertionException("RPC with NetworkBehaviour reference hasn't been received");
+ }
+
+ yield return null;
+ }
+
+ // validate
+ Assert.AreEqual(otherObjectContext.Object, testNetworkBehaviour.RpcReceivedNetworkObject);
+ Assert.AreEqual(otherObjectContext.Object.gameObject, testNetworkBehaviour.RpcReceivedGameObject);
+ }
+
+ [UnityTest]
+ public IEnumerator TestRpcImplicitGameObject()
+ {
+ using var networkObjectContext = UnityObjectContext.CreateNetworkObject();
+ var testNetworkBehaviour = networkObjectContext.Object.gameObject.AddComponent();
+ networkObjectContext.Object.Spawn();
+
+ using var otherObjectContext = UnityObjectContext.CreateNetworkObject();
+ otherObjectContext.Object.Spawn();
+
+ testNetworkBehaviour.SendReferenceServerRpc(otherObjectContext.Object.gameObject);
+
+ // wait for rpc completion
+ float t = 0;
+ while (testNetworkBehaviour.RpcReceivedGameObject == null)
+ {
+ t += Time.deltaTime;
+ if (t > 5f)
+ {
+ new AssertionException("RPC with NetworkBehaviour reference hasn't been received");
+ }
+
+ yield return null;
+ }
+
+ // validate
+ Assert.AreEqual(otherObjectContext.Object, testNetworkBehaviour.RpcReceivedNetworkObject);
+ Assert.AreEqual(otherObjectContext.Object.gameObject, testNetworkBehaviour.RpcReceivedGameObject);
+ }
+
+ [Test]
+ public void TestNetworkVariable()
+ {
+ using var networkObjectContext = UnityObjectContext.CreateNetworkObject();
+ var testNetworkBehaviour = networkObjectContext.Object.gameObject.AddComponent();
+ networkObjectContext.Object.Spawn();
+
+ using var otherObjectContext = UnityObjectContext.CreateNetworkObject();
+ otherObjectContext.Object.Spawn();
+
+ // check default value is null
+ Assert.IsNull((NetworkObject)testNetworkBehaviour.TestVariable.Value);
+
+ testNetworkBehaviour.TestVariable.Value = networkObjectContext.Object;
+
+ Assert.AreEqual((GameObject)testNetworkBehaviour.TestVariable.Value, networkObjectContext.Object.gameObject);
+ Assert.AreEqual((NetworkObject)testNetworkBehaviour.TestVariable.Value, networkObjectContext.Object);
+ }
+
+ [Test]
+ public void TestDespawn()
+ {
+ using var networkObjectContext = UnityObjectContext.CreateNetworkObject();
+ networkObjectContext.Object.Spawn();
+ var originalId = networkObjectContext.Object.NetworkObjectId;
+
+ NetworkObjectReference networkObjectReference = networkObjectContext.Object;
+ Assert.AreEqual(networkObjectContext.Object, (NetworkObject)networkObjectReference);
+
+ networkObjectContext.Object.Despawn();
+ Assert.IsFalse(networkObjectReference.TryGet(out NetworkObject _));
+
+ networkObjectContext.Object.Spawn();
+
+ // After spawning again the reference will still no longer work as it still points to the old object
+ Assert.AreNotEqual(originalId, networkObjectContext.Object.NetworkObjectId);
+ Assert.IsFalse(networkObjectReference.TryGet(out NetworkObject _));
+
+ // creating a new reference will make it work again
+ networkObjectReference = networkObjectContext.Object;
+ Assert.AreEqual(networkObjectContext.Object, (NetworkObject)networkObjectReference);
+ }
+
+ [Test]
+ public void FailSerializeNonSpawnedNetworkObject()
+ {
+ using var networkObjectContext = UnityObjectContext.CreateNetworkObject();
+
+ Assert.Throws(() =>
+ {
+ NetworkObjectReference outReference = networkObjectContext.Object;
+ });
+ }
+
+ [Test]
+ public void FailSerializeGameObjectWithoutNetworkObject()
+ {
+ using var gameObjectContext = UnityObjectContext.CreateGameObject();
+
+ Assert.Throws(() =>
+ {
+ NetworkObjectReference outReference = gameObjectContext.Object;
+ });
+ }
+
+ [Test]
+ public void FailSerializeNullNetworkObject()
+ {
+ Assert.Throws(() =>
+ {
+ NetworkObjectReference outReference = (NetworkObject)null;
+ });
+ }
+
+ [Test]
+ public void FailSerializeNullGameObject()
+ {
+ Assert.Throws(() =>
+ {
+ NetworkObjectReference outReference = (GameObject)null;
+ });
+ }
+
+ public void Dispose()
+ {
+ //Stop, shutdown, and destroy
+ NetworkManagerHelper.ShutdownNetworkManager();
+ }
+
+ public NetworkObjectReferenceTests()
+ {
+ //Create, instantiate, and host
+ NetworkManagerHelper.StartNetworkManager(out _);
+ }
+ }
+
+ ///
+ /// Helper method for tests to create and destroy Unity Objects.
+ ///
+ /// The type of Object this context incorporates.
+ public class UnityObjectContext : UnityObjectContext where T : Object
+ {
+ private T m_Object;
+
+ internal UnityObjectContext(T unityObject, Object root)
+ : base(root)
+ {
+ m_Object = unityObject;
+ }
+
+ public T Object => m_Object;
+ }
+
+ public class UnityObjectContext : IDisposable
+ {
+ private Object m_Root;
+
+ protected UnityObjectContext(Object root)
+ {
+ m_Root = root;
+ }
+
+ public static UnityObjectContext CreateGameObject(string name = "")
+ {
+ var gameObject = new GameObject(name);
+ return new UnityObjectContext(gameObject, gameObject);
+ }
+
+ public static UnityObjectContext CreateNetworkObject(string name = "")
+ {
+ var gameObject = new GameObject(name);
+ var networkObject = gameObject.AddComponent();
+ return new UnityObjectContext(networkObject, gameObject);
+ }
+
+ public void Dispose()
+ {
+ Object.DestroyImmediate(m_Root);
+ }
+ }
+}
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs.meta
new file mode 100644
index 0000000000..bcaad14452
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 182c2000b73248b4bbdd79f70ec90cd2
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/testproject/Assets/Scripts/PingPongMover.cs b/testproject/Assets/Scripts/PingPongMover.cs
new file mode 100644
index 0000000000..4e93ffdb6b
--- /dev/null
+++ b/testproject/Assets/Scripts/PingPongMover.cs
@@ -0,0 +1,23 @@
+using UnityEngine;
+
+public class PingPongMover : MonoBehaviour
+{
+ public Vector3 Direction;
+ public float Time;
+
+ private Vector3 m_StartPosition;
+
+ // Start is called before the first frame update
+ private void Start()
+ {
+ m_StartPosition = transform.position;
+ }
+
+ // Update is called once per frame
+ private void Update()
+ {
+ var t = Mathf.PingPong(UnityEngine.Time.time, Time);
+ var offset = Vector3.Lerp(Vector3.zero, Direction, t);
+ transform.position = m_StartPosition + offset;
+ }
+}
diff --git a/testproject/Assets/Scripts/PingPongMover.cs.meta b/testproject/Assets/Scripts/PingPongMover.cs.meta
new file mode 100644
index 0000000000..2606d32f7f
--- /dev/null
+++ b/testproject/Assets/Scripts/PingPongMover.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1ce1983eca18fbf449477e5e138f44a2
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: