Skip to content

Commit 853cb6d

Browse files
fix: [Backport] rejected parent change restores the original pose (#4203)
* fix: a rejected parent change restores the original pose NGO reverted a rejected parent change by assigning transform.parent, which keeps the world position. A change made with worldPositionStays disabled had already moved the object, so the parent was restored but the object kept its moved position. The local transform is now captured in OnBeforeTransformParentChanged and restored along with the parent. Port of the parent revert fix from #4202. * chore: CHANGELOG entry for the parent revert fix
1 parent 34c377d commit 853cb6d

4 files changed

Lines changed: 150 additions & 6 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Additional documentation and release notes are available at [Multiplayer Documen
2727
- Issue where a `FastBufferReader` created with an offset and the default length of -1 read past the end of its source. The `ArraySegment<byte>` overload that takes an offset now applies it from the start of the segment. (#4221)
2828
- Issue where a client synchronized while a `NetworkList<T>` still had unsent changes applied those changes twice, resulting in duplicate entries and index-based events landing on the wrong elements on that client. (#4215)
2929
- Issue where showing a `NetworkObject` with `NetworkShow` while its `NetworkVariable` updates were held back by `NetworkVariableUpdateTraits` stopped those pending changes from being sent until the variable was modified again. (#4215)
30+
- 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. (#4203)
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`. (#4185)
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. (#4185)
3233
- Issue where mixed authority nested `NetworkTransform` instances could stop child/nested instances from updating because an authoritative `NetworkTransform` (root or otherwise) would remove the `NetworkObject` from the update group, preventing non-authoritative instances from updating their state on the authority side. (#4169)

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

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2396,6 +2396,49 @@ internal void InvokeBehaviourOnNetworkObjectParentChanged(NetworkObject parentNe
23962396
private Transform m_CachedParent; // What is our last set parent Transform reference?
23972397
private bool m_CachedWorldPositionStays = true; // Used to preserve the world position stays parameter passed in TrySetParent
23982398

2399+
// The local transform's state before a parent change. These values are used if the parent change is reverted.
2400+
private Vector3 m_LocalPositionBeforeParentChange;
2401+
private Quaternion m_LocalRotationBeforeParentChange;
2402+
private Vector3 m_LocalScaleBeforeParentChange;
2403+
private bool m_RevertingParentChange;
2404+
2405+
private void OnBeforeTransformParentChanged()
2406+
{
2407+
// Skips the parent change RevertParentChange makes, so the values it restores are not overwritten.
2408+
if (m_RevertingParentChange)
2409+
{
2410+
return;
2411+
}
2412+
var currentTransform = transform;
2413+
m_LocalPositionBeforeParentChange = currentTransform.localPosition;
2414+
m_LocalRotationBeforeParentChange = currentTransform.localRotation;
2415+
m_LocalScaleBeforeParentChange = currentTransform.localScale;
2416+
}
2417+
2418+
/// <summary>
2419+
/// Restores the cached parent and the local transform prior to the parent change.
2420+
/// </summary>
2421+
/// <remarks>
2422+
/// Setting <see cref="Transform.parent"/> keeps the world position. That only restores the original pose when the change also kept the world position.
2423+
/// Restoring the local transform under the cached parent works for either worldPositionStays value.
2424+
/// </remarks>
2425+
private void RevertParentChange()
2426+
{
2427+
m_RevertingParentChange = true;
2428+
try
2429+
{
2430+
var currentTransform = transform;
2431+
currentTransform.SetParent(m_CachedParent, false);
2432+
currentTransform.localPosition = m_LocalPositionBeforeParentChange;
2433+
currentTransform.localRotation = m_LocalRotationBeforeParentChange;
2434+
currentTransform.localScale = m_LocalScaleBeforeParentChange;
2435+
}
2436+
finally
2437+
{
2438+
m_RevertingParentChange = false;
2439+
}
2440+
}
2441+
23992442
/// <summary>
24002443
/// With distributed authority, we need to have a way to determine if the parenting action is authorized.
24012444
/// This is set when handling an incoming ParentSyncMessage and when running as a DAHost and a client has disconnected.
@@ -2553,7 +2596,7 @@ internal bool InternalTrySetParent(NetworkObject parent, bool worldPositionStays
25532596
private void OnTransformParentChanged()
25542597
{
25552598
var networkManager = NetworkManager;
2556-
if (!AutoObjectParentSync || (networkManager != null && networkManager.ShutdownInProgress))
2599+
if (m_RevertingParentChange || !AutoObjectParentSync || (networkManager != null && networkManager.ShutdownInProgress))
25572600
{
25582601
return;
25592602
}
@@ -2565,7 +2608,7 @@ private void OnTransformParentChanged()
25652608

25662609
if (networkManager == null || !networkManager.IsListening)
25672610
{
2568-
transform.parent = m_CachedParent;
2611+
RevertParentChange();
25692612
// We want to log at any LogLevel, since we may not have a network manager may here.
25702613
NetworkLog.LogError($"[{name}] {nameof(networkManager)} is not listening, start a server or host before re-parenting.");
25712614
return;
@@ -2583,7 +2626,7 @@ private void OnTransformParentChanged()
25832626
}
25842627
else
25852628
{
2586-
transform.parent = m_CachedParent;
2629+
RevertParentChange();
25872630
if (networkManager.LogLevel <= LogLevel.Error)
25882631
{
25892632
NetworkLog.LogErrorServer($"[{name}] {nameof(NetworkObject)} can only be re-parented after being spawned!");
@@ -2598,7 +2641,7 @@ private void OnTransformParentChanged()
25982641
// If we are spawned and don't have authority; reset the parent back to the cached parent and exit
25992642
if (!isParentingAuthority)
26002643
{
2601-
transform.parent = m_CachedParent;
2644+
RevertParentChange();
26022645
if (networkManager.LogLevel <= LogLevel.Error)
26032646
{
26042647
if (networkManager.DistributedAuthorityMode)
@@ -2619,7 +2662,7 @@ private void OnTransformParentChanged()
26192662
{
26202663
if (!transform.parent.TryGetComponent(out NetworkObject parentObject))
26212664
{
2622-
transform.parent = m_CachedParent;
2665+
RevertParentChange();
26232666
AuthorityAppliedParenting = false;
26242667
if (networkManager.LogLevel <= LogLevel.Error)
26252668
{
@@ -2629,7 +2672,7 @@ private void OnTransformParentChanged()
26292672
}
26302673
if (!parentObject.IsSpawned)
26312674
{
2632-
transform.parent = m_CachedParent;
2675+
RevertParentChange();
26332676
AuthorityAppliedParenting = false;
26342677
if (networkManager.LogLevel <= LogLevel.Error)
26352678
{
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using System.Collections;
2+
using System.Text.RegularExpressions;
3+
using NUnit.Framework;
4+
using Unity.Netcode.TestHelpers.Runtime;
5+
using UnityEngine;
6+
using UnityEngine.TestTools;
7+
8+
namespace Unity.Netcode.RuntimeTests
9+
{
10+
/// <summary>
11+
/// Validates that a rejected parent change restores the NetworkObject's parent and its original pose.
12+
/// </summary>
13+
[TestFixture(HostOrServer.Host)]
14+
[TestFixture(HostOrServer.Server)]
15+
[TestFixture(HostOrServer.DAHost)]
16+
internal class NetworkObjectParentRevertTests : NetcodeIntegrationTest
17+
{
18+
// The parent is placed away from the child, so a parent change with worldPositionStays disabled moves the child.
19+
private static readonly Vector3 k_ParentPosition = new Vector3(5f, 1.25f, 0f);
20+
private static readonly Vector3 k_ChildPosition = new Vector3(0f, 1.25f, 3f);
21+
22+
protected override int NumberOfClients => 1;
23+
24+
private GameObject m_Prefab;
25+
26+
public NetworkObjectParentRevertTests(HostOrServer hostOrServer) : base(hostOrServer) { }
27+
28+
protected override void OnServerAndClientsCreated()
29+
{
30+
m_Prefab = CreateNetworkObjectPrefab("ParentRevertObject");
31+
base.OnServerAndClientsCreated();
32+
}
33+
34+
private void AssertPoseUnchanged(Transform child, Vector3 worldPosition, Vector3 localPosition)
35+
{
36+
Assert.IsNull(child.parent, $"{child.name}'s parent change should have been reverted!");
37+
Assert.AreEqual(worldPosition, child.position, $"{child.name}'s world position changed!");
38+
Assert.AreEqual(localPosition, child.localPosition, $"{child.name}'s local position changed!");
39+
}
40+
41+
/// <summary>
42+
/// Validates that a parent change made by a peer without parenting authority is reverted to the original pose.
43+
/// </summary>
44+
[UnityTest]
45+
public IEnumerator NonAuthorityParentChangeRestoresPose([Values] bool worldPositionStays)
46+
{
47+
var authority = GetAuthorityNetworkManager();
48+
var parent = SpawnObject(m_Prefab, authority).GetComponent<NetworkObject>();
49+
var child = SpawnObject(m_Prefab, authority).GetComponent<NetworkObject>();
50+
yield return WaitForSpawnedOnAllOrTimeOut(parent);
51+
AssertOnTimeout($"Timed out waiting for {parent.name} to spawn on all instances!");
52+
yield return WaitForSpawnedOnAllOrTimeOut(child);
53+
AssertOnTimeout($"Timed out waiting for {child.name} to spawn on all instances!");
54+
55+
var nonAuthority = GetNonAuthorityNetworkManager();
56+
var localParent = nonAuthority.SpawnManager.SpawnedObjects[parent.NetworkObjectId].transform;
57+
var localChild = nonAuthority.SpawnManager.SpawnedObjects[child.NetworkObjectId].transform;
58+
localParent.position = k_ParentPosition;
59+
localChild.position = k_ChildPosition;
60+
var worldPosition = localChild.position;
61+
var localPosition = localChild.localPosition;
62+
63+
LogAssert.Expect(LogType.Error, new Regex("re-parent"));
64+
localChild.SetParent(localParent, worldPositionStays);
65+
AssertPoseUnchanged(localChild, worldPosition, localPosition);
66+
}
67+
68+
/// <summary>
69+
/// Validates that a parent change under a GameObject without a <see cref="NetworkObject"/> is reverted to the original pose.
70+
/// </summary>
71+
[UnityTest]
72+
public IEnumerator NonNetworkObjectParentRestoresPose([Values] bool worldPositionStays)
73+
{
74+
var authority = GetAuthorityNetworkManager();
75+
var child = SpawnObject(m_Prefab, authority).GetComponent<NetworkObject>();
76+
yield return WaitForSpawnedOnAllOrTimeOut(child);
77+
AssertOnTimeout($"Timed out waiting for {child.name} to spawn on all instances!");
78+
79+
var plainParent = new GameObject("PlainParent");
80+
try
81+
{
82+
plainParent.transform.position = k_ParentPosition;
83+
var childTransform = child.transform;
84+
childTransform.position = k_ChildPosition;
85+
var worldPosition = childTransform.position;
86+
var localPosition = childTransform.localPosition;
87+
88+
LogAssert.Expect(LogType.Error, new Regex("Invalid parenting"));
89+
childTransform.SetParent(plainParent.transform, worldPositionStays);
90+
AssertPoseUnchanged(childTransform, worldPosition, localPosition);
91+
}
92+
finally
93+
{
94+
Object.Destroy(plainParent);
95+
}
96+
}
97+
}
98+
}

‎com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectParentRevertTests.cs.meta‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)