Skip to content

Commit b3e3545

Browse files
fix: [Up-port] mixed authority nested NetworkTransform stops updating non-authority instances
Up-port of #4169. The NetworkManager update registration is per-NetworkObject while the authority motion model is per-NetworkTransform, so the first authority instance to initialize removed the entire NetworkObject from the update group and its non-authority siblings stopped being updated. Gate the removal on whether any NetworkTransform on the NetworkObject is still non-authority for that same update.
1 parent 297c8b0 commit b3e3545

5 files changed

Lines changed: 171 additions & 28 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Additional documentation and release notes are available at [Multiplayer Documen
1818

1919
### Fixed
2020

21+
- Issue with mixed authority nested `NetworkTransform` instances can stop child/nested `NetworkTransform` instances from updating due to a parent (root or otherwise) `NetworkTransform` that is the authority instance will remove the `NetworkObject` completely from the non-authority update group causing non-authority instances to never update their state (interpolating or not) on the authority side. (#4169)
22+
2123
### Security
2224

2325
### Obsolete

‎com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3736,6 +3736,38 @@ private void ResetInterpolatedStateToCurrentAuthoritativeState()
37363736
m_ScaleInterpolator.ResetTo(transform.parent, transform.localScale, serverTime);
37373737
}
37383738

3739+
/// <summary>
3740+
/// Determines if this <see cref="NetworkObject"/> has any <see cref="NetworkTransform"/> instances that are non-authority and are updated during the same update stage.
3741+
/// </summary>
3742+
/// <remarks>
3743+
/// See <see cref="InternalInitialization"/> to better understand how the <paramref name="forUpdate"/> parameter is used to determine which update stage to check for non-authority <see cref="NetworkTransform"/> instances.
3744+
/// </remarks>
3745+
/// <param name="forUpdate">true to check the instances updated during the standard update and false to check the instances updated during the fixed update.</param>
3746+
/// <returns>true if a non-authority NetworkTransform exists on this NetworkObject and false if there are none.</returns>
3747+
private bool HasNonAuthorityNetworkTransform(bool forUpdate)
3748+
{
3749+
var networkTransforms = NetworkObject.NetworkTransforms;
3750+
for (int i = 0; i < networkTransforms.Count; i++)
3751+
{
3752+
var networkTransform = networkTransforms[i];
3753+
#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D
3754+
// If the update stages don't match, then skip this instance.
3755+
// Reference:
3756+
// forUpdate is true for the standard update and false for the fixed update.
3757+
// m_UseRigidbodyForMotion is false for the standard update and true for the fixed update.
3758+
if (forUpdate == networkTransform.m_UseRigidbodyForMotion)
3759+
{
3760+
continue;
3761+
}
3762+
#endif
3763+
if (!(networkTransform.IsServerAuthoritative() ? networkTransform.IsServer : networkTransform.IsOwner))
3764+
{
3765+
return true;
3766+
}
3767+
}
3768+
return false;
3769+
}
3770+
37393771
/// <summary>
37403772
/// The internal initialization method to allow for internal API adjustments
37413773
/// </summary>
@@ -3807,8 +3839,12 @@ internal virtual void InternalInitialization(bool isOwnershipChange = false)
38073839

38083840
if (CanCommitToTransform)
38093841
{
3810-
// Make sure authority doesn't get added to updates (no need to do this on the authority side)
3811-
m_CachedNetworkManager.NetworkTransformRegistration(NetworkObject, forUpdate, false);
3842+
// If there are no non-authority NetworkTransform instances on this NetworkObject using this update, then remove this instance from the NetworkManager's update list.
3843+
// Otherwise, we need to keep it registered for updates so the non-authority instances will process their received state updates and apply them to the transform.
3844+
if (!HasNonAuthorityNetworkTransform(forUpdate))
3845+
{
3846+
m_CachedNetworkManager.NetworkTransformRegistration(NetworkObject, forUpdate, false);
3847+
}
38123848
if (UseHalfFloatPrecision)
38133849
{
38143850
m_HalfPositionState = new NetworkDeltaPosition(currentPosition, m_CachedNetworkManager.ServerTime.Tick, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));

‎com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformMixedAuthorityTests.cs‎

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,93 @@
11
using System.Collections;
22
using System.Text;
3+
using NUnit.Framework;
34
using Unity.Netcode.Components;
45
using Unity.Netcode.TestHelpers.Runtime;
56
using UnityEngine;
67
using UnityEngine.TestTools;
78

89
namespace Unity.Netcode.RuntimeTests
910
{
11+
[TestFixture(HostOrServer.Host, NetworkTransform.AuthorityModes.Server)]
12+
[TestFixture(HostOrServer.Host, NetworkTransform.AuthorityModes.Owner)]
13+
[TestFixture(HostOrServer.Server, NetworkTransform.AuthorityModes.Server)]
14+
[TestFixture(HostOrServer.Server, NetworkTransform.AuthorityModes.Owner)]
1015
internal class NetworkTransformMixedAuthorityTests : IntegrationTestWithApproximation
1116
{
1217
private const float k_MotionMagnitude = 5.5f;
1318
private const int k_Iterations = 4;
1419

1520
protected override int NumberOfClients => 2;
1621

22+
private readonly NetworkTransform.AuthorityModes m_RootAuthorityMode;
23+
private readonly NetworkTransform.AuthorityModes m_ChildAuthorityMode;
24+
1725
private StringBuilder m_ErrorMsg = new StringBuilder();
1826

27+
public NetworkTransformMixedAuthorityTests(HostOrServer hostOrServer, NetworkTransform.AuthorityModes rootAuthorityMode) : base(hostOrServer)
28+
{
29+
m_RootAuthorityMode = rootAuthorityMode;
30+
m_ChildAuthorityMode = rootAuthorityMode == NetworkTransform.AuthorityModes.Server ? NetworkTransform.AuthorityModes.Owner : NetworkTransform.AuthorityModes.Server;
31+
}
32+
1933
protected override void OnCreatePlayerPrefab()
2034
{
21-
m_PlayerPrefab.AddComponent<NetworkTransform>();
35+
m_PlayerPrefab.AddComponent<NetworkTransform>().AuthorityMode = m_RootAuthorityMode;
2236

2337
var childGameObject = new GameObject();
2438
childGameObject.transform.parent = m_PlayerPrefab.transform;
2539
var childNetworkTransform = childGameObject.AddComponent<NetworkTransform>();
26-
childNetworkTransform.AuthorityMode = NetworkTransform.AuthorityModes.Owner;
40+
childNetworkTransform.AuthorityMode = m_ChildAuthorityMode;
2741
childNetworkTransform.InLocalSpace = true;
2842

2943
base.OnCreatePlayerPrefab();
3044
}
3145

46+
/// <summary>
47+
/// Returns the instance of <paramref name="player"/>'s player object that has authority over a
48+
/// <see cref="NetworkTransform"/> using the <paramref name="authorityMode"/> authority mode.
49+
/// </summary>
50+
private NetworkObject GetAuthorityInstance(NetworkManager player, NetworkTransform.AuthorityModes authorityMode)
51+
{
52+
var authority = authorityMode == NetworkTransform.AuthorityModes.Server ? m_ServerNetworkManager : player;
53+
return authority.SpawnManager.SpawnedObjects[player.LocalClient.PlayerObject.NetworkObjectId];
54+
}
55+
3256
private void MovePlayers()
3357
{
34-
foreach (var networkManager in m_NetworkManagers)
58+
foreach (var networkManager in m_ClientNetworkManagers)
3559
{
3660
var direction = GetRandomVector3(-1.0f, 1.0f);
37-
var playerObject = networkManager.LocalClient.PlayerObject;
38-
var playerObjectId = networkManager.LocalClient.PlayerObject.NetworkObjectId;
39-
// Server authoritative
40-
var serverPlayerClone = m_ServerNetworkManager.SpawnManager.SpawnedObjects[playerObjectId];
41-
serverPlayerClone.transform.position += direction * k_MotionMagnitude;
42-
// Owner authoritative
43-
var childTransform = networkManager.LocalClient.PlayerObject.transform.GetChild(0);
44-
childTransform.localPosition += direction * k_MotionMagnitude;
61+
GetAuthorityInstance(networkManager, m_RootAuthorityMode).transform.position += direction * k_MotionMagnitude;
62+
GetAuthorityInstance(networkManager, m_ChildAuthorityMode).transform.GetChild(0).localPosition += direction * k_MotionMagnitude;
4563
}
4664
}
4765

4866
private bool AllInstancePositionsMatch()
4967
{
5068
m_ErrorMsg.Clear();
51-
foreach (var networkManager in m_NetworkManagers)
69+
foreach (var networkManager in m_ClientNetworkManagers)
5270
{
53-
var playerObject = networkManager.LocalClient.PlayerObject;
5471
var playerObjectId = networkManager.LocalClient.PlayerObject.NetworkObjectId;
55-
var serverRootPosition = m_ServerNetworkManager.SpawnManager.SpawnedObjects[playerObjectId].transform.position;
56-
var ownerChildPosition = networkManager.LocalClient.PlayerObject.transform.GetChild(0).localPosition;
72+
var authorityRootPosition = GetAuthorityInstance(networkManager, m_RootAuthorityMode).transform.position;
73+
var authorityChildPosition = GetAuthorityInstance(networkManager, m_ChildAuthorityMode).transform.GetChild(0).localPosition;
74+
75+
// The authority instances are compared too, as an instance with authority over one nested
76+
// NetworkTransform is still non-authority for the other.
5777
foreach (var client in m_NetworkManagers)
5878
{
59-
if (client == networkManager)
60-
{
61-
continue;
62-
}
6379
var playerClone = client.SpawnManager.SpawnedObjects[playerObjectId];
6480
var cloneRootPosition = playerClone.transform.position;
6581
var cloneChildPosition = playerClone.transform.GetChild(0).localPosition;
6682

67-
if (!Approximately(serverRootPosition, cloneRootPosition))
83+
if (!Approximately(authorityRootPosition, cloneRootPosition))
6884
{
69-
m_ErrorMsg.AppendLine($"[{playerObject.name}][{playerClone.name}] Root mismatch ({GetVector3Values(serverRootPosition)})({GetVector3Values(cloneRootPosition)})!");
85+
m_ErrorMsg.AppendLine($"[Client-{client.LocalClientId}][{playerClone.name}] Root mismatch ({GetVector3Values(authorityRootPosition)})({GetVector3Values(cloneRootPosition)})!");
7086
}
7187

72-
if (!Approximately(ownerChildPosition, cloneChildPosition))
88+
if (!Approximately(authorityChildPosition, cloneChildPosition))
7389
{
74-
m_ErrorMsg.AppendLine($"[{playerObject.name}][{playerClone.name}] Child mismatch ({GetVector3Values(ownerChildPosition)})({GetVector3Values(cloneChildPosition)})!");
90+
m_ErrorMsg.AppendLine($"[Client-{client.LocalClientId}][{playerClone.name}] Child mismatch ({GetVector3Values(authorityChildPosition)})({GetVector3Values(cloneChildPosition)})!");
7591
}
7692
}
7793
}
@@ -81,8 +97,8 @@ private bool AllInstancePositionsMatch()
8197
/// <summary>
8298
/// Client-Server Only
8399
/// Validates that mixed authority is working properly
84-
/// Root -- Server Authoritative
85-
/// |--Child -- Owner Authoritative
100+
/// Root -- Server or Owner authoritative
101+
/// |--Child -- The inverse of the root's authority mode
86102
/// </summary>
87103
[UnityTest]
88104
public IEnumerator MixedAuthorityTest()
@@ -91,7 +107,31 @@ public IEnumerator MixedAuthorityTest()
91107
{
92108
MovePlayers();
93109
yield return WaitForConditionOrTimeOut(AllInstancePositionsMatch);
94-
AssertOnTimeout($"Transforms failed to synchronize!");
110+
AssertOnTimeout($"Transforms failed to synchronize!\n{m_ErrorMsg}");
111+
}
112+
}
113+
114+
/// <summary>
115+
/// The update registration is per-NetworkObject while the authority motion model is per-NetworkTransform,
116+
/// so an instance stays registered for as long as any one of its nested NetworkTransform components is
117+
/// non-authority.
118+
/// </summary>
119+
[Test]
120+
public void MixedAuthorityUpdateRegistration()
121+
{
122+
foreach (var networkManager in m_ClientNetworkManagers)
123+
{
124+
var playerObjectId = networkManager.LocalClient.PlayerObject.NetworkObjectId;
125+
foreach (var client in m_NetworkManagers)
126+
{
127+
var playerClone = client.SpawnManager.SpawnedObjects[playerObjectId];
128+
var hasNonAuthority = false;
129+
foreach (var networkTransform in playerClone.NetworkTransforms)
130+
{
131+
hasNonAuthority |= !networkTransform.CanCommitToTransform;
132+
}
133+
Assert.AreEqual(hasNonAuthority, client.NetworkTransformUpdate.ContainsKey(playerObjectId), $"[Client-{client.LocalClientId}][{playerClone.name}] Unexpected update registration!");
134+
}
95135
}
96136
}
97137
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#if COM_UNITY_MODULES_PHYSICS
2+
using System.Collections;
3+
using NUnit.Framework;
4+
using Unity.Netcode.Components;
5+
using Unity.Netcode.TestHelpers.Runtime;
6+
using UnityEngine;
7+
using UnityEngine.TestTools;
8+
9+
namespace Unity.Netcode.RuntimeTests
10+
{
11+
internal class NetworkTransformMixedMotionModelTests : NetcodeIntegrationTest
12+
{
13+
protected override int NumberOfClients => 2;
14+
15+
private GameObject m_MixedMotionModelPrefab;
16+
17+
protected override void OnServerAndClientsCreated()
18+
{
19+
m_MixedMotionModelPrefab = CreateNetworkObjectPrefab("MixedMotionModel");
20+
21+
// The root is owner authoritative and driven by the rigidbody, which places it in the fixed update registration
22+
var rootNetworkTransform = m_MixedMotionModelPrefab.AddComponent<NetworkTransform>();
23+
rootNetworkTransform.AuthorityMode = NetworkTransform.AuthorityModes.Owner;
24+
var rigidbody = m_MixedMotionModelPrefab.AddComponent<Rigidbody>();
25+
rigidbody.useGravity = false;
26+
rigidbody.detectCollisions = false;
27+
m_MixedMotionModelPrefab.AddComponent<NetworkRigidbody>().UseRigidBodyForMotion = true;
28+
29+
// The nested child is server authoritative and driven by the transform, which places it in the update registration
30+
var childGameObject = new GameObject();
31+
childGameObject.transform.parent = m_MixedMotionModelPrefab.transform;
32+
var childNetworkTransform = childGameObject.AddComponent<NetworkTransform>();
33+
childNetworkTransform.AuthorityMode = NetworkTransform.AuthorityModes.Server;
34+
childNetworkTransform.InLocalSpace = true;
35+
36+
base.OnServerAndClientsCreated();
37+
}
38+
39+
/// <summary>
40+
/// A NetworkObject that mixes both the authority motion model and the rigidbody motion model has each nested
41+
/// NetworkTransform registered under a different update. Gaining authority over the instance in one update
42+
/// should not leave it registered for the other.
43+
/// </summary>
44+
[UnityTest]
45+
public IEnumerator UpdateRegistrationFollowsMotionModel()
46+
{
47+
var instance = SpawnObject(m_MixedMotionModelPrefab, m_ServerNetworkManager).GetComponent<NetworkObject>();
48+
yield return WaitForSpawnedOnAllOrTimeOut(instance);
49+
AssertOnTimeout($"Failed to spawn {instance.name} on all clients!");
50+
51+
var newOwner = m_ClientNetworkManagers[0];
52+
instance.ChangeOwnership(newOwner.LocalClientId);
53+
yield return WaitForConditionOrTimeOut(() => newOwner.SpawnManager.SpawnedObjects[instance.NetworkObjectId].OwnerClientId == newOwner.LocalClientId);
54+
AssertOnTimeout($"Client-{newOwner.LocalClientId} never gained ownership of {instance.name}!");
55+
56+
// The new owner is the authority for the rigidbody driven root, so nothing on this instance needs the fixed
57+
// update any longer. The server authoritative child still needs the standard update.
58+
Assert.False(newOwner.NetworkTransformFixedUpdate.ContainsKey(instance.NetworkObjectId), $"Client-{newOwner.LocalClientId} is still registered for the fixed update!");
59+
Assert.True(newOwner.NetworkTransformUpdate.ContainsKey(instance.NetworkObjectId), $"Client-{newOwner.LocalClientId} is not registered for the update!");
60+
}
61+
}
62+
}
63+
#endif

‎com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformMixedMotionModelTests.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)