Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
46a3e77
fix: spawn a hybrid instance's ghost in its NetworkManager's world
NoelStephensUnity Oct 5, 2026
caad140
test: force a client world active for the late join player spawn, re-…
NoelStephensUnity Oct 19, 2026
2395772
feat: hybrid prefab parent replication compatibility
NoelStephensUnity Oct 1, 2026
c7d0b99
test: hybrid worldPositionStays reparent under host smoothing keeps t…
NoelStephensUnity Oct 14, 2026
7416428
test: compile the Rigidbody tests that never compiled
NoelStephensUnity Sep 7, 2026
7a07027
chore: gate hybrid parent replication on UNIFIED_NETCODE
NoelStephensUnity Oct 19, 2026
ef1297f
fix: reject worldPositionStays for NGO parenting of hybrid instances
NoelStephensUnity Oct 20, 2026
4e9e5f3
fix: restore the original pose when a parent change is reverted
NoelStephensUnity Oct 20, 2026
5208f62
chore: add missing PR numbers to the Unreleased CHANGELOG entries
NoelStephensUnity Oct 20, 2026
da74221
style: reword the comments added by this PR
NoelStephensUnity Oct 20, 2026
50df251
style: reword four NetworkObject parenting comments
NoelStephensUnity Oct 20, 2026
e9e2c20
test: guard 3D rigidbody fixtures on COM_UNITY_MODULES_PHYSICS only
NoelStephensUnity Oct 20, 2026
0c35f08
Merge remote-tracking branch 'origin/develop-3.x.x' into fix/hybrid-s…
NoelStephensUnity Oct 20, 2026
f19e6ef
fix: reject spawning a hybrid instance in a session without hybrid pr…
NoelStephensUnity Oct 20, 2026
d94365f
Merge branch 'develop-3.x.x' into fix/hybrid-spawn-world-selection
NoelStephensUnity Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ Additional documentation and release notes are available at [Multiplayer Documen

### Fixed

- Issue where a client's clock kept alternating between running slightly fast and slightly slow on every frame, even after it had synchronized with the server. Interpolated objects juddered (a periodic motion artifact caused by mismatched frame rates): they moved in uneven steps instead of smoothly. Barely noticeable at the default `NetworkTimeSystem.AdjustmentRatio`, and clearly visible at higher values.
- Issue where the IL post-processor could not resolve core library types, such as `Guid` and `List<T>`, in assemblies compiled against .NET reference assemblies, for example when targeting .NET 10. Those split the core library across `System.Runtime`, `System.Collections` and other assemblies.
- Issue where a rejected parent change, for example one made by a client without parenting authority, restored the `NetworkObject`'s parent but left it at the moved position when the change was made with `worldPositionStays` disabled. The original position, rotation and scale are now restored. (#4202)
- Issue where a client's clock kept alternating between running slightly fast and slightly slow on every frame, even after it had synchronized with the server. Interpolated objects juddered (a periodic motion artifact caused by mismatched frame rates): they moved in uneven steps instead of smoothly. Barely noticeable at the default `NetworkTimeSystem.AdjustmentRatio`, and clearly visible at higher values. (#4195)
- Issue where the IL post-processor could not resolve core library types, such as `Guid` and `List<T>`, in assemblies compiled against .NET reference assemblies, for example when targeting .NET 10. Those split the core library across `System.Runtime`, `System.Collections` and other assemblies. (#4188)
- 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)
- Issue where moving a `NetworkObject` into another scene made the clients that did not observe it log "Trying to synchronize NetworkObjectId but it was not spawned". The scene migration is now only sent to the clients that observe the `NetworkObject`. (#4176)
- Issue where a `NetworkObject` that was moved into another scene while hidden from a client spawned in that client's active scene when it was shown with `NetworkShow`, instead of the scene it is in on the server. (#4176)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
return;
}

// NGO spawns the ghost when the NetworkObject spawns. This assures the ghost spawns in that NetworkManager's world.
ghostAdapter.DelaySpawning = true;

Check warning on line 41 in com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs#L41

Added line #L41 was not covered by tests

#if COM_UNITY_MODULES_PHYSICS
var rigidBody = GetComponent<Rigidbody>();
var ghostRigidBody = GetComponent<GhostRigidbody>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ public bool Contains(NetworkPrefab prefab)
#if UNIFIED_NETCODE
internal const string DistributedAuthorityHybridPrefabError = "Distributed authority does not support hybrid prefabs.";
internal const string HybridPrefabAfterStartError = "Hybrid prefabs must be registered before the NetworkManager starts.";
internal const string HybridSpawnWithoutHybridPrefabsError = "Hybrid prefab instances can only be spawned in a session that started with a hybrid prefab registered.";

internal bool HasGhostPrefabs { get; private set; }

Expand Down
195 changes: 189 additions & 6 deletions com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,79 @@
GhostObject.ApplyPostTransformMatrixScale(scale);
}
}

/// <summary>
/// Returns true when N4E replicates this instance's parent. NGO parenting is disabled for the instance in that case.
/// </summary>
internal bool GhostReplicatesParent()
{
return HasGhost && GhostObject.ParentReplication;
}

private bool WarnIfGhostReplicatesParent(string methodName)
{
if (!GhostReplicatesParent())
{
return false;
}
var networkManager = NetworkManager;
if (networkManager == null || networkManager.LogLevel <= LogLevel.Normal)
{
NetworkLog.LogWarning($"[{name}] {methodName} was ignored. {nameof(GhostObject)}.{nameof(GhostObject.ParentReplication)} is enabled, so " +

Check warning on line 438 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L435-L438

Added lines #L435 - L438 were not covered by tests
$"Netcode for Entities replicates the parent of this hybrid instance. Use {nameof(Transform)}.{nameof(Transform.SetParent)} instead.");
}
return true;

Check warning on line 441 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L440-L441

Added lines #L440 - L441 were not covered by tests
}

/// <summary>
/// NGO parenting of a hybrid instance only supports worldPositionStays disabled.
/// </summary>
/// <remarks>
/// The ghost replicates its local transform. With worldPositionStays enabled, the authority rebases that local transform when it reparents.
/// Clients apply the ParentSyncMessage before the snapshot with the rebased values arrives, so the object pops.
/// </remarks>
/// <returns>true when the parent change is rejected</returns>
private bool RejectHybridWorldPositionStays(bool worldPositionStays)
{
if (!HasGhost || !worldPositionStays || GhostObject.ParentReplication)
{
return false;
}
var networkManager = NetworkManager;
if (networkManager == null || networkManager.LogLevel <= LogLevel.Error)
{
NetworkLog.LogError($"[{name}] Hybrid instances can only be parented with worldPositionStays disabled. Use {nameof(TrySetParent)} " +

Check warning on line 461 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L458-L461

Added lines #L458 - L461 were not covered by tests
$"or {nameof(TryRemoveParent)} with worldPositionStays set to false, or enable {nameof(GhostObject)}.{nameof(GhostObject.ParentReplication)}.");
}
return true;

Check warning on line 464 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L463-L464

Added lines #L463 - L464 were not covered by tests
}

// Same tolerances as GhostObject. They are above the error a SetParent round trip leaves behind.
private const float k_LocalSpaceEpsilon = 1e-4f;
private const float k_LocalSpaceAngleEpsilon = 0.01f;

/// <summary>
/// Returns true when parenting occurred with world position stays set to false.
/// </summary>
private bool LocalTransformKeptByParentChange()
{
var currentTransform = transform;
return Vector3.Distance(currentTransform.localPosition, m_LocalPositionBeforeParentChange) <= k_LocalSpaceEpsilon

Check warning on line 477 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L475-L477

Added lines #L475 - L477 were not covered by tests
&& Quaternion.Angle(currentTransform.localRotation, m_LocalRotationBeforeParentChange) <= k_LocalSpaceAngleEpsilon
&& Vector3.Distance(currentTransform.localScale, m_LocalScaleBeforeParentChange) <= k_LocalSpaceEpsilon;
}

Check warning on line 480 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L480

Added line #L480 was not covered by tests

/// <summary>
/// Logs a warning when a hybrid instance is parented under a non-hybrid one or vice versa.
/// </summary>
private void LogHybridParentingArtifacts(NetworkObject parentObject)
{
if (parentObject != null && parentObject.HasGhost != HasGhost)
{
NetworkLog.LogWarning($"[{name}] Parented under {parentObject.name}, but only one of them is a hybrid instance. Their transforms are " +

Check warning on line 489 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L486-L489

Added lines #L486 - L489 were not covered by tests
$"synchronized and interpolated on different timelines, so the child can jitter relative to its parent while either one moves.");
}
}

Check warning on line 492 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L491-L492

Added lines #L491 - L492 were not covered by tests
#endif
/// <summary>
/// This is intentionally private since this is a sealed class.
Expand Down Expand Up @@ -2462,6 +2535,49 @@
private Transform m_CachedParent; // What is our last set parent Transform reference?
private bool m_CachedWorldPositionStays = true; // Used to preserve the world position stays parameter passed in TrySetParent

// The local transform's state before a parent change. These values are used if the parent change is reverted.
private Vector3 m_LocalPositionBeforeParentChange;
private Quaternion m_LocalRotationBeforeParentChange;
private Vector3 m_LocalScaleBeforeParentChange;
private bool m_RevertingParentChange;

private void OnBeforeTransformParentChanged()
{
// Skips the parent change RevertParentChange makes, so the values it restores are not overwritten.
if (m_RevertingParentChange)
{
return;
}
var currentTransform = transform;
m_LocalPositionBeforeParentChange = currentTransform.localPosition;
m_LocalRotationBeforeParentChange = currentTransform.localRotation;
m_LocalScaleBeforeParentChange = currentTransform.localScale;
}

/// <summary>
/// Restores the cached parent and the local transform prior to the parent change.
/// </summary>
/// <remarks>
/// Setting <see cref="Transform.parent"/> keeps the world position. That only restores the original pose when the change also kept the world position.
/// Restoring the local transform under the cached parent works for either worldPositionStays value.
/// </remarks>
private void RevertParentChange()
{
m_RevertingParentChange = true;
try
{
var currentTransform = transform;
currentTransform.SetParent(m_CachedParent, false);
currentTransform.localPosition = m_LocalPositionBeforeParentChange;
currentTransform.localRotation = m_LocalRotationBeforeParentChange;
currentTransform.localScale = m_LocalScaleBeforeParentChange;
}
finally
{
m_RevertingParentChange = false;
}
}

/// <summary>
/// With distributed authority, we need to have a way to determine if the parenting action is authorized.
/// This is set when handling an incoming ParentSyncMessage and when running as a DAHost and a client has disconnected.
Expand Down Expand Up @@ -2517,6 +2633,12 @@
/// <returns>Whether or not re-parenting was successful.</returns>
public bool TrySetParent(Transform parent, bool worldPositionStays = true)
{
#if UNIFIED_NETCODE
if (WarnIfGhostReplicatesParent(nameof(TrySetParent)))
{
return false;

Check warning on line 2639 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L2638-L2639

Added lines #L2638 - L2639 were not covered by tests
}
#endif
// If we are removing ourself from a parent
if (parent == null)
{
Expand All @@ -2537,6 +2659,12 @@
/// <returns>Whether or not re-parenting was successful.</returns>
public bool TrySetParent(GameObject parent, bool worldPositionStays = true)
{
#if UNIFIED_NETCODE
if (WarnIfGhostReplicatesParent(nameof(TrySetParent)))
{
return false;

Check warning on line 2665 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L2664-L2665

Added lines #L2664 - L2665 were not covered by tests
}
#endif
// If we are removing ourself from a parent
if (parent == null)
{
Expand Down Expand Up @@ -2567,6 +2695,12 @@
/// <returns>True if the parent was successfully removed, false if the operation failed or the object was already parentless</returns>
public bool TryRemoveParent(bool worldPositionStays = true)
{
#if UNIFIED_NETCODE
if (WarnIfGhostReplicatesParent(nameof(TryRemoveParent)))
{
return false;

Check warning on line 2701 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L2700-L2701

Added lines #L2700 - L2701 were not covered by tests
}
#endif
return TrySetParent((NetworkObject)null, worldPositionStays);
}

Expand All @@ -2578,6 +2712,12 @@
/// <returns>Whether or not reparenting was successful.</returns>
public bool TrySetParent(NetworkObject parent, bool worldPositionStays = true)
{
#if UNIFIED_NETCODE
if (WarnIfGhostReplicatesParent(nameof(TrySetParent)) || RejectHybridWorldPositionStays(worldPositionStays))
{
return false;

Check warning on line 2718 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L2717-L2718

Added lines #L2717 - L2718 were not covered by tests
}
#endif
if (!AutoObjectParentSync || !IsSpawned || !NetworkManagerOwner.IsListening)
{
return false;
Expand Down Expand Up @@ -2619,19 +2759,27 @@
private void OnTransformParentChanged()
{
var networkManager = NetworkManager;
if (!AutoObjectParentSync || (networkManager != null && networkManager.ShutdownInProgress))
if (m_RevertingParentChange || !AutoObjectParentSync || (networkManager != null && networkManager.ShutdownInProgress))
{
return;
}

#if UNIFIED_NETCODE
// No warning: this also runs for the parent changes N4E applies.
if (GhostReplicatesParent())
{
return;

Check warning on line 2771 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L2770-L2771

Added lines #L2770 - L2771 were not covered by tests
}
#endif

if (transform.parent == m_CachedParent)
{
return;
}

if (networkManager == null || !networkManager.IsListening)
{
transform.parent = m_CachedParent;
RevertParentChange();
// We want to log at any LogLevel, since we may not have a network manager may here.
NetworkLog.LogError($"[{name}] {nameof(networkManager)} is not listening, start a server or host before re-parenting.");
return;
Expand All @@ -2649,7 +2797,7 @@
}
else
{
transform.parent = m_CachedParent;
RevertParentChange();

Check warning on line 2800 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L2800

Added line #L2800 was not covered by tests
if (networkManager.LogLevel <= LogLevel.Error)
{
NetworkLog.LogErrorServer($"[{name}] {nameof(NetworkObject)} can only be re-parented after being spawned!");
Expand All @@ -2664,7 +2812,7 @@
// If we are spawned and don't have authority; reset the parent back to the cached parent and exit
if (!isParentingAuthority)
{
transform.parent = m_CachedParent;
RevertParentChange();
if (networkManager.LogLevel <= LogLevel.Error)
{
if (networkManager.DistributedAuthorityMode)
Expand All @@ -2679,13 +2827,29 @@
return;
}

#if UNIFIED_NETCODE
// A direct Transform parent change has no worldPositionStays value. It is derived from whether the local transform was kept.
if (HasGhost && !AuthorityAppliedParenting && !GhostObject.ParentReplication)
{
if (LocalTransformKeptByParentChange())
{
m_CachedWorldPositionStays = false;
}
else if (RejectHybridWorldPositionStays(true))
{
RevertParentChange();
return;

Check warning on line 2841 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L2833-L2841

Added lines #L2833 - L2841 were not covered by tests
}
}

Check warning on line 2843 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L2843

Added line #L2843 was not covered by tests
#endif

var removeParent = false;
var parentTransform = transform.parent;
if (parentTransform != null)
{
if (!transform.parent.TryGetComponent(out NetworkObject parentObject))
{
transform.parent = m_CachedParent;
RevertParentChange();
AuthorityAppliedParenting = false;
if (networkManager.LogLevel <= LogLevel.Error)
{
Expand All @@ -2695,7 +2859,7 @@
}
if (!parentObject.IsSpawned)
{
transform.parent = m_CachedParent;
RevertParentChange();

Check warning on line 2862 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L2862

Added line #L2862 was not covered by tests
AuthorityAppliedParenting = false;
if (networkManager.LogLevel <= LogLevel.Error)
{
Expand All @@ -2705,6 +2869,12 @@
}

m_LatestParent = parentObject.NetworkObjectId;
#if UNIFIED_NETCODE
if (networkManager.LogLevel <= LogLevel.Developer)
{
LogHybridParentingArtifacts(parentObject);
}

Check warning on line 2876 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L2874-L2876

Added lines #L2874 - L2876 were not covered by tests
#endif
}
else
{
Expand Down Expand Up @@ -2785,6 +2955,14 @@
return false;
}

#if UNIFIED_NETCODE
// NGO does not track this parent. Applying "no parent" here would undo the parent N4E replicated.
if (GhostReplicatesParent())
{
return false;

Check warning on line 2962 in com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

View check run for this annotation

Codecov GitHub.com / codecov/patch

com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs#L2961-L2962

Added lines #L2961 - L2962 were not covered by tests
}
#endif

// SPECIAL CASE:
// The ignoreNotSpawned is a special case scenario where a late joining client has joined
// and loaded one or more scenes that contain nested in-scene placed NetworkObject children
Expand Down Expand Up @@ -3576,7 +3754,12 @@
{
var obj = new SerializedObject
{
#if UNIFIED_NETCODE
// The ghost's spawn carries its own parent.
HasParent = transform.parent != null && !GhostReplicatesParent(),
#else
HasParent = transform.parent != null,
#endif
WorldPositionStays = m_CachedWorldPositionStays,
NetworkObjectId = NetworkObjectId,
OwnerClientId = OwnerClientId,
Expand Down
Loading
Loading