Skip to content

Commit 25c1115

Browse files
Merge branch 'develop-3.x.x' into fix/hybrid-prefab-component-removal
2 parents c043168 + 5313d3f commit 25c1115

19 files changed

Lines changed: 833 additions & 38 deletions

‎com.unity.netcode.gameobjects/CHANGELOG.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ Additional documentation and release notes are available at [Multiplayer Documen
2424
### Fixed
2525

2626
- Issue where a client promoted to session owner could throw an exception when synchronizing a newly joining client. (#4214)
27-
- 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.
28-
- 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.
27+
- 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)
28+
- 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)
29+
- 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)
2930
- 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)
3031
- 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)
3132
- 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)

‎com.unity.netcode.gameobjects/Runtime/Components/Helpers/NetworkObjectBridge.cs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,18 @@ private void OnValidate()
2727
{
2828
return;
2929
}
30-
30+
3131
var networkObject = GetComponent<NetworkObject>();
3232
if (networkObject == null)
3333
{
34+
// Remove the bridge if there is no NetworkObject
3435
EditorApplication.delayCall += () => DestroyImmediate(this, true);
3536
}
37+
else
38+
{
39+
// NGO spawns the ghost when the NetworkObject spawns. This assures the ghost spawns in that NetworkManager's world.
40+
ghostAdapter.DelaySpawning = true;
41+
}
3642
}
3743
#endif
3844

‎com.unity.netcode.gameobjects/Runtime/Configuration/NetworkPrefabs.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ public bool Contains(NetworkPrefab prefab)
368368
#if UNIFIED_NETCODE
369369
internal const string DistributedAuthorityHybridPrefabError = "Distributed authority does not support hybrid prefabs.";
370370
internal const string HybridPrefabAfterStartError = "Hybrid prefabs must be registered before the NetworkManager starts.";
371+
internal const string HybridSpawnWithoutHybridPrefabsError = "Hybrid prefab instances can only be spawned in a session that started with a hybrid prefab registered.";
371372

372373
internal bool HasGhostPrefabs { get; private set; }
373374

‎com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs‎

Lines changed: 189 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,79 @@ public void ApplyScale(Vector3 scale)
467467
GhostObject.ApplyPostTransformMatrixScale(scale);
468468
}
469469
}
470+
471+
/// <summary>
472+
/// Returns true when N4E replicates this instance's parent. NGO parenting is disabled for the instance in that case.
473+
/// </summary>
474+
internal bool GhostReplicatesParent()
475+
{
476+
return HasGhost && GhostObject.ParentReplication;
477+
}
478+
479+
private bool WarnIfGhostReplicatesParent(string methodName)
480+
{
481+
if (!GhostReplicatesParent())
482+
{
483+
return false;
484+
}
485+
var networkManager = NetworkManager;
486+
if (networkManager == null || networkManager.LogLevel <= LogLevel.Normal)
487+
{
488+
NetworkLog.LogWarning($"[{name}] {methodName} was ignored. {nameof(GhostObject)}.{nameof(GhostObject.ParentReplication)} is enabled, so " +
489+
$"Netcode for Entities replicates the parent of this hybrid instance. Use {nameof(Transform)}.{nameof(Transform.SetParent)} instead.");
490+
}
491+
return true;
492+
}
493+
494+
/// <summary>
495+
/// NGO parenting of a hybrid instance only supports worldPositionStays disabled.
496+
/// </summary>
497+
/// <remarks>
498+
/// The ghost replicates its local transform. With worldPositionStays enabled, the authority rebases that local transform when it reparents.
499+
/// Clients apply the ParentSyncMessage before the snapshot with the rebased values arrives, so the object pops.
500+
/// </remarks>
501+
/// <returns>true when the parent change is rejected</returns>
502+
private bool RejectHybridWorldPositionStays(bool worldPositionStays)
503+
{
504+
if (!HasGhost || !worldPositionStays || GhostObject.ParentReplication)
505+
{
506+
return false;
507+
}
508+
var networkManager = NetworkManager;
509+
if (networkManager == null || networkManager.LogLevel <= LogLevel.Error)
510+
{
511+
NetworkLog.LogError($"[{name}] Hybrid instances can only be parented with worldPositionStays disabled. Use {nameof(TrySetParent)} " +
512+
$"or {nameof(TryRemoveParent)} with worldPositionStays set to false, or enable {nameof(GhostObject)}.{nameof(GhostObject.ParentReplication)}.");
513+
}
514+
return true;
515+
}
516+
517+
// Same tolerances as GhostObject. They are above the error a SetParent round trip leaves behind.
518+
private const float k_LocalSpaceEpsilon = 1e-4f;
519+
private const float k_LocalSpaceAngleEpsilon = 0.01f;
520+
521+
/// <summary>
522+
/// Returns true when parenting occurred with world position stays set to false.
523+
/// </summary>
524+
private bool LocalTransformKeptByParentChange()
525+
{
526+
var currentTransform = transform;
527+
return Vector3.Distance(currentTransform.localPosition, m_LocalPositionBeforeParentChange) <= k_LocalSpaceEpsilon
528+
&& Quaternion.Angle(currentTransform.localRotation, m_LocalRotationBeforeParentChange) <= k_LocalSpaceAngleEpsilon
529+
&& Vector3.Distance(currentTransform.localScale, m_LocalScaleBeforeParentChange) <= k_LocalSpaceEpsilon;
530+
}
531+
532+
/// <summary>
533+
/// Logs a warning when a hybrid instance is parented under a non-hybrid one or vice versa.
534+
/// </summary>
535+
private void LogHybridParentingArtifacts(NetworkObject parentObject)
536+
{
537+
if (parentObject != null && parentObject.HasGhost != HasGhost)
538+
{
539+
NetworkLog.LogWarning($"[{name}] Parented under {parentObject.name}, but only one of them is a hybrid instance. Their transforms are " +
540+
$"synchronized and interpolated on different timelines, so the child can jitter relative to its parent while either one moves.");
541+
}
542+
}
470543
#endif
471544
/// <summary>
472545
/// This is intentionally private since this is a sealed class.
@@ -2512,6 +2585,49 @@ internal void InvokeBehaviourOnNetworkObjectParentChanged(NetworkObject parentNe
25122585
private Transform m_CachedParent; // What is our last set parent Transform reference?
25132586
private bool m_CachedWorldPositionStays = true; // Used to preserve the world position stays parameter passed in TrySetParent
25142587

2588+
// The local transform's state before a parent change. These values are used if the parent change is reverted.
2589+
private Vector3 m_LocalPositionBeforeParentChange;
2590+
private Quaternion m_LocalRotationBeforeParentChange;
2591+
private Vector3 m_LocalScaleBeforeParentChange;
2592+
private bool m_RevertingParentChange;
2593+
2594+
private void OnBeforeTransformParentChanged()
2595+
{
2596+
// Skips the parent change RevertParentChange makes, so the values it restores are not overwritten.
2597+
if (m_RevertingParentChange)
2598+
{
2599+
return;
2600+
}
2601+
var currentTransform = transform;
2602+
m_LocalPositionBeforeParentChange = currentTransform.localPosition;
2603+
m_LocalRotationBeforeParentChange = currentTransform.localRotation;
2604+
m_LocalScaleBeforeParentChange = currentTransform.localScale;
2605+
}
2606+
2607+
/// <summary>
2608+
/// Restores the cached parent and the local transform prior to the parent change.
2609+
/// </summary>
2610+
/// <remarks>
2611+
/// Setting <see cref="Transform.parent"/> keeps the world position. That only restores the original pose when the change also kept the world position.
2612+
/// Restoring the local transform under the cached parent works for either worldPositionStays value.
2613+
/// </remarks>
2614+
private void RevertParentChange()
2615+
{
2616+
m_RevertingParentChange = true;
2617+
try
2618+
{
2619+
var currentTransform = transform;
2620+
currentTransform.SetParent(m_CachedParent, false);
2621+
currentTransform.localPosition = m_LocalPositionBeforeParentChange;
2622+
currentTransform.localRotation = m_LocalRotationBeforeParentChange;
2623+
currentTransform.localScale = m_LocalScaleBeforeParentChange;
2624+
}
2625+
finally
2626+
{
2627+
m_RevertingParentChange = false;
2628+
}
2629+
}
2630+
25152631
/// <summary>
25162632
/// With distributed authority, we need to have a way to determine if the parenting action is authorized.
25172633
/// This is set when handling an incoming ParentSyncMessage and when running as a DAHost and a client has disconnected.
@@ -2567,6 +2683,12 @@ internal void ClearNetworkParenting()
25672683
/// <returns>Whether or not re-parenting was successful.</returns>
25682684
public bool TrySetParent(Transform parent, bool worldPositionStays = true)
25692685
{
2686+
#if UNIFIED_NETCODE
2687+
if (WarnIfGhostReplicatesParent(nameof(TrySetParent)))
2688+
{
2689+
return false;
2690+
}
2691+
#endif
25702692
// If we are removing ourself from a parent
25712693
if (parent == null)
25722694
{
@@ -2587,6 +2709,12 @@ public bool TrySetParent(Transform parent, bool worldPositionStays = true)
25872709
/// <returns>Whether or not re-parenting was successful.</returns>
25882710
public bool TrySetParent(GameObject parent, bool worldPositionStays = true)
25892711
{
2712+
#if UNIFIED_NETCODE
2713+
if (WarnIfGhostReplicatesParent(nameof(TrySetParent)))
2714+
{
2715+
return false;
2716+
}
2717+
#endif
25902718
// If we are removing ourself from a parent
25912719
if (parent == null)
25922720
{
@@ -2617,6 +2745,12 @@ internal bool TryRemoveParentCachedWorldPositionStays()
26172745
/// <returns>True if the parent was successfully removed, false if the operation failed or the object was already parentless</returns>
26182746
public bool TryRemoveParent(bool worldPositionStays = true)
26192747
{
2748+
#if UNIFIED_NETCODE
2749+
if (WarnIfGhostReplicatesParent(nameof(TryRemoveParent)))
2750+
{
2751+
return false;
2752+
}
2753+
#endif
26202754
return TrySetParent((NetworkObject)null, worldPositionStays);
26212755
}
26222756

@@ -2628,6 +2762,12 @@ public bool TryRemoveParent(bool worldPositionStays = true)
26282762
/// <returns>Whether or not reparenting was successful.</returns>
26292763
public bool TrySetParent(NetworkObject parent, bool worldPositionStays = true)
26302764
{
2765+
#if UNIFIED_NETCODE
2766+
if (WarnIfGhostReplicatesParent(nameof(TrySetParent)) || RejectHybridWorldPositionStays(worldPositionStays))
2767+
{
2768+
return false;
2769+
}
2770+
#endif
26312771
if (!AutoObjectParentSync || !IsSpawned || !NetworkManagerOwner.IsListening)
26322772
{
26332773
return false;
@@ -2669,19 +2809,27 @@ internal bool InternalTrySetParent(NetworkObject parent, bool worldPositionStays
26692809
private void OnTransformParentChanged()
26702810
{
26712811
var networkManager = NetworkManager;
2672-
if (!AutoObjectParentSync || (networkManager != null && networkManager.ShutdownInProgress))
2812+
if (m_RevertingParentChange || !AutoObjectParentSync || (networkManager != null && networkManager.ShutdownInProgress))
26732813
{
26742814
return;
26752815
}
26762816

2817+
#if UNIFIED_NETCODE
2818+
// No warning: this also runs for the parent changes N4E applies.
2819+
if (GhostReplicatesParent())
2820+
{
2821+
return;
2822+
}
2823+
#endif
2824+
26772825
if (transform.parent == m_CachedParent)
26782826
{
26792827
return;
26802828
}
26812829

26822830
if (networkManager == null || !networkManager.IsListening)
26832831
{
2684-
transform.parent = m_CachedParent;
2832+
RevertParentChange();
26852833
// We want to log at any LogLevel, since we may not have a network manager may here.
26862834
NetworkLog.LogError($"[{name}] {nameof(networkManager)} is not listening, start a server or host before re-parenting.");
26872835
return;
@@ -2699,7 +2847,7 @@ private void OnTransformParentChanged()
26992847
}
27002848
else
27012849
{
2702-
transform.parent = m_CachedParent;
2850+
RevertParentChange();
27032851
if (networkManager.LogLevel <= LogLevel.Error)
27042852
{
27052853
NetworkLog.LogErrorServer($"[{name}] {nameof(NetworkObject)} can only be re-parented after being spawned!");
@@ -2714,7 +2862,7 @@ private void OnTransformParentChanged()
27142862
// If we are spawned and don't have authority; reset the parent back to the cached parent and exit
27152863
if (!isParentingAuthority)
27162864
{
2717-
transform.parent = m_CachedParent;
2865+
RevertParentChange();
27182866
if (networkManager.LogLevel <= LogLevel.Error)
27192867
{
27202868
if (networkManager.DistributedAuthorityMode)
@@ -2729,13 +2877,29 @@ private void OnTransformParentChanged()
27292877
return;
27302878
}
27312879

2880+
#if UNIFIED_NETCODE
2881+
// A direct Transform parent change has no worldPositionStays value. It is derived from whether the local transform was kept.
2882+
if (HasGhost && !AuthorityAppliedParenting && !GhostObject.ParentReplication)
2883+
{
2884+
if (LocalTransformKeptByParentChange())
2885+
{
2886+
m_CachedWorldPositionStays = false;
2887+
}
2888+
else if (RejectHybridWorldPositionStays(true))
2889+
{
2890+
RevertParentChange();
2891+
return;
2892+
}
2893+
}
2894+
#endif
2895+
27322896
var removeParent = false;
27332897
var parentTransform = transform.parent;
27342898
if (parentTransform != null)
27352899
{
27362900
if (!transform.parent.TryGetComponent(out NetworkObject parentObject))
27372901
{
2738-
transform.parent = m_CachedParent;
2902+
RevertParentChange();
27392903
AuthorityAppliedParenting = false;
27402904
if (networkManager.LogLevel <= LogLevel.Error)
27412905
{
@@ -2745,7 +2909,7 @@ private void OnTransformParentChanged()
27452909
}
27462910
if (!parentObject.IsSpawned)
27472911
{
2748-
transform.parent = m_CachedParent;
2912+
RevertParentChange();
27492913
AuthorityAppliedParenting = false;
27502914
if (networkManager.LogLevel <= LogLevel.Error)
27512915
{
@@ -2755,6 +2919,12 @@ private void OnTransformParentChanged()
27552919
}
27562920

27572921
m_LatestParent = parentObject.NetworkObjectId;
2922+
#if UNIFIED_NETCODE
2923+
if (networkManager.LogLevel <= LogLevel.Developer)
2924+
{
2925+
LogHybridParentingArtifacts(parentObject);
2926+
}
2927+
#endif
27582928
}
27592929
else
27602930
{
@@ -2835,6 +3005,14 @@ internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpa
28353005
return false;
28363006
}
28373007

3008+
#if UNIFIED_NETCODE
3009+
// NGO does not track this parent. Applying "no parent" here would undo the parent N4E replicated.
3010+
if (GhostReplicatesParent())
3011+
{
3012+
return false;
3013+
}
3014+
#endif
3015+
28383016
// SPECIAL CASE:
28393017
// The ignoreNotSpawned is a special case scenario where a late joining client has joined
28403018
// and loaded one or more scenes that contain nested in-scene placed NetworkObject children
@@ -3626,7 +3804,12 @@ internal SerializedObject SerializeSpawnedObject(ulong targetClientId = NetworkM
36263804
{
36273805
var obj = new SerializedObject
36283806
{
3807+
#if UNIFIED_NETCODE
3808+
// The ghost's spawn carries its own parent.
3809+
HasParent = transform.parent != null && !GhostReplicatesParent(),
3810+
#else
36293811
HasParent = transform.parent != null,
3812+
#endif
36303813
WorldPositionStays = m_CachedWorldPositionStays,
36313814
NetworkObjectId = NetworkObjectId,
36323815
OwnerClientId = OwnerClientId,

0 commit comments

Comments
 (0)