Skip to content

fix: NetworkTransform synchronization fixes and owner authoritative performance improvement on server-host [MTT-6971] #2636

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ Additional documentation and release notes are available at [Multiplayer Documen

### Fixed

- 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)
- Fixed issue with scale not synchronizing properly depending upon the spawn order of NetworkObjects. (#2636)
- Fixed issue position was not properly transitioning between ownership changes with an owner authoritative NetworkTransform. (#2636)
- 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)

### Changed

## [1.5.2] - 2023-07-24

### Added

### Fixed

- Fixed issue where `NetworkClient.OwnedObjects` was not returning any owned objects due to the `NetworkClient.IsConnected` not being properly set. (#2631)
- Fixed a crash when calling TrySetParent with a null Transform (#2625)
- Fixed issue where a `NetworkTransform` using full precision state updates was losing transform state updates when interpolation was enabled. (#2624)
Expand Down
13 changes: 12 additions & 1 deletion com.unity.netcode.gameobjects/Components/HalfVector3.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public struct HalfVector3 : INetworkSerializable
/// <summary>
/// The half float precision value of the z-axis as a <see cref="half"/>.
/// </summary>
public half Z => Axis.x;
public half Z => Axis.z;

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

/// <summary>
/// Directly sets each axial value to the passed in full precision values
/// that are converted to half precision
/// </summary>
internal void Set(float x, float y, float z)
{
Axis.x = math.half(x);
Axis.y = math.half(y);
Axis.z = math.half(z);
}

private void SerializeWrite(FastBufferWriter writer)
{
for (int i = 0; i < Length; i++)
Expand Down
424 changes: 305 additions & 119 deletions com.unity.netcode.gameobjects/Components/NetworkTransform.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ public bool Deserialize(FastBufferReader reader, ref NetworkContext context, int

public void Handle(ref NetworkContext context)
{
((NetworkManager)context.SystemOwner).CustomMessagingManager.InvokeNamedMessage(Hash, context.SenderId, m_ReceiveData, context.SerializedHeaderSize);
var networkManager = (NetworkManager)context.SystemOwner;
if (!networkManager.ShutdownInProgress)
{
((NetworkManager)context.SystemOwner).CustomMessagingManager.InvokeNamedMessage(Hash, context.SenderId, m_ReceiveData, context.SerializedHeaderSize);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -518,15 +518,18 @@ internal int GetMessageVersion(Type type, ulong clientId, bool forReceive = fals
{
if (!m_PerClientMessageVersions.TryGetValue(clientId, out var versionMap))
{
if (forReceive)
var networkManager = NetworkManager.Singleton;
if (networkManager != null && networkManager.LogLevel == LogLevel.Developer)
{
Debug.LogWarning($"Trying to receive {type.Name} from client {clientId} which is not in a connected state.");
}
else
{
Debug.LogWarning($"Trying to send {type.Name} to client {clientId} which is not in a connected state.");
if (forReceive)
{
NetworkLog.LogWarning($"Trying to receive {type.Name} from client {clientId} which is not in a connected state.");
}
else
{
NetworkLog.LogWarning($"Trying to send {type.Name} to client {clientId} which is not in a connected state.");
}
}

return -1;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,93 @@ protected override void OnServerAndClientsCreated()
base.OnServerAndClientsCreated();
}

/// <summary>
/// Clients created during a test need to have their prefabs list updated to
/// match the server's prefab list.
/// </summary>
protected override void OnNewClientCreated(NetworkManager networkManager)
{
foreach (var networkPrefab in m_ServerNetworkManager.NetworkConfig.Prefabs.Prefabs)
{
networkManager.NetworkConfig.Prefabs.Add(networkPrefab);
}

base.OnNewClientCreated(networkManager);
}

private bool ClientIsOwner()
{
var clientId = m_ClientNetworkManagers[0].LocalClientId;
if (!VerifyObjectIsSpawnedOnClient.GetClientsThatSpawnedThisPrefab().Contains(clientId))
{
return false;
}
if (VerifyObjectIsSpawnedOnClient.GetClientInstance(clientId).OwnerClientId != clientId)
{
return false;
}
return true;
}

/// <summary>
/// This test verifies a late joining client cannot change the transform when:
/// - A NetworkObject is spawned with a host and one or more connected clients
/// - The NetworkTransform is owner authoritative and spawned with the host as the owner
/// - The host does not change the transform values
/// - One of the already connected clients gains ownership of the spawned NetworkObject
/// - The new client owner does not change the transform values
/// - A new late joining client connects and is synchronized
/// - The newly connected late joining client tries to change the transform of the NetworkObject
/// it does not own
/// </summary>
[UnityTest]
public IEnumerator LateJoinedNonOwnerClientCannotChangeTransform()
{
// Spawn the m_ClientNetworkTransformPrefab with the host starting as the owner
var hostInstance = SpawnObject(m_ClientNetworkTransformPrefab, m_ServerNetworkManager);

// Wait for the client to spawn it
yield return WaitForConditionOrTimeOut(() => VerifyObjectIsSpawnedOnClient.GetClientsThatSpawnedThisPrefab().Contains(m_ClientNetworkManagers[0].LocalClientId));

// Change the ownership to the connectd client
hostInstance.GetComponent<NetworkObject>().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId);

// Wait until the client gains ownership
yield return WaitForConditionOrTimeOut(ClientIsOwner);

// Spawn a new client
yield return CreateAndStartNewClient();

// Get the instance of the object relative to the newly joined client
var newClientObjectInstance = VerifyObjectIsSpawnedOnClient.GetClientInstance(m_ClientNetworkManagers[1].LocalClientId);

// Attempt to change the transform values
var currentPosition = newClientObjectInstance.transform.position;
newClientObjectInstance.transform.position = GetRandomVector3(0.5f, 10.0f);
var rotation = newClientObjectInstance.transform.rotation;
var currentRotation = rotation.eulerAngles;
rotation.eulerAngles = GetRandomVector3(1.0f, 180.0f);
var currentScale = newClientObjectInstance.transform.localScale;
newClientObjectInstance.transform.localScale = GetRandomVector3(0.25f, 4.0f);

// Wait one frame so the NetworkTransform can apply the owner's last state received on the late joining client side
// (i.e. prevent the non-owner from changing the transform)
yield return null;

// Get the owner instance
var ownerInstance = VerifyObjectIsSpawnedOnClient.GetClientInstance(m_ClientNetworkManagers[0].LocalClientId);

// Verify that the non-owner instance transform values are the same before they were changed last frame
Assert.True(Approximately(currentPosition, newClientObjectInstance.transform.position), $"Non-owner instance was able to change the position!");
Assert.True(Approximately(currentRotation, newClientObjectInstance.transform.rotation.eulerAngles), $"Non-owner instance was able to change the rotation!");
Assert.True(Approximately(currentScale, newClientObjectInstance.transform.localScale), $"Non-owner instance was able to change the scale!");

// Verify that the non-owner instance transform is still the same as the owner instance transform
Assert.True(Approximately(ownerInstance.transform.position, newClientObjectInstance.transform.position), "Non-owner and owner instance position values are not the same!");
Assert.True(Approximately(ownerInstance.transform.rotation.eulerAngles, newClientObjectInstance.transform.rotation.eulerAngles), "Non-owner and owner instance rotation values are not the same!");
Assert.True(Approximately(ownerInstance.transform.localScale, newClientObjectInstance.transform.localScale), "Non-owner and owner instance scale values are not the same!");
}

public enum StartingOwnership
{
HostStartsAsOwner,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Unity.Netcode.Components;
using UnityEngine;


namespace Unity.Netcode.RuntimeTests
{

Expand Down Expand Up @@ -89,6 +90,125 @@ private bool WillAnAxisBeSynchronized(ref NetworkTransform networkTransform)
networkTransform.SyncPositionX || networkTransform.SyncPositionY || networkTransform.SyncPositionZ;
}

[Test]
public void NetworkTransformStateFlags()
{
var indexValues = new System.Collections.Generic.List<uint>();
var currentFlag = (uint)0x00000001;
for (int j = 0; j < 18; j++)
{
indexValues.Add(currentFlag);
currentFlag = currentFlag << 1;
}

// TrackByStateId is unique
indexValues.Add(0x10000000);

var boolSet = new System.Collections.Generic.List<bool>();
var transformState = new NetworkTransform.NetworkTransformState();
// Test setting one at a time.
for (int j = 0; j < 19; j++)
{
boolSet = new System.Collections.Generic.List<bool>();
for (int i = 0; i < 19; i++)
{
if (i == j)
{
boolSet.Add(true);
}
else
{
boolSet.Add(false);
}
}
transformState = new NetworkTransform.NetworkTransformState()
{
InLocalSpace = boolSet[0],
HasPositionX = boolSet[1],
HasPositionY = boolSet[2],
HasPositionZ = boolSet[3],
HasRotAngleX = boolSet[4],
HasRotAngleY = boolSet[5],
HasRotAngleZ = boolSet[6],
HasScaleX = boolSet[7],
HasScaleY = boolSet[8],
HasScaleZ = boolSet[9],
IsTeleportingNextFrame = boolSet[10],
UseInterpolation = boolSet[11],
QuaternionSync = boolSet[12],
QuaternionCompression = boolSet[13],
UseHalfFloatPrecision = boolSet[14],
IsSynchronizing = boolSet[15],
UsePositionSlerp = boolSet[16],
IsParented = boolSet[17],
TrackByStateId = boolSet[18],
};
Assert.True((transformState.BitSet & indexValues[j]) == indexValues[j], $"[FlagTest][Individual] Set flag value {indexValues[j]} at index {j}, but BitSet value did not match!");
}

// Test setting all flag values
boolSet = new System.Collections.Generic.List<bool>();
for (int i = 0; i < 19; i++)
{
boolSet.Add(true);
}

transformState = new NetworkTransform.NetworkTransformState()
{
InLocalSpace = boolSet[0],
HasPositionX = boolSet[1],
HasPositionY = boolSet[2],
HasPositionZ = boolSet[3],
HasRotAngleX = boolSet[4],
HasRotAngleY = boolSet[5],
HasRotAngleZ = boolSet[6],
HasScaleX = boolSet[7],
HasScaleY = boolSet[8],
HasScaleZ = boolSet[9],
IsTeleportingNextFrame = boolSet[10],
UseInterpolation = boolSet[11],
QuaternionSync = boolSet[12],
QuaternionCompression = boolSet[13],
UseHalfFloatPrecision = boolSet[14],
IsSynchronizing = boolSet[15],
UsePositionSlerp = boolSet[16],
IsParented = boolSet[17],
TrackByStateId = boolSet[18],
};

for (int j = 0; j < 19; j++)
{
Assert.True((transformState.BitSet & indexValues[j]) == indexValues[j], $"[FlagTest][All] All flag values are set but failed to detect flag value {indexValues[j]}!");
}

// Test getting all flag values
transformState = new NetworkTransform.NetworkTransformState();
for (int i = 0; i < 19; i++)
{
transformState.BitSet |= indexValues[i];
}

Assert.True(transformState.InLocalSpace, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.InLocalSpace)}!");
Assert.True(transformState.HasPositionX, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasPositionX)}!");
Assert.True(transformState.HasPositionY, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasPositionY)}!");
Assert.True(transformState.HasPositionZ, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasPositionZ)}!");
Assert.True(transformState.HasRotAngleX, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasRotAngleX)}!");
Assert.True(transformState.HasRotAngleY, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasRotAngleY)}!");
Assert.True(transformState.HasRotAngleZ, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasRotAngleZ)}!");
Assert.True(transformState.HasScaleX, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasScaleX)}!");
Assert.True(transformState.HasScaleY, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasScaleY)}!");
Assert.True(transformState.HasScaleZ, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.HasScaleZ)}!");
Assert.True(transformState.IsTeleportingNextFrame, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.IsTeleportingNextFrame)}!");
Assert.True(transformState.UseInterpolation, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.UseInterpolation)}!");
Assert.True(transformState.QuaternionSync, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.QuaternionSync)}!");
Assert.True(transformState.QuaternionCompression, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.QuaternionCompression)}!");
Assert.True(transformState.UseHalfFloatPrecision, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.UseHalfFloatPrecision)}!");
Assert.True(transformState.IsSynchronizing, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.IsSynchronizing)}!");
Assert.True(transformState.UsePositionSlerp, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.UsePositionSlerp)}!");
Assert.True(transformState.IsParented, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.IsParented)}!");
Assert.True(transformState.TrackByStateId, $"[FlagTest][Get] Failed to detect {nameof(NetworkTransform.NetworkTransformState.TrackByStateId)}!");
}

[Test]
public void TestSyncAxes([Values] SynchronizationType synchronizationType, [Values] SyncAxis syncAxis)

Expand Down
Loading