Skip to content

Commit 422aa23

Browse files
fix: NetworkTransform synchronization fixes and owner authoritative performance improvement on server-host [MTT-6971] (#2636)
* fix This resolves an issue with half float precision not synchronizing properly when running in owner authoritative mode, the owner client sends an update to the host-server, the host-server then forwards the state update to the remaining clients at the end of the frame but during the initial processing of the state update the delta position value is collapsed into the full float position due to exceeding the maximum delta. The primary issue was the order of operations that occurred using NetworkVariables to update the transform state when in owner authority mode. This also reduces the amount of serialization and processing a host has to perform when receiving owner authoritative updates and there are 2 or more remote clients connected (i.e. it has to forward the result to the other clients). Now, it just forwards the message payload to the non-owner clients upon receiving the state update. This also fixes: - an on-going issue with scale where really the answer was to send both the lossy and local scale values when synchronizing and/or teleporting and letting the client-side determine which to use depending upon whether the NetworkObject was parented, was the parenting applied when applying the states, and whether the parenting used WorldPositionStays. - some minor issues with the parenting test not checking if the NetworkObject was parented before testing the final values. - updates named message handling slightly so it doesn't spam the log console during shutdown if there are incoming messages while shutting down. - resolves an issue where you have an owner authoritative NetworkTransform and a host-server with two remote clients connected and the following sequence occurs: - Client A connects to the host first and takes ownership of a NetworkObject but does not change the transform state. - Client B connects and attempts to move the NetworkObject that Client A owns - Client B was able to move the NetworkObject * test Adding NetworkTransformStateFlags test to validate the NetworkTransformState flags. Adding internal BitSet setter and getter for testing and a BitSet flag test. This test adds a "sub-child" (NetworkObject parented under a child) to the parenting test to add the additional check and make sure that all nested children preserve their values when changed for connected clients and those changes are applied and preserved for late joining clients. Adding a test to verify that when a late joining client is connected after ownership has been transferred from the host-server to an already connected client and the owner authoritative transform has not changed yet that the non-owner client cannot change the transform values.
1 parent fae403a commit 422aa23

File tree

10 files changed

+677
-186
lines changed

10 files changed

+677
-186
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ Additional documentation and release notes are available at [Multiplayer Documen
1212

1313
### Fixed
1414

15+
- Fixed issue with client synchronization of position when using half precision and the delta position reaches the maximum value and is collapsed on the host prior to being forwarded to the non-owner clients. (#2636)
16+
- Fixed issue with scale not synchronizing properly depending upon the spawn order of NetworkObjects. (#2636)
17+
- Fixed issue position was not properly transitioning between ownership changes with an owner authoritative NetworkTransform. (#2636)
18+
- Fixed issue where a late joining non-owner client could update an owner authoritative NetworkTransform if ownership changed without any updates to position prior to the non-owner client joining. (#2636)
19+
20+
### Changed
21+
22+
## [1.5.2] - 2023-07-24
23+
24+
### Added
25+
26+
### Fixed
27+
1528
- Fixed issue where `NetworkClient.OwnedObjects` was not returning any owned objects due to the `NetworkClient.IsConnected` not being properly set. (#2631)
1629
- Fixed a crash when calling TrySetParent with a null Transform (#2625)
1730
- Fixed issue where a `NetworkTransform` using full precision state updates was losing transform state updates when interpolation was enabled. (#2624)

com.unity.netcode.gameobjects/Components/HalfVector3.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public struct HalfVector3 : INetworkSerializable
2727
/// <summary>
2828
/// The half float precision value of the z-axis as a <see cref="half"/>.
2929
/// </summary>
30-
public half Z => Axis.x;
30+
public half Z => Axis.z;
3131

3232
/// <summary>
3333
/// Used to store the half float precision values as a <see cref="half3"/>
@@ -39,6 +39,17 @@ public struct HalfVector3 : INetworkSerializable
3939
/// </summary>
4040
public bool3 AxisToSynchronize;
4141

42+
/// <summary>
43+
/// Directly sets each axial value to the passed in full precision values
44+
/// that are converted to half precision
45+
/// </summary>
46+
internal void Set(float x, float y, float z)
47+
{
48+
Axis.x = math.half(x);
49+
Axis.y = math.half(y);
50+
Axis.z = math.half(z);
51+
}
52+
4253
private void SerializeWrite(FastBufferWriter writer)
4354
{
4455
for (int i = 0; i < Length; i++)

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

Lines changed: 305 additions & 119 deletions
Large diffs are not rendered by default.

com.unity.netcode.gameobjects/Runtime/Messaging/Messages/NamedMessage.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int
2424

2525
public void Handle(ref NetworkContext context)
2626
{
27-
((NetworkManager)context.SystemOwner).CustomMessagingManager.InvokeNamedMessage(Hash, context.SenderId, m_ReceiveData, context.SerializedHeaderSize);
27+
var networkManager = (NetworkManager)context.SystemOwner;
28+
if (!networkManager.ShutdownInProgress)
29+
{
30+
((NetworkManager)context.SystemOwner).CustomMessagingManager.InvokeNamedMessage(Hash, context.SenderId, m_ReceiveData, context.SerializedHeaderSize);
31+
}
2832
}
2933
}
3034
}

com.unity.netcode.gameobjects/Runtime/Messaging/NetworkMessageManager.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -518,15 +518,18 @@ internal int GetMessageVersion(Type type, ulong clientId, bool forReceive = fals
518518
{
519519
if (!m_PerClientMessageVersions.TryGetValue(clientId, out var versionMap))
520520
{
521-
if (forReceive)
521+
var networkManager = NetworkManager.Singleton;
522+
if (networkManager != null && networkManager.LogLevel == LogLevel.Developer)
522523
{
523-
Debug.LogWarning($"Trying to receive {type.Name} from client {clientId} which is not in a connected state.");
524-
}
525-
else
526-
{
527-
Debug.LogWarning($"Trying to send {type.Name} to client {clientId} which is not in a connected state.");
524+
if (forReceive)
525+
{
526+
NetworkLog.LogWarning($"Trying to receive {type.Name} from client {clientId} which is not in a connected state.");
527+
}
528+
else
529+
{
530+
NetworkLog.LogWarning($"Trying to send {type.Name} to client {clientId} which is not in a connected state.");
531+
}
528532
}
529-
530533
return -1;
531534
}
532535

com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformOwnershipTests.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,93 @@ protected override void OnServerAndClientsCreated()
4646
base.OnServerAndClientsCreated();
4747
}
4848

49+
/// <summary>
50+
/// Clients created during a test need to have their prefabs list updated to
51+
/// match the server's prefab list.
52+
/// </summary>
53+
protected override void OnNewClientCreated(NetworkManager networkManager)
54+
{
55+
foreach (var networkPrefab in m_ServerNetworkManager.NetworkConfig.Prefabs.Prefabs)
56+
{
57+
networkManager.NetworkConfig.Prefabs.Add(networkPrefab);
58+
}
59+
60+
base.OnNewClientCreated(networkManager);
61+
}
62+
63+
private bool ClientIsOwner()
64+
{
65+
var clientId = m_ClientNetworkManagers[0].LocalClientId;
66+
if (!VerifyObjectIsSpawnedOnClient.GetClientsThatSpawnedThisPrefab().Contains(clientId))
67+
{
68+
return false;
69+
}
70+
if (VerifyObjectIsSpawnedOnClient.GetClientInstance(clientId).OwnerClientId != clientId)
71+
{
72+
return false;
73+
}
74+
return true;
75+
}
76+
77+
/// <summary>
78+
/// This test verifies a late joining client cannot change the transform when:
79+
/// - A NetworkObject is spawned with a host and one or more connected clients
80+
/// - The NetworkTransform is owner authoritative and spawned with the host as the owner
81+
/// - The host does not change the transform values
82+
/// - One of the already connected clients gains ownership of the spawned NetworkObject
83+
/// - The new client owner does not change the transform values
84+
/// - A new late joining client connects and is synchronized
85+
/// - The newly connected late joining client tries to change the transform of the NetworkObject
86+
/// it does not own
87+
/// </summary>
88+
[UnityTest]
89+
public IEnumerator LateJoinedNonOwnerClientCannotChangeTransform()
90+
{
91+
// Spawn the m_ClientNetworkTransformPrefab with the host starting as the owner
92+
var hostInstance = SpawnObject(m_ClientNetworkTransformPrefab, m_ServerNetworkManager);
93+
94+
// Wait for the client to spawn it
95+
yield return WaitForConditionOrTimeOut(() => VerifyObjectIsSpawnedOnClient.GetClientsThatSpawnedThisPrefab().Contains(m_ClientNetworkManagers[0].LocalClientId));
96+
97+
// Change the ownership to the connectd client
98+
hostInstance.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);
99+
100+
// Wait until the client gains ownership
101+
yield return WaitForConditionOrTimeOut(ClientIsOwner);
102+
103+
// Spawn a new client
104+
yield return CreateAndStartNewClient();
105+
106+
// Get the instance of the object relative to the newly joined client
107+
var newClientObjectInstance = VerifyObjectIsSpawnedOnClient.GetClientInstance(m_ClientNetworkManagers[1].LocalClientId);
108+
109+
// Attempt to change the transform values
110+
var currentPosition = newClientObjectInstance.transform.position;
111+
newClientObjectInstance.transform.position = GetRandomVector3(0.5f, 10.0f);
112+
var rotation = newClientObjectInstance.transform.rotation;
113+
var currentRotation = rotation.eulerAngles;
114+
rotation.eulerAngles = GetRandomVector3(1.0f, 180.0f);
115+
var currentScale = newClientObjectInstance.transform.localScale;
116+
newClientObjectInstance.transform.localScale = GetRandomVector3(0.25f, 4.0f);
117+
118+
// Wait one frame so the NetworkTransform can apply the owner's last state received on the late joining client side
119+
// (i.e. prevent the non-owner from changing the transform)
120+
yield return null;
121+
122+
// Get the owner instance
123+
var ownerInstance = VerifyObjectIsSpawnedOnClient.GetClientInstance(m_ClientNetworkManagers[0].LocalClientId);
124+
125+
// Verify that the non-owner instance transform values are the same before they were changed last frame
126+
Assert.True(Approximately(currentPosition, newClientObjectInstance.transform.position), $"Non-owner instance was able to change the position!");
127+
Assert.True(Approximately(currentRotation, newClientObjectInstance.transform.rotation.eulerAngles), $"Non-owner instance was able to change the rotation!");
128+
Assert.True(Approximately(currentScale, newClientObjectInstance.transform.localScale), $"Non-owner instance was able to change the scale!");
129+
130+
// Verify that the non-owner instance transform is still the same as the owner instance transform
131+
Assert.True(Approximately(ownerInstance.transform.position, newClientObjectInstance.transform.position), "Non-owner and owner instance position values are not the same!");
132+
Assert.True(Approximately(ownerInstance.transform.rotation.eulerAngles, newClientObjectInstance.transform.rotation.eulerAngles), "Non-owner and owner instance rotation values are not the same!");
133+
Assert.True(Approximately(ownerInstance.transform.localScale, newClientObjectInstance.transform.localScale), "Non-owner and owner instance scale values are not the same!");
134+
}
135+
49136
public enum StartingOwnership
50137
{
51138
HostStartsAsOwner,

com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Unity.Netcode.Components;
33
using UnityEngine;
44

5+
56
namespace Unity.Netcode.RuntimeTests
67
{
78

@@ -89,6 +90,125 @@ private bool WillAnAxisBeSynchronized(ref NetworkTransform networkTransform)
8990
networkTransform.SyncPositionX || networkTransform.SyncPositionY || networkTransform.SyncPositionZ;
9091
}
9192

93+
[Test]
94+
public void NetworkTransformStateFlags()
95+
{
96+
var indexValues = new System.Collections.Generic.List<uint>();
97+
var currentFlag = (uint)0x00000001;
98+
for (int j = 0; j < 18; j++)
99+
{
100+
indexValues.Add(currentFlag);
101+
currentFlag = currentFlag << 1;
102+
}
103+
104+
// TrackByStateId is unique
105+
indexValues.Add(0x10000000);
106+
107+
var boolSet = new System.Collections.Generic.List<bool>();
108+
var transformState = new NetworkTransform.NetworkTransformState();
109+
// Test setting one at a time.
110+
for (int j = 0; j < 19; j++)
111+
{
112+
boolSet = new System.Collections.Generic.List<bool>();
113+
for (int i = 0; i < 19; i++)
114+
{
115+
if (i == j)
116+
{
117+
boolSet.Add(true);
118+
}
119+
else
120+
{
121+
boolSet.Add(false);
122+
}
123+
}
124+
transformState = new NetworkTransform.NetworkTransformState()
125+
{
126+
InLocalSpace = boolSet[0],
127+
HasPositionX = boolSet[1],
128+
HasPositionY = boolSet[2],
129+
HasPositionZ = boolSet[3],
130+
HasRotAngleX = boolSet[4],
131+
HasRotAngleY = boolSet[5],
132+
HasRotAngleZ = boolSet[6],
133+
HasScaleX = boolSet[7],
134+
HasScaleY = boolSet[8],
135+
HasScaleZ = boolSet[9],
136+
IsTeleportingNextFrame = boolSet[10],
137+
UseInterpolation = boolSet[11],
138+
QuaternionSync = boolSet[12],
139+
QuaternionCompression = boolSet[13],
140+
UseHalfFloatPrecision = boolSet[14],
141+
IsSynchronizing = boolSet[15],
142+
UsePositionSlerp = boolSet[16],
143+
IsParented = boolSet[17],
144+
TrackByStateId = boolSet[18],
145+
};
146+
Assert.True((transformState.BitSet & indexValues[j]) == indexValues[j], $"[FlagTest][Individual] Set flag value {indexValues[j]} at index {j}, but BitSet value did not match!");
147+
}
148+
149+
// Test setting all flag values
150+
boolSet = new System.Collections.Generic.List<bool>();
151+
for (int i = 0; i < 19; i++)
152+
{
153+
boolSet.Add(true);
154+
}
155+
156+
transformState = new NetworkTransform.NetworkTransformState()
157+
{
158+
InLocalSpace = boolSet[0],
159+
HasPositionX = boolSet[1],
160+
HasPositionY = boolSet[2],
161+
HasPositionZ = boolSet[3],
162+
HasRotAngleX = boolSet[4],
163+
HasRotAngleY = boolSet[5],
164+
HasRotAngleZ = boolSet[6],
165+
HasScaleX = boolSet[7],
166+
HasScaleY = boolSet[8],
167+
HasScaleZ = boolSet[9],
168+
IsTeleportingNextFrame = boolSet[10],
169+
UseInterpolation = boolSet[11],
170+
QuaternionSync = boolSet[12],
171+
QuaternionCompression = boolSet[13],
172+
UseHalfFloatPrecision = boolSet[14],
173+
IsSynchronizing = boolSet[15],
174+
UsePositionSlerp = boolSet[16],
175+
IsParented = boolSet[17],
176+
TrackByStateId = boolSet[18],
177+
};
178+
179+
for (int j = 0; j < 19; j++)
180+
{
181+
Assert.True((transformState.BitSet & indexValues[j]) == indexValues[j], $"[FlagTest][All] All flag values are set but failed to detect flag value {indexValues[j]}!");
182+
}
183+
184+
// Test getting all flag values
185+
transformState = new NetworkTransform.NetworkTransformState();
186+
for (int i = 0; i < 19; i++)
187+
{
188+
transformState.BitSet |= indexValues[i];
189+
}
190+
191+
Assert.True(transformState.InLocalSpace, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.InLocalSpace)}!");
192+
Assert.True(transformState.HasPositionX, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasPositionX)}!");
193+
Assert.True(transformState.HasPositionY, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasPositionY)}!");
194+
Assert.True(transformState.HasPositionZ, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasPositionZ)}!");
195+
Assert.True(transformState.HasRotAngleX, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasRotAngleX)}!");
196+
Assert.True(transformState.HasRotAngleY, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasRotAngleY)}!");
197+
Assert.True(transformState.HasRotAngleZ, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasRotAngleZ)}!");
198+
Assert.True(transformState.HasScaleX, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasScaleX)}!");
199+
Assert.True(transformState.HasScaleY, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasScaleY)}!");
200+
Assert.True(transformState.HasScaleZ, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasScaleZ)}!");
201+
Assert.True(transformState.IsTeleportingNextFrame, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.IsTeleportingNextFrame)}!");
202+
Assert.True(transformState.UseInterpolation, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.UseInterpolation)}!");
203+
Assert.True(transformState.QuaternionSync, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.QuaternionSync)}!");
204+
Assert.True(transformState.QuaternionCompression, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.QuaternionCompression)}!");
205+
Assert.True(transformState.UseHalfFloatPrecision, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.UseHalfFloatPrecision)}!");
206+
Assert.True(transformState.IsSynchronizing, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.IsSynchronizing)}!");
207+
Assert.True(transformState.UsePositionSlerp, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.UsePositionSlerp)}!");
208+
Assert.True(transformState.IsParented, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.IsParented)}!");
209+
Assert.True(transformState.TrackByStateId, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.TrackByStateId)}!");
210+
}
211+
92212
[Test]
93213
public void TestSyncAxes([Values] SynchronizationType synchronizationType, [Values] SyncAxis syncAxis)
94214

0 commit comments

Comments
 (0)