diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 0b27befc3..6ba2d1734 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -32,6 +32,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Changed +- Changed `NetworkObjectReference` and `NetworkBehaviourReference` to allow null references when constructing and serializing. (#2874) - Changed `NetworkAnimator` no longer requires the `Animator` component to exist on the same `GameObject`. (#2872) - Changed `NetworkTransform` to now use `NetworkTransformMessage` as opposed to named messages for NetworkTransformState updates. (#2810) - Changed `CustomMessageManager` so it no longer attempts to register or "unregister" a null or empty string and will log an error if this condition occurs. (#2807) diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkBehaviourReference.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkBehaviourReference.cs index 6df91b8f5..4bc978cd3 100644 --- a/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkBehaviourReference.cs +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkBehaviourReference.cs @@ -11,6 +11,7 @@ namespace Unity.Netcode { private NetworkObjectReference m_NetworkObjectReference; private ushort m_NetworkBehaviourId; + private static ushort s_NullId = ushort.MaxValue; /// /// Creates a new instance of the struct. @@ -21,7 +22,9 @@ namespace Unity.Netcode { if (networkBehaviour == null) { - throw new ArgumentNullException(nameof(networkBehaviour)); + m_NetworkObjectReference = new NetworkObjectReference((NetworkObject)null); + m_NetworkBehaviourId = s_NullId; + return; } if (networkBehaviour.NetworkObject == null) { @@ -60,6 +63,10 @@ namespace Unity.Netcode [MethodImpl(MethodImplOptions.AggressiveInlining)] private static NetworkBehaviour GetInternal(NetworkBehaviourReference networkBehaviourRef, NetworkManager networkManager = null) { + if (networkBehaviourRef.m_NetworkBehaviourId == s_NullId) + { + return null; + } if (networkBehaviourRef.m_NetworkObjectReference.TryGet(out NetworkObject networkObject, networkManager)) { return networkObject.GetNetworkBehaviourAtOrderIndex(networkBehaviourRef.m_NetworkBehaviourId); diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkObjectReference.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkObjectReference.cs index dc910440e..c2f1ba46f 100644 --- a/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkObjectReference.cs +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkObjectReference.cs @@ -10,6 +10,7 @@ namespace Unity.Netcode public struct NetworkObjectReference : INetworkSerializable, IEquatable { private ulong m_NetworkObjectId; + private static ulong s_NullId = ulong.MaxValue; /// /// The of the referenced . @@ -24,13 +25,13 @@ namespace Unity.Netcode /// Creates a new instance of the struct. /// /// The to reference. - /// /// public NetworkObjectReference(NetworkObject networkObject) { if (networkObject == null) { - throw new ArgumentNullException(nameof(networkObject)); + m_NetworkObjectId = s_NullId; + return; } if (networkObject.IsSpawned == false) @@ -45,16 +46,20 @@ namespace Unity.Netcode /// 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)); + m_NetworkObjectId = s_NullId; + return; } - var networkObject = gameObject.GetComponent() ?? throw new ArgumentException($"Cannot create {nameof(NetworkObjectReference)} from {nameof(GameObject)} without a {nameof(NetworkObject)} component."); + var networkObject = gameObject.GetComponent(); + if (!networkObject) + { + 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."); @@ -80,10 +85,14 @@ namespace Unity.Netcode /// /// The reference. /// The networkmanager. Uses to resolve if null. - /// The resolves . Returns null if the networkobject was not found + /// The resolved . Returns null if the networkobject was not found [MethodImpl(MethodImplOptions.AggressiveInlining)] private static NetworkObject Resolve(NetworkObjectReference networkObjectRef, NetworkManager networkManager = null) { + if (networkObjectRef.m_NetworkObjectId == s_NullId) + { + return null; + } networkManager = networkManager ?? NetworkManager.Singleton; networkManager.SpawnManager.SpawnedObjects.TryGetValue(networkObjectRef.m_NetworkObjectId, out NetworkObject networkObject); diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs index 472d6176c..8cf3edcae 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkBehaviourReferenceTests.cs @@ -17,6 +17,8 @@ namespace Unity.Netcode.RuntimeTests { private class TestNetworkBehaviour : NetworkBehaviour { + public static bool ReceivedRPC; + public NetworkVariable TestVariable = new NetworkVariable(); public TestNetworkBehaviour RpcReceivedBehaviour; @@ -25,6 +27,7 @@ namespace Unity.Netcode.RuntimeTests public void SendReferenceServerRpc(NetworkBehaviourReference value) { RpcReceivedBehaviour = (TestNetworkBehaviour)value; + ReceivedRPC = true; } } @@ -57,8 +60,43 @@ namespace Unity.Netcode.RuntimeTests Assert.AreEqual(testNetworkBehaviour, testNetworkBehaviour.RpcReceivedBehaviour); } + [UnityTest] + public IEnumerator TestSerializeNull([Values] bool initializeWithNull) + { + TestNetworkBehaviour.ReceivedRPC = false; + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + var testNetworkBehaviour = networkObjectContext.Object.gameObject.AddComponent(); + networkObjectContext.Object.Spawn(); + using var otherObjectContext = UnityObjectContext.CreateNetworkObject(); + otherObjectContext.Object.Spawn(); + // If not initializing with null, then use the default constructor with no assigned NetworkBehaviour + if (!initializeWithNull) + { + testNetworkBehaviour.SendReferenceServerRpc(new NetworkBehaviourReference()); + } + else // Otherwise, initialize and pass in null as the reference + { + testNetworkBehaviour.SendReferenceServerRpc(new NetworkBehaviourReference(null)); + } + + // wait for rpc completion + float t = 0; + while (!TestNetworkBehaviour.ReceivedRPC) + { + t += Time.deltaTime; + if (t > 5f) + { + new AssertionException("RPC with NetworkBehaviour reference hasn't been received"); + } + + yield return null; + } + + // validate + Assert.AreEqual(null, testNetworkBehaviour.RpcReceivedBehaviour); + } [UnityTest] public IEnumerator TestRpcImplicitNetworkBehaviour() @@ -89,6 +127,7 @@ namespace Unity.Netcode.RuntimeTests Assert.AreEqual(testNetworkBehaviour, testNetworkBehaviour.RpcReceivedBehaviour); } + [Test] public void TestNetworkVariable() { @@ -131,15 +170,6 @@ namespace Unity.Netcode.RuntimeTests }); } - [Test] - public void FailSerializeNullBehaviour() - { - Assert.Throws(() => - { - NetworkBehaviourReference outReference = null; - }); - } - public void Dispose() { //Stop, shutdown, and destroy diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs index 8b29d17e6..ef459dea4 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Serialization/NetworkObjectReferenceTests.cs @@ -19,6 +19,7 @@ namespace Unity.Netcode.RuntimeTests { private class TestNetworkBehaviour : NetworkBehaviour { + public static bool ReceivedRPC; public NetworkVariable TestVariable = new NetworkVariable(); public NetworkObject RpcReceivedNetworkObject; @@ -28,6 +29,7 @@ namespace Unity.Netcode.RuntimeTests [ServerRpc] public void SendReferenceServerRpc(NetworkObjectReference value) { + ReceivedRPC = true; RpcReceivedGameObject = value; RpcReceivedNetworkObject = value; } @@ -150,6 +152,60 @@ namespace Unity.Netcode.RuntimeTests Assert.AreEqual(networkObject, result); } + public enum NetworkObjectConstructorTypes + { + None, + NullNetworkObject, + NullGameObject + } + + [UnityTest] + public IEnumerator TestSerializeNull([Values] NetworkObjectConstructorTypes networkObjectConstructorTypes) + { + TestNetworkBehaviour.ReceivedRPC = false; + using var networkObjectContext = UnityObjectContext.CreateNetworkObject(); + var testNetworkBehaviour = networkObjectContext.Object.gameObject.AddComponent(); + networkObjectContext.Object.Spawn(); + + switch (networkObjectConstructorTypes) + { + case NetworkObjectConstructorTypes.None: + { + testNetworkBehaviour.SendReferenceServerRpc(new NetworkObjectReference()); + break; + } + case NetworkObjectConstructorTypes.NullNetworkObject: + { + testNetworkBehaviour.SendReferenceServerRpc(new NetworkObjectReference((NetworkObject)null)); + break; + } + case NetworkObjectConstructorTypes.NullGameObject: + { + testNetworkBehaviour.SendReferenceServerRpc(new NetworkObjectReference((GameObject)null)); + break; + } + } + + + // wait for rpc completion + float t = 0; + while (!TestNetworkBehaviour.ReceivedRPC) + { + + t += Time.deltaTime; + if (t > 5f) + { + new AssertionException("RPC with NetworkBehaviour reference hasn't been received"); + } + + yield return null; + } + + // validate + Assert.AreEqual(null, testNetworkBehaviour.RpcReceivedNetworkObject); + Assert.AreEqual(null, testNetworkBehaviour.RpcReceivedGameObject); + } + [UnityTest] public IEnumerator TestRpc() { @@ -305,24 +361,6 @@ namespace Unity.Netcode.RuntimeTests }); } - [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