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);
}