diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 9be4e4fba7..d33a474a71 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -18,6 +18,8 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed +- Fixed issue where a prefab added to a `NetworkPrefabsList` while a session was running was registered more than once and logged a duplicate `GlobalObjectIdHash` error. (#4184) + ### Security ### Obsolete diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs index bb6c6b54d8..4e8b643db3 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs @@ -21,13 +21,6 @@ public abstract class NetworkRigidbodyBase : NetworkBehaviour internal bool NetworkRigidbodyBaseExpanded; #endif - // TODO-UNIFIED: - // Provide an option to automatically remove the NetworkRigidbodyBase component at runtime if it is a hybrid prefab that is spawned since this - // component is primarily used for the Rigidbody interpolation and extrapolation features of NetworkTransform which are not relevant for a hybrid - // prefab that is spawned since it will be using N4E's built in interpolation and extrapolation features. This greatly improves performance on - // the client side. If using N4E prediction or distributed authority mode, then Rigibody and any component derived from this should always be used. - - /// /// When enabled, the associated will use the Rigidbody/Rigidbody2D to apply and synchronize changes in position, rotation, and /// allows for the use of Rigidbody interpolation/extrapolation. @@ -133,6 +126,13 @@ protected void Initialize(RigidbodyTypes rigidbodyType, NetworkTransform network NetworkTransform = GetComponent(); } +#if UNIFIED_NETCODE + if (InitializeHybrid()) + { + return; + } +#endif + if (NetworkTransform != null) { NetworkTransform.RegisterRigidbody(this); @@ -184,6 +184,13 @@ protected void Initialize(RigidbodyTypes rigidbodyType, NetworkTransform network NetworkTransform = GetComponent(); } +#if UNIFIED_NETCODE + if (InitializeHybrid()) + { + return; + } +#endif + if (NetworkTransform != null) { NetworkTransform.RegisterRigidbody(this); @@ -201,11 +208,6 @@ protected void Initialize(RigidbodyTypes rigidbodyType, NetworkTransform network #endif #if COM_UNITY_MODULES_PHYSICS && COM_UNITY_MODULES_PHYSICS2D -#if UNIFIED_NETCODE - // Used to keep track of the original kinematic state upon awake. - // (see OnDestroy below) - private bool m_OriginalKinematicState; -#endif /// /// Initializes the networked Rigidbody based on the /// passed in as a parameter. @@ -248,6 +250,13 @@ protected void Initialize(RigidbodyTypes rigidbodyType, NetworkTransform network NetworkTransform = GetComponent(); } +#if UNIFIED_NETCODE + if (InitializeHybrid()) + { + return; + } +#endif + if (NetworkTransform != null) { NetworkTransform.RegisterRigidbody(this); @@ -259,31 +268,68 @@ protected void Initialize(RigidbodyTypes rigidbodyType, NetworkTransform network if (AutoUpdateKinematicState) { -#if UNIFIED_NETCODE - // Keep track of the original kinematic state. (see OnDestroy) - m_OriginalKinematicState = IsKinematic(); -#endif SetIsKinematic(true); } } +#endif #if UNIFIED_NETCODE + // The authored kinematic state, restored in OnDestroy. + private bool m_OriginalKinematicState; + + /// + /// Skips initialization on a hybrid prefab, whose GhostObject drives its motion. The component stays so values match on every peer. + /// + /// true for a hybrid prefab + private bool InitializeHybrid() + { + if (NetworkObject == null || !NetworkObject.HasGhost) + { + return false; + } + + // Clears any registration left behind by a prior Initialize call. + if (NetworkTransform != null) + { + NetworkTransform.UnregisterRigidbody(); + } + + m_OriginalKinematicState = IsKinematic(); + return true; + } + + /// + /// Kinematic on every peer except the server. Called at spawn because an in-scene placed instance has no session during Awake. + /// + private void SetHybridKinematicState() + { + if (!m_LocalNetworkManager.IsServer) + { + SetIsKinematic(true); + } + } + + /// public override void OnDestroy() { base.OnDestroy(); - // If the user has left this component on their prefab and this is a hybrid prefab, - // then we want to set the rigid body back to its original kinematic settings since - // we are automatically destroying these components at runtime when it is a hybrid - // prefab that is spawned. - if (NetworkObject && NetworkObject.HasGhost) + if (!NetworkObject || !NetworkObject.HasGhost) { - if (m_InternalRigidbody || m_InternalRigidbody2D) - { - SetIsKinematic(m_OriginalKinematicState); - } + return; } - } +#if COM_UNITY_MODULES_PHYSICS && COM_UNITY_MODULES_PHYSICS2D + if (m_InternalRigidbody || m_InternalRigidbody2D) #endif +#if COM_UNITY_MODULES_PHYSICS && !COM_UNITY_MODULES_PHYSICS2D + if (m_InternalRigidbody) +#endif +#if !COM_UNITY_MODULES_PHYSICS && COM_UNITY_MODULES_PHYSICS2D + if (m_InternalRigidbody2D) +#endif + { + SetIsKinematic(m_OriginalKinematicState); + } + } #endif internal Vector3 GetAdjustedPositionThreshold() { @@ -1019,7 +1065,12 @@ public void ResetInterpolation() protected override void OnOwnershipChanged(ulong previous, ulong current) { - UpdateOwnershipAuthority(); +#if UNIFIED_NETCODE + if (!NetworkObject.HasGhost) +#endif + { + UpdateOwnershipAuthority(); + } base.OnOwnershipChanged(previous, current); } @@ -1062,6 +1113,13 @@ internal override void InternalOnNetworkPreSpawn(ref NetworkManager networkManag /// public override void OnNetworkSpawn() { +#if UNIFIED_NETCODE + if (NetworkObject.HasGhost) + { + SetHybridKinematicState(); + return; + } +#endif m_TickFrequency = 1.0f / m_LocalNetworkManager.NetworkConfig.TickRate; m_TickRate = m_LocalNetworkManager.NetworkConfig.TickRate; UpdateOwnershipAuthority(); diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody.cs index ffd82ff5f4..590fad77b7 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody.cs @@ -9,10 +9,7 @@ namespace Unity.Netcode.Components /// mode of the and disabling it on all peers but the authoritative one. /// [RequireComponent(typeof(NetworkTransform))] - // TODO-UNIFIED: We should not require this for unified and come up with a different way of handling the dependency -#if !UNIFIED_NETCODE [RequireComponent(typeof(Rigidbody))] -#endif [AddComponentMenu("Netcode/Network Rigidbody")] [HelpURL(HelpUrls.NetworkRigidbody)] public class NetworkRigidbody : NetworkRigidbodyBase diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody2D.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody2D.cs index 8912a2cff0..dd84d52252 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody2D.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidbody2D.cs @@ -9,10 +9,7 @@ namespace Unity.Netcode.Components /// mode of the rigidbody and disabling it on all peers but the authoritative one. /// [RequireComponent(typeof(NetworkTransform))] - // TODO-UNIFIED: We should not require this for unified and come up with a different way of handling the dependency -#if !UNIFIED_NETCODE [RequireComponent(typeof(Rigidbody2D))] -#endif [AddComponentMenu("Netcode/Network Rigidbody 2D")] [HelpURL(HelpUrls.NetworkRigidbody2D)] public class NetworkRigidbody2D : NetworkRigidbodyBase diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs index 338d1f991f..614dc26f50 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs @@ -1908,6 +1908,13 @@ private bool ShouldSynchronizeHalfFloat(ulong targetClientId) /// protected override void OnSynchronize(ref BufferSerializer serializer) { +#if UNIFIED_NETCODE + // No transform state is synchronized for a hybrid prefab. + if (NetworkObject.HasGhost) + { + return; + } +#endif var targetClientId = m_TargetIdBeingSynchronized; SynchronizeState = new NetworkTransformState() { @@ -3527,6 +3534,13 @@ private void NonAuthorityFinalizeSynchronization() /// protected internal override void InternalOnNetworkSessionSynchronized() { +#if UNIFIED_NETCODE + // Nothing was synchronized for a hybrid prefab. + if (NetworkObject.HasGhost) + { + return; + } +#endif NonAuthorityFinalizeSynchronization(); base.InternalOnNetworkSessionSynchronized(); @@ -3548,6 +3562,12 @@ private void ApplyPlayerTransformState() /// protected internal override void InternalOnNetworkPostSpawn() { +#if UNIFIED_NETCODE + if (NetworkObject.HasGhost) + { + return; + } +#endif // This is a special case for client-server where a server is spawning an owner authoritative NetworkObject but has yet to serialize anything. // When the server detects that: // - We are not in a distributed authority session (DAHost check). @@ -3643,9 +3663,6 @@ internal override void InternalOnNetworkPreSpawn(ref NetworkManager networkManag public override void OnNetworkSpawn() { #if UNIFIED_NETCODE - // TODO-UNIFIED: - // Provide a notification to users that NetworkTransform component will be removed at runtime if it is a hybrid prefab that is spawned since - // it will be using N4E's built in interpolation and extrapolation features. if (NetworkObject.HasGhost) { return; @@ -3743,9 +3760,7 @@ private void ResetInterpolatedStateToCurrentAuthoritativeState() internal virtual void InternalInitialization(bool isOwnershipChange = false) { #if UNIFIED_NETCODE - // TODO-UNIFIED: - // Provide a notification to users that NetworkTransform component will be removed at runtime if it is a hybrid prefab that is spawned since - // it will be using N4E's built in interpolation and extrapolation features. + // Inert on a hybrid prefab, but kept so NetworkBehaviourId values match on every peer. if (NetworkObject.HasGhost) { return; @@ -3932,6 +3947,13 @@ private void DefaultParentChanged() internal override void InternalOnNetworkObjectParentChanged(NetworkObject parentNetworkObject) { +#if UNIFIED_NETCODE + // A hybrid prefab's transform space is driven by its GhostObject. + if (NetworkObject.HasGhost) + { + return; + } +#endif if (!SwitchTransformSpaceWhenParented) { // Motion authority doesn't need to adjust anything diff --git a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkConfig.cs b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkConfig.cs index 00e7719e4a..13a5756f69 100644 --- a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkConfig.cs +++ b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkConfig.cs @@ -388,6 +388,17 @@ internal void InitializePrefabs() Prefabs.Initialize(); } +#if UNIFIED_NETCODE + /// + /// Registers any prefab list assigned after Awake, and returns false for a distributed authority session with a hybrid prefab registered. + /// + internal bool InitializePrefabsForStart() + { + InitializePrefabs(); + return NetworkTopology != NetworkTopologyTypes.DistributedAuthority || Prefabs.ValidateForDistributedAuthority(); + } +#endif + [NonSerialized] private bool m_DidWarnOldPrefabList = false; diff --git a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefab.cs b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefab.cs index a873404b3c..68725f3bbd 100644 --- a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefab.cs +++ b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefab.cs @@ -29,6 +29,7 @@ public enum NetworkPrefabOverride /// Class that represents a NetworkPrefab /// [Serializable] + [System.Diagnostics.DebuggerDisplay("{GetDebugName()}")] public class NetworkPrefab { /// @@ -154,7 +155,8 @@ public uint TargetPrefabGlobalObjectIdHash /// True if the NetworkPrefab is valid and ready for use, false otherwise public bool Validate(int index = -1) { - NetworkObject networkObject; + // Null for a hash override. + NetworkObject networkObject = null; if (Override == NetworkPrefabOverride.None) { if (Prefab == null) @@ -270,9 +272,43 @@ public bool Validate(int index = -1) return false; } +#if UNIFIED_NETCODE + // N4E spawns the ghost's own prefab on every client, so the override would never be applied. + if ((networkObject != null && networkObject.HasGhost) + || (OverridingTargetPrefab.TryGetComponent(out NetworkObject targetNetworkObject) && targetNetworkObject.HasGhost)) + { + NetworkLog.LogError($"{HybridPrefabOverrideError} {GetDebugName()} (entry will be ignored)."); + return false; + } +#endif return true; } +#if UNIFIED_NETCODE + internal const string HybridPrefabOverrideError = "NetworkPrefab overrides are not supported for hybrid prefabs yet."; +#endif + + /// + /// Names the prefab, including its override target, for logs and the debugger. + /// + internal string GetDebugName() + { + switch (Override) + { + case NetworkPrefabOverride.Prefab: + return $"{GetName(SourcePrefabToOverride)} (overridden by {GetName(OverridingTargetPrefab)})"; + case NetworkPrefabOverride.Hash: + return $"{SourceHashToOverride} (overridden by {GetName(OverridingTargetPrefab)})"; + default: + return GetName(Prefab); + } + } + + private static string GetName(GameObject prefab) + { + return prefab != null ? prefab.name : "null"; + } + /// /// Returns a string representation of this NetworkPrefab's source and target hash values /// diff --git a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabs.cs b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabs.cs index 0603ba44a7..bfe91891b3 100644 --- a/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabs.cs +++ b/com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabs.cs @@ -172,6 +172,9 @@ internal void Shutdown() list.OnAdd -= AddTriggeredByNetworkPrefabList; list.OnRemove -= RemoveTriggeredByNetworkPrefabList; } +#if UNIFIED_NETCODE + m_RejectGhostPrefabs = false; +#endif } /// @@ -183,10 +186,17 @@ public void Initialize(bool warnInvalid = true) { m_PrefabHashIds.Clear(); m_Prefabs.Clear(); +#if UNIFIED_NETCODE + // Recomputed by the registrations below. + HasGhostPrefabs = false; +#endif NetworkPrefabsLists.RemoveAll(x => x == null); foreach (var list in NetworkPrefabsLists) { + // Initialize runs more than once per session, so unsubscribe first to keep a single subscription. + list.OnAdd -= AddTriggeredByNetworkPrefabList; list.OnAdd += AddTriggeredByNetworkPrefabList; + list.OnRemove -= RemoveTriggeredByNetworkPrefabList; list.OnRemove += RemoveTriggeredByNetworkPrefabList; } @@ -356,7 +366,41 @@ public bool Contains(NetworkPrefab prefab) } #if UNIFIED_NETCODE + internal const string DistributedAuthorityHybridPrefabError = "Distributed authority does not support hybrid prefabs."; + internal bool HasGhostPrefabs { get; private set; } + + // Cleared in Shutdown. + private bool m_RejectGhostPrefabs; + + /// + /// A distributed authority session rejects hybrid prefabs added while it runs. + /// + internal void OnSessionStarting(bool distributedAuthority) + { + m_RejectGhostPrefabs = distributedAuthority; + } + + /// + /// Logs every registered hybrid prefab and returns false if there is any. + /// + internal bool ValidateForDistributedAuthority() + { + if (!HasGhostPrefabs) + { + return true; + } + var hybridPrefabNames = new List(); + foreach (var networkPrefab in m_Prefabs) + { + if (networkPrefab.HasGhost) + { + hybridPrefabNames.Add(networkPrefab.GetDebugName()); + } + } + NetworkLog.LogError($"{DistributedAuthorityHybridPrefabError} Remove the GhostObject from these prefabs or use a prefab list without them: {string.Join(", ", hybridPrefabNames)}"); + return false; + } #endif @@ -381,6 +425,12 @@ private bool AddPrefabRegistration(NetworkPrefab networkPrefab) #if UNIFIED_NETCODE if (networkPrefab.HasGhost) { + // Registering a hybrid prefab mid-session would switch NetworkManager into hybrid mode and stop its send queue. + if (m_RejectGhostPrefabs) + { + Debug.LogError($"{DistributedAuthorityHybridPrefabError} {networkPrefab.GetDebugName()} was not added."); + return false; + } HasGhostPrefabs = true; } #endif diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index 185d682f66..41f69dc2bf 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -1271,6 +1271,9 @@ internal void Initialize(bool server) NetworkConfig.NetworkTransport = gameObject.AddComponent(); } #endif +#if UNIFIED_NETCODE + NetworkConfig.Prefabs.OnSessionStarting(DistributedAuthorityMode); +#endif MetricsManager.Initialize(this); @@ -1364,6 +1367,12 @@ private bool CanStart(StartType type) } } +#if UNIFIED_NETCODE + if (!NetworkConfig.InitializePrefabsForStart()) + { + return false; + } +#endif return true; } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 0b39c84f6f..6a51c7fecd 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -3057,72 +3057,6 @@ internal bool InitializeChildNetworkBehaviours() } #endif } -#if UNIFIED_NETCODE - // For now, cycle through all known NetworkRigidbodyBase derived components - // and destroy them all if this is a hybrid prefab instance. - // This allows a user to not have to make direct adjustments until trying out their NGO prefab - // as a hybrid spawned prefab. - if (HasGhost && !NetworkManager.DistributedAuthorityMode) - { -#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D - // TODO-UNIFIED: This needs to be updated to make it "opt-in". - // If the GhostObject is not configured for prediction but is still using a Rigidbody, then go ahead and remove it on - // the client side to improve performance by default. - // TODO-UNIFIED: Determine if recent unified physics updates does not require checking for prediction. - if (NetworkRigidbodies != null) - { - var isServer = NetworkManager.IsServer; - for (int i = NetworkRigidbodies.Count - 1; i >= 0; i--) - { - var currenObject = NetworkRigidbodies[i].gameObject; - var currentHasGhostRigidBody = currenObject.GetComponent() != null; - if (!isServer) - { -#if COM_UNITY_MODULES_PHYSICS - var rigidBody = currenObject.GetComponent(); - - if (rigidBody != null && !currentHasGhostRigidBody) - { - Destroy(rigidBody); - } -#endif -#if COM_UNITY_MODULES_PHYSICS2D - var rigidBody2D = currenObject.GetComponent(); - if (rigidBody2D != null && !currentHasGhostRigidBody) - { - Destroy(rigidBody2D); - } -#endif - } - // Both the server and clients will still remove and destroy the NetworkRigidbody - // since there is no point in synchronizing these when it is handled via unified. - var networkRigidbody = NetworkRigidbodies[i]; - NetworkRigidbodies.Remove(networkRigidbody); - ChildNetworkBehaviours.Remove(networkRigidbody.NetworkBehaviourId); - Destroy(networkRigidbody); - } - } -#endif - // This is defined out since users might have derived NetworkTransforms -#if UNIFIED_NETCODE_DESTROY - // When hybrid spawning, the transform is synchronized by the GhostObject. - // As a convenience, we remove and destroy all NetworkTransforms. - // TODO-Parenting-Related-Area: We need to replicate this functionality in a GhostObject - // Possibly use a "Synchronize" property and display only on children of a root parent GhostObject. - if (NetworkTransforms != null) - { - NetworkManager.Log.Warning(new Logging.Context(LogLevel.Developer, $"[]{name} Hybrid spawned objects do not support {nameof(NetworkTransform)} and " + - $"are removed at runtime. If hybrid spawning is intended, then remove it from the network prefab to avoid allocating and de-allocating at runtime.")); - for (int i = NetworkTransforms.Count - 1; i >= 0; i--) - { - ChildNetworkBehaviours.Remove(NetworkTransforms[i].NetworkBehaviourId); - Destroy(NetworkTransforms[i]); - } - NetworkTransforms.Clear(); - } -#endif - } -#endif return true; } diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs index daf8d153c0..6d4e1987d2 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs @@ -32,7 +32,7 @@ public class NetworkPrefabHandler /// private readonly Dictionary m_PrefabInstanceToPrefabAsset = new Dictionary(); - internal static string PrefabDebugHelper(NetworkPrefab networkPrefab) => $"{nameof(NetworkPrefab)} \"{networkPrefab.Prefab.name}\""; + internal static string PrefabDebugHelper(NetworkPrefab networkPrefab) => $"{nameof(NetworkPrefab)} \"{networkPrefab.GetDebugName()}\""; /// /// Use a to register a class that implements the interface with the diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/UnifiedHybridPrefabBehaviourIdTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/UnifiedHybridPrefabBehaviourIdTests.cs new file mode 100644 index 0000000000..b7971225c1 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/UnifiedHybridPrefabBehaviourIdTests.cs @@ -0,0 +1,192 @@ +#if UNIFIED_NETCODE && COM_UNITY_MODULES_PHYSICS +using System; +using System.Collections; +using System.Text; +using NUnit.Framework; +using Unity.Netcode.Components; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// Added to the hybrid prefab last, so its would change if the + /// or were removed from the instance. + /// + internal class HybridTrailingBehaviour : NetworkBehaviour + { + } + + /// + /// A hybrid prefab keeps its and components inert on the instance.
+ /// Validates that every peer assigns the same values and that the body stays kinematic on every peer except the server.
+ ///
+ [TestFixture(HostOrServer.UnifiedHost)] + internal class UnifiedHybridPrefabBehaviourIdTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 2; + + /// + /// The types in component order, read from the prefab because the test helpers add their own.
+ /// Each index is the expected .
+ ///
+ private Type[] m_AuthoredBehaviourOrder; + + private GameObject m_Prefab; + private NetworkObject m_Instance; + + public UnifiedHybridPrefabBehaviourIdTests(HostOrServer hostOrServer) : base(hostOrServer) + { + } + + protected override bool UseUnifiedTests() + { + return true; + } + + protected override void OnServerAndClientsCreated() + { + m_Prefab = CreateNetworkObjectPrefab("HybridOrdering"); + // Owner authority makes an ungated NetworkRigidbody change the kinematic state on an ownership change. + m_Prefab.AddComponent().AuthorityMode = NetworkTransform.AuthorityModes.Owner; + m_Prefab.AddComponent(); + m_Prefab.AddComponent(); + m_Prefab.AddComponent(); + + var authoredBehaviours = m_Prefab.GetComponents(); + m_AuthoredBehaviourOrder = new Type[authoredBehaviours.Length]; + for (int i = 0; i < authoredBehaviours.Length; i++) + { + m_AuthoredBehaviourOrder[i] = authoredBehaviours[i].GetType(); + } + + base.OnServerAndClientsCreated(); + } + + [HideInCallstack] + private IEnumerator SpawnHybridInstance() + { + m_Instance = SpawnObject(m_Prefab, GetAuthorityNetworkManager()).GetComponent(); + + yield return WaitForSpawnedOnAllOrTimeOut(m_Instance); + AssertOnTimeout($"Failed to spawn {m_Instance.name} on all clients!"); + } + + /// + /// Every peer has to hold the same behaviour table for the instance: the same number of entries, the + /// same type at each index, and the same on each. + /// + private bool ValidateBehaviourTable(StringBuilder errorLog) + { + foreach (var networkManager in m_NetworkManagers) + { + var instance = networkManager.SpawnManager.SpawnedObjects[m_Instance.NetworkObjectId]; + var childBehaviours = instance.ChildNetworkBehaviours; + if (childBehaviours.Count != m_AuthoredBehaviourOrder.Length) + { + errorLog.AppendLine($"[Client-{networkManager.LocalClientId}] {nameof(NetworkObject.ChildNetworkBehaviours)} holds " + + $"{childBehaviours.Count} entries but {m_AuthoredBehaviourOrder.Length} were authored!"); + continue; + } + + for (ushort index = 0; index < m_AuthoredBehaviourOrder.Length; index++) + { + if (!childBehaviours.TryGetValue(index, out var behaviour)) + { + errorLog.AppendLine($"[Client-{networkManager.LocalClientId}] No {nameof(NetworkBehaviour)} at index {index}!"); + continue; + } + + if (behaviour.GetType() != m_AuthoredBehaviourOrder[index]) + { + errorLog.AppendLine($"[Client-{networkManager.LocalClientId}] Index {index} holds a {behaviour.GetType().Name} " + + $"but a {m_AuthoredBehaviourOrder[index].Name} was authored there!"); + } + + if (behaviour.NetworkBehaviourId != index) + { + errorLog.AppendLine($"[Client-{networkManager.LocalClientId}] {behaviour.GetType().Name} has a " + + $"{nameof(NetworkBehaviour.NetworkBehaviourId)} of {behaviour.NetworkBehaviourId} but is at index {index}!"); + } + } + + ValidateGatedComponents(networkManager, instance, errorLog); + } + + return errorLog.Length == 0; + } + + /// + /// The gated components are still present and still inert.
+ /// The body is kinematic on every peer except the server.
+ ///
+ private void ValidateGatedComponents(NetworkManager networkManager, NetworkObject instance, StringBuilder errorLog) + { + if (instance.GetComponent() == null) + { + errorLog.AppendLine($"[Client-{networkManager.LocalClientId}] The {nameof(NetworkRigidbody)} was removed from the instance!"); + } + + var isKinematic = instance.GetComponent().isKinematic; + if (isKinematic == networkManager.IsServer) + { + errorLog.AppendLine($"[Client-{networkManager.LocalClientId}] {nameof(Rigidbody.isKinematic)} is {isKinematic} but {!networkManager.IsServer} was expected!"); + } + + var networkTransform = instance.GetComponent(); + if (networkTransform == null) + { + errorLog.AppendLine($"[Client-{networkManager.LocalClientId}] The {nameof(NetworkTransform)} was removed from the instance!"); + return; + } + + // A NetworkTransform that initialized takes authority on the server and registers its NetworkObject + // for the per frame update pass everywhere else. A gated one does neither. + if (networkTransform.CanCommitToTransform) + { + errorLog.AppendLine($"[Client-{networkManager.LocalClientId}] The {nameof(NetworkTransform)} took authority over a hybrid prefab!"); + } + + if (networkManager.NetworkTransformUpdate.ContainsKey(instance.NetworkObjectId)) + { + errorLog.AppendLine($"[Client-{networkManager.LocalClientId}] The {nameof(NetworkTransform)} registered a hybrid prefab for updates!"); + } + } + + private bool ValidateOwner(StringBuilder errorLog, ulong ownerClientId) + { + foreach (var networkManager in m_NetworkManagers) + { + var ownerOnPeer = networkManager.SpawnManager.SpawnedObjects[m_Instance.NetworkObjectId].OwnerClientId; + if (ownerOnPeer != ownerClientId) + { + errorLog.AppendLine($"[Client-{networkManager.LocalClientId}] Owner is Client-{ownerOnPeer} but Client-{ownerClientId} was expected!"); + } + } + return errorLog.Length == 0; + } + + /// + /// Validates that an ownership change leaves the body kinematic on every peer except the server. + /// + [UnityTest] + public IEnumerator KinematicStateSurvivesOwnershipChange() + { + yield return SpawnHybridInstance(); + + yield return WaitForConditionOrTimeOut(ValidateBehaviourTable); + AssertOnTimeout("A peer disagreed about the hybrid prefab's behaviour table!"); + + var newOwnerClientId = m_ClientNetworkManagers[0].LocalClientId; + m_Instance.ChangeOwnership(newOwnerClientId); + + yield return WaitForConditionOrTimeOut(errorLog => ValidateOwner(errorLog, newOwnerClientId)); + AssertOnTimeout($"Ownership did not change to Client-{newOwnerClientId} on every peer!"); + + yield return WaitForConditionOrTimeOut(ValidateBehaviourTable); + AssertOnTimeout("The ownership change altered the hybrid prefab's gated components!"); + } + } +} +#endif diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/UnifiedHybridPrefabBehaviourIdTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/UnifiedHybridPrefabBehaviourIdTests.cs.meta new file mode 100644 index 0000000000..a761227255 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/UnifiedHybridPrefabBehaviourIdTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 95f1ef1b64600f04d9085d092ee72245 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/UnifiedHybridPrefabValidationTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/UnifiedHybridPrefabValidationTests.cs new file mode 100644 index 0000000000..1769656bf8 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/UnifiedHybridPrefabValidationTests.cs @@ -0,0 +1,170 @@ +#if UNIFIED_NETCODE +using System; +using System.Collections; +using System.Text.RegularExpressions; +using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// Validates the hybrid prefab registrations that are rejected:
+ /// - Any hybrid prefab in a distributed authority session. The start fails, and one added during the session is not registered.
+ /// - A NetworkPrefab override with a hybrid source or target prefab, in any topology. The entry is ignored.
+ ///
+ /// + /// The fixture's managers are not started. Each test creates its own that registers only the prefabs it needs. + /// + [TestFixture(HostOrServer.UnifiedHost)] + internal class UnifiedHybridPrefabValidationTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 0; + + protected override bool m_UseMockTransport => true; + + private GameObject m_HybridPrefab; + private GameObject m_Prefab; + private NetworkManager m_NetworkManager; + + public UnifiedHybridPrefabValidationTests(HostOrServer hostOrServer) : base(hostOrServer) + { + } + + protected override bool UseUnifiedTests() + { + return true; + } + + protected override bool CanStartServerAndClients() + { + return false; + } + + protected override void OnServerAndClientsCreated() + { + m_HybridPrefab = CreateNetworkObjectPrefab("ValidationHybrid"); + m_Prefab = CreateNetworkObjectPrefab("ValidationPrefab", false); + base.OnServerAndClientsCreated(); + } + + protected override void OnNewClientCreated(NetworkManager networkManager) + { + // Each test registers only the prefabs it needs. + } + + protected override IEnumerator OnTearDown() + { + if (m_NetworkManager != null) + { + yield return StopOneClient(m_NetworkManager, true); + m_NetworkManager = null; + } + yield return base.OnTearDown(); + } + + private NetworkManager CreateNetworkManager(NetworkTopologyTypes topology) + { + m_NetworkManager = CreateNewClient(); + m_NetworkManager.NetworkConfig.NetworkTopology = topology; + // Allows prefabs to be added during the session. + m_NetworkManager.NetworkConfig.ForceSamePrefabs = false; + return m_NetworkManager; + } + + [UnityTest] + public IEnumerator StartFailsWithHybridPrefabRegistered() + { + var networkManager = CreateNetworkManager(NetworkTopologyTypes.DistributedAuthority); + networkManager.AddNetworkPrefab(m_HybridPrefab); + var prefabs = networkManager.NetworkConfig.Prefabs; + var transport = networkManager.NetworkConfig.NetworkTransport; + var hybridPrefabError = new Regex($"{Regex.Escape(NetworkPrefabs.DistributedAuthorityHybridPrefabError)}.*{m_HybridPrefab.name}"); + + var startMethods = new (string Name, Func Start)[] + { + (nameof(NetworkManager.StartServer), networkManager.StartServer), + (nameof(NetworkManager.StartHost), networkManager.StartHost), + (nameof(NetworkManager.StartClient), networkManager.StartClient), + }; + + foreach (var startMethod in startMethods) + { + LogAssert.Expect(LogType.Error, hybridPrefabError); + Assert.IsFalse(startMethod.Start(), $"{startMethod.Name} started a distributed authority session with a hybrid prefab registered!"); + Assert.IsFalse(networkManager.IsListening, $"{startMethod.Name} left the {nameof(NetworkManager)} listening!"); + Assert.AreSame(transport, networkManager.NetworkConfig.NetworkTransport, $"{startMethod.Name} replaced the transport!"); + } + + networkManager.RemoveNetworkPrefab(m_HybridPrefab); + Assert.IsTrue(networkManager.StartHost(), "Failed to start a distributed authority session once the hybrid prefab was removed!"); + + LogAssert.Expect(LogType.Error, hybridPrefabError); + networkManager.AddNetworkPrefab(m_HybridPrefab); + Assert.IsFalse(prefabs.Contains(m_HybridPrefab), "The hybrid prefab was registered during a distributed authority session!"); + Assert.IsFalse(prefabs.HasGhostPrefabs, $"{nameof(NetworkPrefabs.HasGhostPrefabs)} was set during a distributed authority session!"); + + networkManager.Shutdown(); + yield return WaitForConditionOrTimeOut(() => !networkManager.IsListening); + AssertOnTimeout("The distributed authority session did not shut down!"); + + networkManager.AddNetworkPrefab(m_HybridPrefab); + Assert.IsTrue(prefabs.Contains(m_HybridPrefab), "The hybrid prefab was rejected after the distributed authority session ended!"); + } + + [Test] + public void FailedStartDoesNotRejectHybridPrefabs() + { + var networkManager = CreateNetworkManager(NetworkTopologyTypes.DistributedAuthority); + + // StartServer is not valid in a distributed authority session and fails after the hybrid prefab check. + LogAssert.Expect(LogType.Error, new Regex("distributed authority mode")); + Assert.IsFalse(networkManager.StartServer(), "StartServer succeeded in a distributed authority session!"); + // SetRole keeps IsServer set when it rejects the start, which makes the NetworkManager throw when destroyed. + networkManager.ConnectionManager.LocalClient.SetRole(false, false); + + networkManager.NetworkConfig.NetworkTopology = NetworkTopologyTypes.ClientServer; + networkManager.AddNetworkPrefab(m_HybridPrefab); + Assert.IsTrue(networkManager.NetworkConfig.Prefabs.Contains(m_HybridPrefab), "The hybrid prefab was rejected after a failed distributed authority start!"); + } + + [Test] + public void PrefabListAddDuringSessionRegistersOnce() + { + var networkManager = CreateNetworkManager(NetworkTopologyTypes.ClientServer); + var prefabList = ScriptableObject.CreateInstance(); + networkManager.NetworkConfig.Prefabs.NetworkPrefabsLists.Add(prefabList); + Assert.IsTrue(networkManager.StartHost(), "Failed to start the session!"); + + // A second subscription to the list would register the prefab twice and log a duplicate registration error. + prefabList.Add(new NetworkPrefab() { Prefab = m_Prefab }); + Assert.IsTrue(networkManager.NetworkConfig.Prefabs.Contains(m_Prefab), "The prefab added to the list was not registered!"); + UnityEngine.Object.Destroy(prefabList); + } + + /// + /// Validates that an override with a hybrid source or target is ignored, with an error naming it. + /// + [Test] + public void HybridOverrideIsRejected() + { + var prefabs = CreateNetworkManager(NetworkTopologyTypes.ClientServer).NetworkConfig.Prefabs; + var overrides = new[] + { + new NetworkPrefab() { Override = NetworkPrefabOverride.Prefab, SourcePrefabToOverride = m_Prefab, OverridingTargetPrefab = m_HybridPrefab }, + new NetworkPrefab() { Override = NetworkPrefabOverride.Hash, SourceHashToOverride = m_Prefab.GetComponent().GlobalObjectIdHash, OverridingTargetPrefab = m_HybridPrefab }, + new NetworkPrefab() { Override = NetworkPrefabOverride.Prefab, SourcePrefabToOverride = m_HybridPrefab, OverridingTargetPrefab = m_Prefab }, + }; + + foreach (var hybridOverride in overrides) + { + var debugName = hybridOverride.GetDebugName(); + LogAssert.Expect(LogType.Error, new Regex($"{Regex.Escape(NetworkPrefab.HybridPrefabOverrideError)}.*{Regex.Escape(debugName)}")); + Assert.IsFalse(prefabs.Add(hybridOverride), $"[{debugName}] The override was registered!"); + Assert.IsFalse(prefabs.HasGhostPrefabs, $"[{debugName}] {nameof(NetworkPrefabs.HasGhostPrefabs)} was set by a rejected override!"); + } + } + } +} +#endif diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/UnifiedHybridPrefabValidationTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/UnifiedHybridPrefabValidationTests.cs.meta new file mode 100644 index 0000000000..20334057c4 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/UnifiedHybridPrefabValidationTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9dfaff802302483786620552292de1bc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs index 3d24452348..aab259cc27 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs @@ -2526,7 +2526,18 @@ internal void WaitForMessagesReceivedWithTimeTravel(List messagesInOrder, protected GameObject CreateNetworkObjectPrefab(string baseName) { #if UNIFIED_NETCODE - if (m_AllPrefabsAsHybrid) + return CreateNetworkObjectPrefab(baseName, m_AllPrefabsAsHybrid); + } + + /// + /// Creates a hybrid prefab only when is true, so a unified test can create a NetworkObject-only prefab. + /// + /// the basic name to be used for each instance + /// when true, the prefab also has a GhostObject + /// The assigned to the new NetworkPrefab entry + protected GameObject CreateNetworkObjectPrefab(string baseName, bool asHybrid) + { + if (asHybrid) { return CreateHybridPrefab(baseName, true); }