diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md
index 5814739ba6..1d58cf423f 100644
--- a/com.unity.netcode.gameobjects/CHANGELOG.md
+++ b/com.unity.netcode.gameobjects/CHANGELOG.md
@@ -14,8 +14,12 @@ Additional documentation and release notes are available at [Multiplayer Documen
- Fixed a bug where having a class with Rpcs that inherits from a class without Rpcs that inherits from NetworkVariable would cause a compile error. (#2751)
- Fixed issue where `NetworkBehaviour.Synchronize` was not truncating the write buffer if nothing was serialized during `NetworkBehaviour.OnSynchronize` causing an additional 6 bytes to be written per `NetworkBehaviour` component instance. (#2749)
+- Fixed issue where a parented in-scene placed NetworkObject would be destroyed upon a client or server exiting a network session but not unloading the original scene in which the NetworkObject was placed. (#2737)
+- Fixed issue where during client synchronization and scene loading, when client synchronization or the scene loading mode are set to `LoadSceneMode.Single`, a `CreateObjectMessage` could be received, processed, and the resultant spawned `NetworkObject` could be instantiated in the client's currently active scene that could, towards the end of the client synchronization or loading process, be unloaded and cause the newly created `NetworkObject` to be destroyed (and throw and exception). (#2735)
+- Fixed issue where a `NetworkTransform` instance with interpolation enabled would result in wide visual motion gaps (stuttering) under above normal latency conditions and a 1-5% or higher packet are drop rate. (#2713)
### Changed
+- Changed `NetworkTransform` authoritative instance tick registration so a single `NetworkTransform` specific tick event update will update all authoritative instances to improve perofmance. (#2713)
## [1.7.0] - 2023-10-11
diff --git a/com.unity.netcode.gameobjects/Components/NetworkDeltaPosition.cs b/com.unity.netcode.gameobjects/Components/NetworkDeltaPosition.cs
index a7ed563106..ec5c176874 100644
--- a/com.unity.netcode.gameobjects/Components/NetworkDeltaPosition.cs
+++ b/com.unity.netcode.gameobjects/Components/NetworkDeltaPosition.cs
@@ -23,12 +23,20 @@ public struct NetworkDeltaPosition : INetworkSerializable
internal Vector3 DeltaPosition;
internal int NetworkTick;
+ internal bool SynchronizeBase;
+
+ internal bool CollapsedDeltaIntoBase;
+
///
/// The serialization implementation of
///
public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter
{
HalfVector3.NetworkSerialize(serializer);
+ if (SynchronizeBase)
+ {
+ serializer.SerializeValue(ref CurrentBasePosition);
+ }
}
///
@@ -122,6 +130,7 @@ public Vector3 GetDeltaPosition()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void UpdateFrom(ref Vector3 vector3, int networkTick)
{
+ CollapsedDeltaIntoBase = false;
NetworkTick = networkTick;
DeltaPosition = (vector3 + PrecisionLossDelta) - CurrentBasePosition;
for (int i = 0; i < HalfVector3.Length; i++)
@@ -136,6 +145,7 @@ public void UpdateFrom(ref Vector3 vector3, int networkTick)
CurrentBasePosition[i] += HalfDeltaConvertedBack[i];
HalfDeltaConvertedBack[i] = 0.0f;
DeltaPosition[i] = 0.0f;
+ CollapsedDeltaIntoBase = true;
}
}
}
@@ -164,6 +174,8 @@ public NetworkDeltaPosition(Vector3 vector3, int networkTick, bool3 axisToSynchr
DeltaPosition = Vector3.zero;
HalfDeltaConvertedBack = Vector3.zero;
HalfVector3 = new HalfVector3(vector3, axisToSynchronize);
+ SynchronizeBase = false;
+ CollapsedDeltaIntoBase = false;
UpdateFrom(ref vector3, networkTick);
}
diff --git a/com.unity.netcode.gameobjects/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Components/NetworkTransform.cs
index 2e0b8d88d8..3fd3436352 100644
--- a/com.unity.netcode.gameobjects/Components/NetworkTransform.cs
+++ b/com.unity.netcode.gameobjects/Components/NetworkTransform.cs
@@ -55,6 +55,29 @@ public class NetworkTransform : NetworkBehaviour
///
internal static bool TrackByStateId;
+ ///
+ /// Enabled by default.
+ /// When set (enabled by default), NetworkTransform will send common state updates using unreliable network delivery
+ /// to provide a higher tolerance to poor network conditions (especially packet loss). When disabled, all state updates
+ /// are sent using a reliable fragmented sequenced network delivery.
+ ///
+ ///
+ /// The following more critical state updates are still sent as reliable fragmented sequenced:
+ /// - The initial synchronization state update
+ /// - The teleporting state update.
+ /// - When using half float precision and the `NetworkDeltaPosition` delta exceeds the maximum delta forcing the axis in
+ /// question to be collapsed into the core base position, this state update will be sent as reliable fragmented sequenced.
+ ///
+ /// In order to preserve a continual consistency of axial values when unreliable delta messaging is enabled (due to the
+ /// possibility of dropping packets), NetworkTransform instances will send 1 axial frame synchronization update per
+ /// second (only for the axis marked to synchronize are sent as reliable fragmented sequenced) as long as a delta state
+ /// update had been previously sent. When a NetworkObject is at rest, axial frame synchronization updates are not sent.
+ ///
+ [Tooltip("When set (enabled by default), NetworkTransform will send common state updates using unreliable network delivery " +
+ "to provide a higher tolerance to poor network conditions (especially packet loss). When disabled, all state updates are " +
+ "sent using reliable fragmented sequenced network delivery.")]
+ public bool UseUnreliableDeltas = true;
+
///
/// Data structure used to synchronize the
///
@@ -78,6 +101,10 @@ public struct NetworkTransformState : INetworkSerializable
private const int k_Synchronization = 0x00008000;
private const int k_PositionSlerp = 0x00010000; // Persists between state updates (authority dictates if this is set)
private const int k_IsParented = 0x00020000; // When parented and synchronizing, we need to have both lossy and local scale due to varying spawn order
+ private const int k_SynchBaseHalfFloat = 0x00040000;
+ private const int k_ReliableFragmentedSequenced = 0x00080000;
+ private const int k_UseUnreliableDeltas = 0x00100000;
+ private const int k_UnreliableFrameSync = 0x00200000;
private const int k_TrackStateId = 0x10000000; // (Internal Debugging) When set each state update will contain a state identifier
// Stores persistent and state relative flags
@@ -438,6 +465,42 @@ internal bool IsParented
}
}
+ internal bool SynchronizeBaseHalfFloat
+ {
+ get => GetFlag(k_SynchBaseHalfFloat);
+ set
+ {
+ SetFlag(value, k_SynchBaseHalfFloat);
+ }
+ }
+
+ internal bool ReliableFragmentedSequenced
+ {
+ get => GetFlag(k_ReliableFragmentedSequenced);
+ set
+ {
+ SetFlag(value, k_ReliableFragmentedSequenced);
+ }
+ }
+
+ internal bool UseUnreliableDeltas
+ {
+ get => GetFlag(k_UseUnreliableDeltas);
+ set
+ {
+ SetFlag(value, k_UseUnreliableDeltas);
+ }
+ }
+
+ internal bool UnreliableFrameSync
+ {
+ get => GetFlag(k_UnreliableFrameSync);
+ set
+ {
+ SetFlag(value, k_UnreliableFrameSync);
+ }
+ }
+
internal bool TrackByStateId
{
get => GetFlag(k_TrackStateId);
@@ -463,7 +526,7 @@ private void SetFlag(bool set, int flag)
internal void ClearBitSetForNextTick()
{
// Clear everything but flags that should persist between state updates until changed by authority
- m_Bitset &= k_InLocalSpaceBit | k_Interpolate | k_UseHalfFloats | k_QuaternionSync | k_QuaternionCompress | k_PositionSlerp;
+ m_Bitset &= k_InLocalSpaceBit | k_Interpolate | k_UseHalfFloats | k_QuaternionSync | k_QuaternionCompress | k_PositionSlerp | k_UseUnreliableDeltas;
IsDirty = false;
}
@@ -594,6 +657,24 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade
{
if (isWriting)
{
+ if (UseUnreliableDeltas)
+ {
+ // If teleporting, synchronizing, doing an axial frame sync, or using half float precision and we collapsed a delta into the base position
+ if (IsTeleportingNextFrame || IsSynchronizing || UnreliableFrameSync || (UseHalfFloatPrecision && NetworkDeltaPosition.CollapsedDeltaIntoBase))
+ {
+ // Send the message reliably
+ ReliableFragmentedSequenced = true;
+ }
+ else
+ {
+ ReliableFragmentedSequenced = false;
+ }
+ }
+ else // If not using UseUnreliableDeltas, then always use reliable fragmented sequenced
+ {
+ ReliableFragmentedSequenced = true;
+ }
+
BytePacker.WriteValueBitPacked(m_Writer, m_Bitset);
// We use network ticks as opposed to absolute time as the authoritative
// side updates on every new tick.
@@ -620,6 +701,8 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade
{
if (UseHalfFloatPrecision)
{
+ NetworkDeltaPosition.SynchronizeBase = SynchronizeBaseHalfFloat;
+
// Apply which axis should be updated for both write/read (teleporting, synchronizing, or just updating)
NetworkDeltaPosition.HalfVector3.AxisToSynchronize[0] = HasPositionX;
NetworkDeltaPosition.HalfVector3.AxisToSynchronize[1] = HasPositionY;
@@ -1083,7 +1166,7 @@ private bool SynchronizeScale
/// Internally used by to keep track of the instance assigned to this
/// this derived class instance.
///
- protected NetworkManager m_CachedNetworkManager; // Note: we no longer use this and are only keeping it until we decide to deprecate it
+ protected NetworkManager m_CachedNetworkManager;
///
/// Helper method that returns the space relative position of the transform.
@@ -1194,7 +1277,17 @@ public Vector3 GetScale(bool getCurrentState = false)
// This represents the most recent local authoritative state.
private NetworkTransformState m_LocalAuthoritativeNetworkState;
- internal NetworkTransformState LocalAuthoritativeNetworkState => m_LocalAuthoritativeNetworkState;
+ internal NetworkTransformState LocalAuthoritativeNetworkState
+ {
+ get
+ {
+ return m_LocalAuthoritativeNetworkState;
+ }
+ set
+ {
+ m_LocalAuthoritativeNetworkState = value;
+ }
+ }
private ClientRpcParams m_ClientRpcParams = new ClientRpcParams() { Send = new ClientRpcSendParams() };
private List m_ClientIds = new List() { 0 };
@@ -1408,6 +1501,11 @@ protected virtual void OnAuthorityPushTransformState(ref NetworkTransformState n
// Tracks the last tick a state update was sent (see further below)
private int m_LastTick;
+
+ // Only set if a delta has been sent, this is reset after an axial synch has been sent
+ // to assure the instance doesn't continue to send axial synchs when an object is at rest.
+ private bool m_DeltaSynch;
+
///
/// Authoritative side only
/// If there are any transform delta states, this method will synchronize the
@@ -1437,7 +1535,8 @@ private void TryCommitTransform(ref Transform transformToCommit, bool synchroniz
{
m_LocalAuthoritativeNetworkState.NetworkTick = m_LocalAuthoritativeNetworkState.NetworkTick + 1;
}
- else
+ else // If we are sending unreliable deltas this could happen with (axial) frame synch
+ if (!UseUnreliableDeltas)
{
NetworkLog.LogError($"[NT TICK DUPLICATE] Server already sent an update on tick {m_LastTick} and is attempting to send again on the same network tick!");
}
@@ -1446,6 +1545,14 @@ private void TryCommitTransform(ref Transform transformToCommit, bool synchroniz
// Update the state
UpdateTransformState();
+ // When sending unreliable traffic, we send 1 full (axial) frame synch every nth tick
+ // which is based on tick rate and each instance's (axial) frame synch is distributed
+ // across the ticks per second span (excluding initial synchronization).
+ if (UseUnreliableDeltas && !m_LocalAuthoritativeNetworkState.UnreliableFrameSync && !synchronize)
+ {
+ m_DeltaSynch = true;
+ }
+
OnAuthorityPushTransformState(ref m_LocalAuthoritativeNetworkState);
m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame = false;
}
@@ -1490,11 +1597,13 @@ internal NetworkTransformState ApplyLocalNetworkState(Transform transform)
///
internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkState, double dirtyTime, Transform transformToUse)
{
+ m_CachedNetworkManager = NetworkManager;
// Apply the interpolate and PostionDeltaCompression flags, otherwise we get false positives whether something changed or not.
networkState.UseInterpolation = Interpolate;
networkState.QuaternionSync = UseQuaternionSynchronization;
networkState.UseHalfFloatPrecision = UseHalfFloatPrecision;
networkState.QuaternionCompression = UseQuaternionCompression;
+ networkState.UseUnreliableDeltas = UseUnreliableDeltas;
m_HalfPositionState = new NetworkDeltaPosition(Vector3.zero, 0, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
return ApplyTransformToNetworkStateWithInfo(ref networkState, ref transformToUse);
@@ -1506,6 +1615,23 @@ internal bool ApplyTransformToNetworkState(ref NetworkTransformState networkStat
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState networkState, ref Transform transformToUse, bool isSynchronization = false, ulong targetClientId = 0)
{
+ // As long as we are not teleporting or doing our first synchronization and we are sending unreliable deltas,
+ // each NetworkTransform will stagger their axial synchronization over a 1 second period based on their
+ // assigned tick synchronization (m_TickSync) value.
+ var isAxisSync = false;
+ if (!networkState.IsTeleportingNextFrame && !isSynchronization && m_DeltaSynch && UseUnreliableDeltas)
+ {
+ var modTick = m_CachedNetworkManager.ServerTime.Tick % m_CachedNetworkManager.NetworkConfig.TickRate;
+ isAxisSync = modTick == m_TickSync;
+
+ if (isAxisSync)
+ {
+ m_DeltaSynch = false;
+ }
+ }
+ // This is used to determine if we need to send the state update reliably (if we are doing an axial sync)
+ networkState.UnreliableFrameSync = isAxisSync;
+
var isTeleportingAndNotSynchronizing = networkState.IsTeleportingNextFrame && !isSynchronization;
var isDirty = false;
var isPositionDirty = isTeleportingAndNotSynchronizing ? networkState.HasPositionChange : false;
@@ -1515,9 +1641,54 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw
var position = InLocalSpace ? transformToUse.localPosition : transformToUse.position;
var rotAngles = InLocalSpace ? transformToUse.localEulerAngles : transformToUse.eulerAngles;
var scale = transformToUse.localScale;
-
networkState.IsSynchronizing = isSynchronization;
+ // Check for parenting when synchronizing and/or teleporting
+ if (isSynchronization || networkState.IsTeleportingNextFrame)
+ {
+ // This all has to do with complex nested hierarchies and how it impacts scale
+ // when set for the first time and depending upon whether the NetworkObject is parented
+ // (or not parented) at the time the scale values are applied.
+ var hasParentNetworkObject = false;
+
+ // If the NetworkObject belonging to this NetworkTransform instance has a parent
+ // (i.e. this handles nested NetworkTransforms under a parent at some layer above)
+ if (NetworkObject.transform.parent != null)
+ {
+ var parentNetworkObject = NetworkObject.transform.parent.GetComponent();
+
+ // In-scene placed NetworkObjects parented under a GameObject with no
+ // NetworkObject preserve their lossyScale when synchronizing.
+ if (parentNetworkObject == null && NetworkObject.IsSceneObject != false)
+ {
+ hasParentNetworkObject = true;
+ }
+ else
+ {
+ // Or if the relative NetworkObject has a parent NetworkObject
+ hasParentNetworkObject = parentNetworkObject != null;
+ }
+ }
+
+ networkState.IsParented = hasParentNetworkObject;
+
+ // When synchronizing with a parent, world position stays impacts position whether
+ // the NetworkTransform is using world or local space synchronization.
+ // WorldPositionStays: (always use world space)
+ // !WorldPositionStays: (always use local space)
+ if (isSynchronization)
+ {
+ if (NetworkObject.WorldPositionStays())
+ {
+ position = transformToUse.position;
+ }
+ else
+ {
+ position = transformToUse.localPosition;
+ }
+ }
+ }
+
if (InLocalSpace != networkState.InLocalSpace)
{
networkState.InLocalSpace = InLocalSpace;
@@ -1561,23 +1732,30 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw
networkState.IsTeleportingNextFrame = true;
}
+ if (UseUnreliableDeltas != networkState.UseUnreliableDeltas)
+ {
+ networkState.UseUnreliableDeltas = UseUnreliableDeltas;
+ isDirty = true;
+ networkState.IsTeleportingNextFrame = true;
+ }
+
if (!UseHalfFloatPrecision)
{
- if (SyncPositionX && (Mathf.Abs(networkState.PositionX - position.x) >= PositionThreshold || networkState.IsTeleportingNextFrame))
+ if (SyncPositionX && (Mathf.Abs(networkState.PositionX - position.x) >= PositionThreshold || networkState.IsTeleportingNextFrame || isAxisSync))
{
networkState.PositionX = position.x;
networkState.HasPositionX = true;
isPositionDirty = true;
}
- if (SyncPositionY && (Mathf.Abs(networkState.PositionY - position.y) >= PositionThreshold || networkState.IsTeleportingNextFrame))
+ if (SyncPositionY && (Mathf.Abs(networkState.PositionY - position.y) >= PositionThreshold || networkState.IsTeleportingNextFrame || isAxisSync))
{
networkState.PositionY = position.y;
networkState.HasPositionY = true;
isPositionDirty = true;
}
- if (SyncPositionZ && (Mathf.Abs(networkState.PositionZ - position.z) >= PositionThreshold || networkState.IsTeleportingNextFrame))
+ if (SyncPositionZ && (Mathf.Abs(networkState.PositionZ - position.z) >= PositionThreshold || networkState.IsTeleportingNextFrame || isAxisSync))
{
networkState.PositionZ = position.z;
networkState.HasPositionZ = true;
@@ -1587,7 +1765,11 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw
else if (SynchronizePosition)
{
// If we are teleporting then we can skip the delta threshold check
- isPositionDirty = networkState.IsTeleportingNextFrame;
+ isPositionDirty = networkState.IsTeleportingNextFrame || isAxisSync;
+ if (m_HalfFloatTargetTickOwnership > m_CachedNetworkManager.ServerTime.Tick)
+ {
+ isPositionDirty = true;
+ }
// For NetworkDeltaPosition, if any axial value is dirty then we always send a full update
if (!isPositionDirty)
@@ -1628,6 +1810,15 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw
}
networkState.NetworkDeltaPosition = m_HalfPositionState;
+
+ if (m_HalfFloatTargetTickOwnership > m_CachedNetworkManager.ServerTime.Tick && !networkState.IsTeleportingNextFrame)
+ {
+ networkState.SynchronizeBaseHalfFloat = true;
+ }
+ else
+ {
+ networkState.SynchronizeBaseHalfFloat = UseUnreliableDeltas ? m_HalfPositionState.CollapsedDeltaIntoBase : false;
+ }
}
else // If synchronizing is set, then use the current full position value on the server side
{
@@ -1679,21 +1870,21 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw
if (!UseQuaternionSynchronization)
{
- if (SyncRotAngleX && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleX, rotAngles.x)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame))
+ if (SyncRotAngleX && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleX, rotAngles.x)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame || isAxisSync))
{
networkState.RotAngleX = rotAngles.x;
networkState.HasRotAngleX = true;
isRotationDirty = true;
}
- if (SyncRotAngleY && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleY, rotAngles.y)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame))
+ if (SyncRotAngleY && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleY, rotAngles.y)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame || isAxisSync))
{
networkState.RotAngleY = rotAngles.y;
networkState.HasRotAngleY = true;
isRotationDirty = true;
}
- if (SyncRotAngleZ && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleZ, rotAngles.z)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame))
+ if (SyncRotAngleZ && (Mathf.Abs(Mathf.DeltaAngle(networkState.RotAngleZ, rotAngles.z)) >= RotAngleThreshold || networkState.IsTeleportingNextFrame || isAxisSync))
{
networkState.RotAngleZ = rotAngles.z;
networkState.HasRotAngleZ = true;
@@ -1703,7 +1894,7 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw
else if (SynchronizeRotation)
{
// If we are teleporting then we can skip the delta threshold check
- isRotationDirty = networkState.IsTeleportingNextFrame;
+ isRotationDirty = networkState.IsTeleportingNextFrame || isAxisSync;
// For quaternion synchronization, if one angle is dirty we send a full update
if (!isRotationDirty)
{
@@ -1729,34 +1920,9 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw
// For scale, we need to check for parenting when synchronizing and/or teleporting
if (isSynchronization || networkState.IsTeleportingNextFrame)
{
- // This all has to do with complex nested hierarchies and how it impacts scale
- // when set for the first time and depending upon whether the NetworkObject is parented
- // (or not parented) at the time the scale values are applied.
- var hasParentNetworkObject = false;
-
- // If the NetworkObject belonging to this NetworkTransform instance has a parent
- // (i.e. this handles nested NetworkTransforms under a parent at some layer above)
- if (NetworkObject.transform.parent != null)
- {
- var parentNetworkObject = NetworkObject.transform.parent.GetComponent();
-
- // In-scene placed NetworkObjects parented under a GameObject with no
- // NetworkObject preserve their lossyScale when synchronizing.
- if (parentNetworkObject == null && NetworkObject.IsSceneObject != false)
- {
- hasParentNetworkObject = true;
- }
- else
- {
- // Or if the relative NetworkObject has a parent NetworkObject
- hasParentNetworkObject = parentNetworkObject != null;
- }
- }
-
- networkState.IsParented = hasParentNetworkObject;
// If we are synchronizing and the associated NetworkObject has a parent then we want to send the
- // LossyScale if the NetworkObject has a parent since NetworkObject spawn order is not guaranteed
- if (hasParentNetworkObject)
+ // LossyScale if the NetworkObject has a parent since NetworkObject spawn order is not guaranteed
+ if (networkState.IsParented)
{
networkState.LossyScale = transform.lossyScale;
}
@@ -1767,21 +1933,21 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw
{
if (!UseHalfFloatPrecision)
{
- if (SyncScaleX && (Mathf.Abs(networkState.ScaleX - scale.x) >= ScaleThreshold || networkState.IsTeleportingNextFrame))
+ if (SyncScaleX && (Mathf.Abs(networkState.ScaleX - scale.x) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync))
{
networkState.ScaleX = scale.x;
networkState.HasScaleX = true;
isScaleDirty = true;
}
- if (SyncScaleY && (Mathf.Abs(networkState.ScaleY - scale.y) >= ScaleThreshold || networkState.IsTeleportingNextFrame))
+ if (SyncScaleY && (Mathf.Abs(networkState.ScaleY - scale.y) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync))
{
networkState.ScaleY = scale.y;
networkState.HasScaleY = true;
isScaleDirty = true;
}
- if (SyncScaleZ && (Mathf.Abs(networkState.ScaleZ - scale.z) >= ScaleThreshold || networkState.IsTeleportingNextFrame))
+ if (SyncScaleZ && (Mathf.Abs(networkState.ScaleZ - scale.z) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync))
{
networkState.ScaleZ = scale.z;
networkState.HasScaleZ = true;
@@ -1793,7 +1959,7 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw
var previousScale = networkState.Scale;
for (int i = 0; i < 3; i++)
{
- if (Mathf.Abs(scale[i] - previousScale[i]) >= ScaleThreshold || networkState.IsTeleportingNextFrame)
+ if (Mathf.Abs(scale[i] - previousScale[i]) >= ScaleThreshold || networkState.IsTeleportingNextFrame || isAxisSync)
{
isScaleDirty = true;
networkState.Scale[i] = scale[i];
@@ -1828,7 +1994,7 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw
// NetworkManager
if (enabled)
{
- networkState.NetworkTick = NetworkManager.ServerTime.Tick;
+ networkState.NetworkTick = m_CachedNetworkManager.ServerTime.Tick;
}
}
@@ -1857,6 +2023,8 @@ private void ApplyAuthoritativeState()
UseHalfFloatPrecision = networkState.UseHalfFloatPrecision;
UseQuaternionSynchronization = networkState.QuaternionSync;
UseQuaternionCompression = networkState.QuaternionCompression;
+ UseUnreliableDeltas = networkState.UseUnreliableDeltas;
+
if (SlerpPosition != networkState.UsePositionSlerp)
{
SlerpPosition = networkState.UsePositionSlerp;
@@ -1922,7 +2090,7 @@ private void ApplyAuthoritativeState()
{
if (networkState.HasPositionChange && SynchronizePosition)
{
- adjustedPosition = networkState.CurrentPosition;
+ adjustedPosition = m_TargetPosition;
}
if (networkState.HasScaleChange && SynchronizeScale)
@@ -2067,6 +2235,7 @@ private void ApplyTeleportingState(NetworkTransformState newState)
// offset or not. This is specific to owner authoritative mode on the owner side only
if (isSynchronization)
{
+ // Need to use NetworkManager vs m_CachedNetworkManager here since we are yet to be spawned
if (ShouldSynchronizeHalfFloat(NetworkManager.LocalClientId))
{
m_HalfPositionState.HalfVector3.Axis = newState.NetworkDeltaPosition.HalfVector3.Axis;
@@ -2123,7 +2292,6 @@ private void ApplyTeleportingState(NetworkTransformState newState)
}
}
-
if (UseHalfFloatPrecision)
{
currentScale = shouldUseLossy ? newState.LossyScale : newState.Scale;
@@ -2216,6 +2384,8 @@ private void ApplyUpdatedState(NetworkTransformState newState)
UseQuaternionSynchronization = newState.QuaternionSync;
UseQuaternionCompression = newState.QuaternionCompression;
UseHalfFloatPrecision = newState.UseHalfFloatPrecision;
+ UseUnreliableDeltas = newState.UseUnreliableDeltas;
+
if (SlerpPosition != newState.UsePositionSlerp)
{
SlerpPosition = newState.UsePositionSlerp;
@@ -2236,11 +2406,21 @@ private void ApplyUpdatedState(NetworkTransformState newState)
// Only if using half float precision and our position had changed last update then
if (UseHalfFloatPrecision && m_LocalAuthoritativeNetworkState.HasPositionChange)
{
- // assure our local NetworkDeltaPosition state is updated
- m_HalfPositionState.HalfVector3.Axis = m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.HalfVector3.Axis;
- // and update our target position
+ if (m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat)
+ {
+ m_HalfPositionState = m_LocalAuthoritativeNetworkState.NetworkDeltaPosition;
+ }
+ else
+ {
+ // assure our local NetworkDeltaPosition state is updated
+ m_HalfPositionState.HalfVector3.Axis = m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.HalfVector3.Axis;
+ m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.CurrentBasePosition = m_HalfPositionState.CurrentBasePosition;
+
+ // This is to assure when you get the position of the state it is the correct position
+ m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.ToVector3(0);
+ }
+ // Update our target position
m_TargetPosition = m_HalfPositionState.ToVector3(newState.NetworkTick);
- m_LocalAuthoritativeNetworkState.NetworkDeltaPosition.CurrentBasePosition = m_HalfPositionState.CurrentBasePosition;
m_LocalAuthoritativeNetworkState.CurrentPosition = m_TargetPosition;
}
@@ -2369,13 +2549,15 @@ private void OnNetworkStateChanged(NetworkTransformState oldState, NetworkTransf
}
// Get the time when this new state was sent
- newState.SentTime = new NetworkTime(NetworkManager.NetworkConfig.TickRate, newState.NetworkTick).Time;
+ newState.SentTime = new NetworkTime(m_CachedNetworkManager.NetworkConfig.TickRate, newState.NetworkTick).Time;
// Apply the new state
ApplyUpdatedState(newState);
// Provide notifications when the state has been updated
- OnNetworkTransformStateUpdated(ref oldState, ref newState);
+ // We use the m_LocalAuthoritativeNetworkState because newState has been applied and adjustments could have
+ // been made (i.e. half float precision position values will have been updated)
+ OnNetworkTransformStateUpdated(ref oldState, ref m_LocalAuthoritativeNetworkState);
}
///
@@ -2491,13 +2673,10 @@ private void NetworkTickSystem_Tick()
m_CurrentPosition = GetSpaceRelativePosition();
m_TargetPosition = GetSpaceRelativePosition();
}
- else
+ else // If we are no longer authority, unsubscribe to the tick event
+ if (NetworkManager != null && NetworkManager.NetworkTickSystem != null)
{
- // If we are no longer authority, unsubscribe to the tick event
- if (NetworkManager != null && NetworkManager.NetworkTickSystem != null)
- {
- NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick;
- }
+ NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick;
}
}
@@ -2507,22 +2686,28 @@ public override void OnNetworkSpawn()
///////////////////////////////////////////////////////////////
// NOTE: Legacy and no longer used (candidates for deprecation)
m_CachedIsServer = IsServer;
- m_CachedNetworkManager = NetworkManager;
///////////////////////////////////////////////////////////////
+ // Started using this again to avoid the getter processing cost of NetworkBehaviour.NetworkManager
+ m_CachedNetworkManager = NetworkManager;
+
// Register a custom named message specifically for this instance
m_MessageName = $"NTU_{NetworkObjectId}_{NetworkBehaviourId}";
- NetworkManager.CustomMessagingManager.RegisterNamedMessageHandler(m_MessageName, TransformStateUpdate);
+ m_CachedNetworkManager.CustomMessagingManager.RegisterNamedMessageHandler(m_MessageName, TransformStateUpdate);
Initialize();
}
///
public override void OnNetworkDespawn()
{
+ // During destroy, use NetworkBehaviour.NetworkManager as opposed to m_CachedNetworkManager
if (!NetworkManager.ShutdownInProgress && NetworkManager.CustomMessagingManager != null)
{
NetworkManager.CustomMessagingManager.UnregisterNamedMessageHandler(m_MessageName);
}
+
+ DeregisterForTickUpdate(this);
+
CanCommitToTransform = false;
if (NetworkManager != null && NetworkManager.NetworkTickSystem != null)
{
@@ -2533,6 +2718,7 @@ public override void OnNetworkDespawn()
///
public override void OnDestroy()
{
+ // During destroy, use NetworkBehaviour.NetworkManager as opposed to m_CachedNetworkManager
if (NetworkManager != null && NetworkManager.NetworkTickSystem != null)
{
NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick;
@@ -2556,9 +2742,9 @@ public override void OnGainedOwnership()
protected override void OnOwnershipChanged(ulong previous, ulong current)
{
// If we were the previous owner or the newly assigned owner then reinitialize
- if (current == NetworkManager.LocalClientId || previous == NetworkManager.LocalClientId)
+ if (current == m_CachedNetworkManager.LocalClientId || previous == m_CachedNetworkManager.LocalClientId)
{
- Initialize();
+ InternalInitialization(true);
}
base.OnOwnershipChanged(previous, current);
}
@@ -2585,11 +2771,13 @@ protected virtual void OnInitialize(ref NetworkVariable r
}
+ private int m_HalfFloatTargetTickOwnership;
///
- /// Initializes NetworkTransform when spawned and ownership changes.
+ /// The internal initialzation method to allow for internal API adjustments
///
- protected void Initialize()
+ ///
+ private void InternalInitialization(bool isOwnershipChange = false)
{
if (!IsSpawned)
{
@@ -2604,25 +2792,31 @@ protected void Initialize()
{
if (UseHalfFloatPrecision)
{
- m_HalfPositionState = new NetworkDeltaPosition(currentPosition, NetworkManager.NetworkTickSystem.ServerTime.Tick, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
+ m_HalfPositionState = new NetworkDeltaPosition(currentPosition, m_CachedNetworkManager.NetworkTickSystem.ServerTime.Tick, math.bool3(SyncPositionX, SyncPositionY, SyncPositionZ));
}
m_CurrentPosition = currentPosition;
m_TargetPosition = currentPosition;
- // Authority only updates once per network tick
- NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick;
- NetworkManager.NetworkTickSystem.Tick += NetworkTickSystem_Tick;
- // Teleport to current position
- SetStateInternal(currentPosition, currentRotation, transform.localScale, true);
+ RegisterForTickUpdate(this);
+
+ m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat = false;
+ if (UseHalfFloatPrecision && isOwnershipChange && !IsServerAuthoritative() && Interpolate)
+ {
+ m_HalfFloatTargetTickOwnership = m_CachedNetworkManager.ServerTime.Tick + 3;
+ }
+ else
+ {
+ // Teleport to current position
+ SetStateInternal(currentPosition, currentRotation, transform.localScale, true);
+ }
}
else
{
-
- // Assure we no longer subscribe to the tick event
- NetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick;
+ // Remove this instance from the tick update
+ DeregisterForTickUpdate(this);
ResetInterpolatedStateToCurrentAuthoritativeState();
-
+ m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat = false;
m_CurrentPosition = currentPosition;
m_TargetPosition = currentPosition;
m_CurrentScale = transform.localScale;
@@ -2640,6 +2834,14 @@ protected void Initialize()
}
}
+ ///
+ /// Initializes NetworkTransform when spawned and ownership changes.
+ ///
+ protected void Initialize()
+ {
+ InternalInitialization();
+ }
+
///
///
/// When a parent changes, non-authoritative instances should:
@@ -2653,16 +2855,23 @@ public override void OnNetworkObjectParentChanged(NetworkObject parentNetworkObj
// Only if we are not authority
if (!CanCommitToTransform)
{
- m_CurrentPosition = GetSpaceRelativePosition();
+ m_TargetPosition = m_CurrentPosition = GetSpaceRelativePosition();
m_CurrentRotation = GetSpaceRelativeRotation();
- m_CurrentScale = GetScale();
- m_ScaleInterpolator.Clear();
- m_PositionInterpolator.Clear();
- m_RotationInterpolator.Clear();
- var tempTime = new NetworkTime(NetworkManager.NetworkConfig.TickRate, NetworkManager.ServerTime.Tick).Time;
- UpdatePositionInterpolator(m_CurrentPosition, tempTime, true);
- m_ScaleInterpolator.ResetTo(m_CurrentScale, tempTime);
- m_RotationInterpolator.ResetTo(m_CurrentRotation, tempTime);
+ m_TargetRotation = m_CurrentRotation.eulerAngles;
+ m_TargetScale = m_CurrentScale = GetScale();
+
+ if (Interpolate)
+ {
+ m_ScaleInterpolator.Clear();
+ m_PositionInterpolator.Clear();
+ m_RotationInterpolator.Clear();
+
+ // Always use NetworkManager here as this can be invoked prior to spawning
+ var tempTime = new NetworkTime(NetworkManager.NetworkConfig.TickRate, NetworkManager.ServerTime.Tick).Time;
+ UpdatePositionInterpolator(m_CurrentPosition, tempTime, true);
+ m_ScaleInterpolator.ResetTo(m_CurrentScale, tempTime);
+ m_RotationInterpolator.ResetTo(m_CurrentRotation, tempTime);
+ }
}
base.OnNetworkObjectParentChanged(parentNetworkObject);
}
@@ -2773,24 +2982,14 @@ private void SetStateServerRpc(Vector3 pos, Quaternion rot, Vector3 scale, bool
SetStateInternal(pos, rot, scale, shouldTeleport);
}
- ///
- ///
- /// If you override this method, be sure that:
- /// - Non-authority always invokes this base class method.
- ///
- protected virtual void Update()
- {
- // If not spawned or this instance has authority, exit early
- if (!IsSpawned || CanCommitToTransform)
- {
- return;
- }
+ private void UpdateInterpolation()
+ {
// Non-Authority
if (Interpolate)
{
- var serverTime = NetworkManager.ServerTime;
- var cachedDeltaTime = NetworkManager.RealTimeProvider.DeltaTime;
+ var serverTime = m_CachedNetworkManager.ServerTime;
+ var cachedDeltaTime = m_CachedNetworkManager.RealTimeProvider.DeltaTime;
var cachedServerTime = serverTime.Time;
// With owner authoritative mode, non-authority clients can lag behind
@@ -2819,6 +3018,23 @@ protected virtual void Update()
m_ScaleInterpolator.Update(cachedDeltaTime, cachedRenderTime, cachedServerTime);
}
}
+ }
+
+ ///
+ ///
+ /// If you override this method, be sure that:
+ /// - Non-authority always invokes this base class method.
+ ///
+ protected virtual void Update()
+ {
+ // If not spawned or this instance has authority, exit early
+ if (!IsSpawned || CanCommitToTransform)
+ {
+ return;
+ }
+
+ // Non-Authority
+ UpdateInterpolation();
// Apply the current authoritative state
ApplyAuthoritativeState();
@@ -2870,24 +3086,35 @@ public bool IsServerAuthoritative()
/// serialzied
private void TransformStateUpdate(ulong senderId, FastBufferReader messagePayload)
{
- if (!OnIsServerAuthoritative() && IsServer && OwnerClientId == NetworkManager.ServerClientId)
+ var ownerAuthoritativeServerSide = !OnIsServerAuthoritative() && IsServer;
+ if (ownerAuthoritativeServerSide && OwnerClientId == NetworkManager.ServerClientId)
{
// Ownership must have changed, ignore any additional pending messages that might have
// come from a previous owner client.
return;
}
- // Forward owner authoritative messages before doing anything else
- if (IsServer && !OnIsServerAuthoritative())
- {
- ForwardStateUpdateMessage(messagePayload);
- }
// Store the previous/old state
m_OldState = m_LocalAuthoritativeNetworkState;
- // Deserialize the message
+ // Save the current payload stream position
+ var currentPosition = messagePayload.Position;
+
+ // Deserialize the message (and determine network delivery)
messagePayload.ReadNetworkSerializableInPlace(ref m_LocalAuthoritativeNetworkState);
+ // Rewind back prior to serialization
+ messagePayload.Seek(currentPosition);
+
+ // Get the network delivery method used to send this state update
+ var networkDelivery = m_LocalAuthoritativeNetworkState.ReliableFragmentedSequenced ? NetworkDelivery.ReliableFragmentedSequenced : NetworkDelivery.UnreliableSequenced;
+
+ // Forward owner authoritative messages before doing anything else
+ if (ownerAuthoritativeServerSide)
+ {
+ ForwardStateUpdateMessage(messagePayload, networkDelivery);
+ }
+
// Apply the message
OnNetworkStateChanged(m_OldState, m_LocalAuthoritativeNetworkState);
}
@@ -2896,7 +3123,7 @@ private void TransformStateUpdate(ulong senderId, FastBufferReader messagePayloa
/// Forwards owner authoritative state updates when received by the server
///
/// the owner state message payload
- private unsafe void ForwardStateUpdateMessage(FastBufferReader messagePayload)
+ private unsafe void ForwardStateUpdateMessage(FastBufferReader messagePayload, NetworkDelivery networkDelivery)
{
var serverAuthoritative = OnIsServerAuthoritative();
var currentPosition = messagePayload.Position;
@@ -2906,15 +3133,15 @@ private unsafe void ForwardStateUpdateMessage(FastBufferReader messagePayload)
{
writer.WriteBytesSafe(messagePayload.GetUnsafePtr(), messageSize, currentPosition);
- var clientCount = NetworkManager.ConnectionManager.ConnectedClientsList.Count;
+ var clientCount = m_CachedNetworkManager.ConnectionManager.ConnectedClientsList.Count;
for (int i = 0; i < clientCount; i++)
{
- var clientId = NetworkManager.ConnectionManager.ConnectedClientsList[i].ClientId;
+ var clientId = m_CachedNetworkManager.ConnectionManager.ConnectedClientsList[i].ClientId;
if (NetworkManager.ServerClientId == clientId || (!serverAuthoritative && clientId == OwnerClientId))
{
continue;
}
- NetworkManager.CustomMessagingManager.SendNamedMessage(m_MessageName, clientId, writer);
+ m_CachedNetworkManager.CustomMessagingManager.SendNamedMessage(m_MessageName, clientId, writer, networkDelivery);
}
}
messagePayload.Seek(currentPosition);
@@ -2925,7 +3152,7 @@ private unsafe void ForwardStateUpdateMessage(FastBufferReader messagePayload)
///
private void UpdateTransformState()
{
- if (NetworkManager.ShutdownInProgress)
+ if (m_CachedNetworkManager.ShutdownInProgress)
{
return;
}
@@ -2939,31 +3166,141 @@ private void UpdateTransformState()
{
Debug.LogError($"Owner authoritative {nameof(NetworkTransform)} can only be updated by the owner!");
}
- var customMessageManager = NetworkManager.CustomMessagingManager;
+ var customMessageManager = m_CachedNetworkManager.CustomMessagingManager;
var writer = new FastBufferWriter(128, Allocator.Temp);
+ // Determine what network delivery method to use:
+ // When to send reliable packets:
+ // - If UsUnrealiable is not enabled
+ // - If teleporting or synchronizing
+ // - If sending an UnrealiableFrameSync or synchronizing the base position of the NetworkDeltaPosition
+ var networkDelivery = !UseUnreliableDeltas | m_LocalAuthoritativeNetworkState.IsTeleportingNextFrame | m_LocalAuthoritativeNetworkState.IsSynchronizing
+ | m_LocalAuthoritativeNetworkState.UnreliableFrameSync | m_LocalAuthoritativeNetworkState.SynchronizeBaseHalfFloat
+ ? NetworkDelivery.ReliableFragmentedSequenced : NetworkDelivery.UnreliableSequenced;
+
using (writer)
{
writer.WriteNetworkSerializable(m_LocalAuthoritativeNetworkState);
// Server-host always sends updates to all clients (but itself)
if (IsServer)
{
- var clientCount = NetworkManager.ConnectionManager.ConnectedClientsList.Count;
+ var clientCount = m_CachedNetworkManager.ConnectionManager.ConnectedClientsList.Count;
for (int i = 0; i < clientCount; i++)
{
- var clientId = NetworkManager.ConnectionManager.ConnectedClientsList[i].ClientId;
+ var clientId = m_CachedNetworkManager.ConnectionManager.ConnectedClientsList[i].ClientId;
if (NetworkManager.ServerClientId == clientId)
{
continue;
}
- customMessageManager.SendNamedMessage(m_MessageName, clientId, writer);
+ customMessageManager.SendNamedMessage(m_MessageName, clientId, writer, networkDelivery);
}
}
else
{
// Clients (owner authoritative) send messages to the server-host
- customMessageManager.SendNamedMessage(m_MessageName, NetworkManager.ServerClientId, writer);
+ customMessageManager.SendNamedMessage(m_MessageName, NetworkManager.ServerClientId, writer, networkDelivery);
+ }
+ }
+ }
+
+
+ private static Dictionary s_NetworkTickRegistration = new Dictionary();
+
+ private static void RemoveTickUpdate(NetworkManager networkManager)
+ {
+ s_NetworkTickRegistration.Remove(networkManager);
+ }
+
+ ///
+ /// Having the tick update once and cycling through registered instances to update is evidently less processor
+ /// intensive than having each instance subscribe and update individually.
+ ///
+ private class NetworkTransformTickRegistration
+ {
+ private Action m_NetworkTickUpdate;
+ private NetworkManager m_NetworkManager;
+ public HashSet NetworkTransforms = new HashSet();
+ private void OnNetworkManagerStopped(bool value)
+ {
+ Remove();
+ }
+
+ public void Remove()
+ {
+ m_NetworkManager.NetworkTickSystem.Tick -= m_NetworkTickUpdate;
+ m_NetworkTickUpdate = null;
+ NetworkTransforms.Clear();
+ RemoveTickUpdate(m_NetworkManager);
+ }
+
+ ///
+ /// Invoked once per network tick, this will update any registered
+ /// authority instances.
+ ///
+ private void TickUpdate()
+ {
+ foreach (var networkTransform in NetworkTransforms)
+ {
+ networkTransform.NetworkTickSystem_Tick();
+ }
+ }
+
+ public NetworkTransformTickRegistration(NetworkManager networkManager)
+ {
+ m_NetworkManager = networkManager;
+ m_NetworkTickUpdate = new Action(TickUpdate);
+ networkManager.NetworkTickSystem.Tick += m_NetworkTickUpdate;
+ if (networkManager.IsServer)
+ {
+ networkManager.OnServerStopped += OnNetworkManagerStopped;
+ }
+ else
+ {
+ networkManager.OnClientStopped += OnNetworkManagerStopped;
+ }
+ }
+ }
+ private static int s_TickSynchPosition;
+ private int m_TickSync;
+
+ internal void RegisterForTickSynchronization()
+ {
+ s_TickSynchPosition++;
+ s_TickSynchPosition = s_TickSynchPosition % (int)NetworkManager.NetworkConfig.TickRate;
+ m_TickSync = s_TickSynchPosition;
+ }
+
+ ///
+ /// Will register the NetworkTransform instance for the single tick update entry point.
+ /// If a NetworkTransformTickRegistration has not yet been registered for the NetworkManager
+ /// instance, then create an entry.
+ ///
+ ///
+ private static void RegisterForTickUpdate(NetworkTransform networkTransform)
+ {
+ if (!s_NetworkTickRegistration.ContainsKey(networkTransform.NetworkManager))
+ {
+ s_NetworkTickRegistration.Add(networkTransform.NetworkManager, new NetworkTransformTickRegistration(networkTransform.NetworkManager));
+ }
+ networkTransform.RegisterForTickSynchronization();
+ s_NetworkTickRegistration[networkTransform.NetworkManager].NetworkTransforms.Add(networkTransform);
+ }
+
+ ///
+ /// If a NetworkTransformTickRegistration exists for the NetworkManager instance, then this will
+ /// remove the NetworkTransform instance from the single tick update entry point.
+ ///
+ ///
+ private static void DeregisterForTickUpdate(NetworkTransform networkTransform)
+ {
+ if (s_NetworkTickRegistration.ContainsKey(networkTransform.NetworkManager))
+ {
+ s_NetworkTickRegistration[networkTransform.NetworkManager].NetworkTransforms.Remove(networkTransform);
+ if (s_NetworkTickRegistration[networkTransform.NetworkManager].NetworkTransforms.Count == 0)
+ {
+ var registrationEntry = s_NetworkTickRegistration[networkTransform.NetworkManager];
+ registrationEntry.Remove();
}
}
}
diff --git a/com.unity.netcode.gameobjects/Editor/NetworkTransformEditor.cs b/com.unity.netcode.gameobjects/Editor/NetworkTransformEditor.cs
index 6a5be7845b..4e7831d5d8 100644
--- a/com.unity.netcode.gameobjects/Editor/NetworkTransformEditor.cs
+++ b/com.unity.netcode.gameobjects/Editor/NetworkTransformEditor.cs
@@ -10,6 +10,7 @@ namespace Unity.Netcode.Editor
[CustomEditor(typeof(NetworkTransform), true)]
public class NetworkTransformEditor : UnityEditor.Editor
{
+ private SerializedProperty m_UseUnreliableDeltas;
private SerializedProperty m_SyncPositionXProperty;
private SerializedProperty m_SyncPositionYProperty;
private SerializedProperty m_SyncPositionZProperty;
@@ -39,6 +40,7 @@ public class NetworkTransformEditor : UnityEditor.Editor
///
public void OnEnable()
{
+ m_UseUnreliableDeltas = serializedObject.FindProperty(nameof(NetworkTransform.UseUnreliableDeltas));
m_SyncPositionXProperty = serializedObject.FindProperty(nameof(NetworkTransform.SyncPositionX));
m_SyncPositionYProperty = serializedObject.FindProperty(nameof(NetworkTransform.SyncPositionY));
m_SyncPositionZProperty = serializedObject.FindProperty(nameof(NetworkTransform.SyncPositionZ));
@@ -129,7 +131,9 @@ public override void OnInspectorGUI()
EditorGUILayout.PropertyField(m_PositionThresholdProperty);
EditorGUILayout.PropertyField(m_RotAngleThresholdProperty);
EditorGUILayout.PropertyField(m_ScaleThresholdProperty);
-
+ EditorGUILayout.Space();
+ EditorGUILayout.LabelField("Delivery", EditorStyles.boldLabel);
+ EditorGUILayout.PropertyField(m_UseUnreliableDeltas);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Configurations", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(m_InLocalSpaceProperty);
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformPacketLossTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformPacketLossTests.cs
new file mode 100644
index 0000000000..6c422d6a19
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformPacketLossTests.cs
@@ -0,0 +1,965 @@
+using System.Collections;
+using System.Text;
+using NUnit.Framework;
+using Unity.Netcode.TestHelpers.Runtime;
+using UnityEngine;
+using UnityEngine.TestTools;
+
+namespace Unity.Netcode.RuntimeTests
+{
+ ///
+ /// Integration tests for NetworkTransform that will test both
+ /// server and host operating modes and will test both authoritative
+ /// models for each operating mode.
+ ///
+ [TestFixture(HostOrServer.Host, Authority.ServerAuthority)]
+ [TestFixture(HostOrServer.Host, Authority.OwnerAuthority)]
+
+ public class NetworkTransformPacketLossTests : IntegrationTestWithApproximation
+ {
+ private NetworkObject m_AuthoritativePlayer;
+ private NetworkObject m_NonAuthoritativePlayer;
+ private NetworkObject m_ChildObject;
+ private NetworkObject m_SubChildObject;
+ private NetworkObject m_ParentObject;
+
+ private NetworkTransformTestComponent m_AuthoritativeTransform;
+ private NetworkTransformTestComponent m_NonAuthoritativeTransform;
+ private NetworkTransformTestComponent m_OwnerTransform;
+
+ private readonly Authority m_Authority;
+
+ public enum Authority
+ {
+ ServerAuthority,
+ OwnerAuthority
+ }
+
+ public enum Interpolation
+ {
+ DisableInterpolate,
+ EnableInterpolate
+ }
+
+ public enum Precision
+ {
+ Half,
+ Full
+ }
+
+ public enum Rotation
+ {
+ Euler,
+ Quaternion
+ }
+
+ public enum RotationCompression
+ {
+ None,
+ QuaternionCompress
+ }
+
+
+ public enum TransformSpace
+ {
+ World,
+ Local
+ }
+
+ public enum OverrideState
+ {
+ Update,
+ CommitToTransform,
+ SetState
+ }
+
+ public enum Axis
+ {
+ X,
+ Y,
+ Z,
+ XY,
+ XZ,
+ YZ,
+ XYZ
+ }
+
+ public enum NetworkConditions
+ {
+ PacketLoss,
+ LatencyAndPacketLoss
+ }
+
+ ///
+ /// Constructor
+ ///
+ /// Determines if we are running as a server or host
+ /// Determines if we are using server or owner authority
+ public NetworkTransformPacketLossTests(HostOrServer testWithHost, Authority authority)
+ {
+ m_UseHost = testWithHost == HostOrServer.Host ? true : false;
+ m_Authority = authority;
+ }
+
+ protected override int NumberOfClients => 1;
+ protected override bool m_SetupIsACoroutine => false;
+ protected override bool m_TearDownIsACoroutine => false;
+
+ private const int k_TickRate = 60;
+ private int m_OriginalTargetFrameRate;
+ protected override void OnOneTimeSetup()
+ {
+ m_OriginalTargetFrameRate = Application.targetFrameRate;
+ Application.targetFrameRate = 120;
+ base.OnOneTimeSetup();
+ }
+
+ protected override void OnOneTimeTearDown()
+ {
+ Application.targetFrameRate = m_OriginalTargetFrameRate;
+ base.OnOneTimeTearDown();
+ }
+
+ protected override void OnInlineSetup()
+ {
+ NetworkTransformTestComponent.AuthorityInstance = null;
+ m_Precision = Precision.Full;
+ ChildObjectComponent.Reset();
+ }
+
+ protected override void OnInlineTearDown()
+ {
+ m_EnableVerboseDebug = false;
+ Object.DestroyImmediate(m_PlayerPrefab);
+ }
+
+ protected override void OnCreatePlayerPrefab()
+ {
+ var networkTransformTestComponent = m_PlayerPrefab.AddComponent();
+ networkTransformTestComponent.ServerAuthority = m_Authority == Authority.ServerAuthority;
+ }
+
+ private const int k_Latency = 100;
+ private const int k_PacketLoss = 5;
+ protected override void OnServerAndClientsCreated()
+ {
+ var subChildObject = CreateNetworkObjectPrefab("SubChildObject");
+ var subChildNetworkTransform = subChildObject.AddComponent();
+ subChildNetworkTransform.ServerAuthority = m_Authority == Authority.ServerAuthority;
+ m_SubChildObject = subChildObject.GetComponent();
+
+ var childObject = CreateNetworkObjectPrefab("ChildObject");
+ var childNetworkTransform = childObject.AddComponent();
+ childNetworkTransform.ServerAuthority = m_Authority == Authority.ServerAuthority;
+ m_ChildObject = childObject.GetComponent();
+
+ var parentObject = CreateNetworkObjectPrefab("ParentObject");
+ var parentNetworkTransform = parentObject.AddComponent();
+ parentNetworkTransform.ServerAuthority = m_Authority == Authority.ServerAuthority;
+ m_ParentObject = parentObject.GetComponent();
+
+ // Now apply local transform values
+ m_ChildObject.transform.position = m_ChildObjectLocalPosition;
+ var childRotation = m_ChildObject.transform.rotation;
+ childRotation.eulerAngles = m_ChildObjectLocalRotation;
+ m_ChildObject.transform.rotation = childRotation;
+ m_ChildObject.transform.localScale = m_ChildObjectLocalScale;
+
+ m_SubChildObject.transform.position = m_SubChildObjectLocalPosition;
+ var subChildRotation = m_SubChildObject.transform.rotation;
+ subChildRotation.eulerAngles = m_SubChildObjectLocalRotation;
+ m_SubChildObject.transform.rotation = childRotation;
+ m_SubChildObject.transform.localScale = m_SubChildObjectLocalScale;
+
+ if (m_EnableVerboseDebug)
+ {
+ m_ServerNetworkManager.LogLevel = LogLevel.Developer;
+ foreach (var clientNetworkManager in m_ClientNetworkManagers)
+ {
+ clientNetworkManager.LogLevel = LogLevel.Developer;
+ }
+ }
+
+ m_ServerNetworkManager.NetworkConfig.TickRate = k_TickRate;
+
+ var unityTransport = m_ServerNetworkManager.NetworkConfig.NetworkTransport as Transports.UTP.UnityTransport;
+ unityTransport.SetDebugSimulatorParameters(k_Latency, 0, k_PacketLoss);
+ foreach (var clientNetworkManager in m_ClientNetworkManagers)
+ {
+ clientNetworkManager.NetworkConfig.TickRate = k_TickRate;
+ }
+ }
+
+ protected override IEnumerator OnServerAndClientsConnected()
+ {
+
+ // Wait for the client-side to notify it is finished initializing and spawning.
+ yield return WaitForClientsConnectedOrTimeOut();
+ AssertOnTimeout("Timed out waiting for client-side to notify it is ready!");
+
+ // Get the client player representation on both the server and the client side
+ var serverSideClientPlayer = m_PlayerNetworkObjects[0][m_ClientNetworkManagers[0].LocalClientId];// m_ServerNetworkManager.ConnectedClients[m_ClientNetworkManagers[0].LocalClientId].PlayerObject;
+ var clientSideClientPlayer = m_PlayerNetworkObjects[m_ClientNetworkManagers[0].LocalClientId][m_ClientNetworkManagers[0].LocalClientId];
+
+ m_AuthoritativePlayer = m_Authority == Authority.ServerAuthority ? serverSideClientPlayer : clientSideClientPlayer;
+ m_NonAuthoritativePlayer = m_Authority == Authority.ServerAuthority ? clientSideClientPlayer : serverSideClientPlayer;
+
+ // Get the NetworkTransformTestComponent to make sure the client side is ready before starting test
+ m_AuthoritativeTransform = m_AuthoritativePlayer.GetComponent();
+ m_NonAuthoritativeTransform = m_NonAuthoritativePlayer.GetComponent();
+
+ m_OwnerTransform = m_AuthoritativeTransform.IsOwner ? m_AuthoritativeTransform : m_NonAuthoritativeTransform;
+
+ Assert.True(m_AuthoritativeTransform.CanCommitToTransform);
+ Assert.False(m_NonAuthoritativeTransform.CanCommitToTransform);
+
+ yield return s_DefaultWaitForTick;
+ }
+
+ ///
+ /// Returns true when the server-host and all clients have
+ /// instantiated the child object to be used in
+ ///
+ ///
+ private bool AllChildObjectInstancesAreSpawned()
+ {
+ if (ChildObjectComponent.AuthorityInstance == null)
+ {
+ return false;
+ }
+
+ if (ChildObjectComponent.HasSubChild && ChildObjectComponent.AuthoritySubInstance == null)
+ {
+ return false;
+ }
+
+ foreach (var clientNetworkManager in m_ClientNetworkManagers)
+ {
+ if (!ChildObjectComponent.ClientInstances.ContainsKey(clientNetworkManager.LocalClientId))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private bool AllChildObjectInstancesHaveChild()
+ {
+ foreach (var instance in ChildObjectComponent.ClientInstances.Values)
+ {
+ if (instance.transform.parent == null)
+ {
+ return false;
+ }
+ }
+ if (ChildObjectComponent.HasSubChild)
+ {
+ foreach (var instance in ChildObjectComponent.ClientSubChildInstances.Values)
+ {
+ if (instance.transform.parent == null)
+ {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ // To test that local position, rotation, and scale remain the same when parented.
+ private Vector3 m_ChildObjectLocalPosition = new Vector3(5.0f, 0.0f, -5.0f);
+ private Vector3 m_ChildObjectLocalRotation = new Vector3(-35.0f, 90.0f, 270.0f);
+ private Vector3 m_ChildObjectLocalScale = new Vector3(0.1f, 0.5f, 0.4f);
+ private Vector3 m_SubChildObjectLocalPosition = new Vector3(2.0f, 1.0f, -1.0f);
+ private Vector3 m_SubChildObjectLocalRotation = new Vector3(5.0f, 15.0f, 124.0f);
+ private Vector3 m_SubChildObjectLocalScale = new Vector3(1.0f, 0.15f, 0.75f);
+
+
+ ///
+ /// A wait condition specific method that assures the local space coordinates
+ /// are not impacted by NetworkTransform when parented.
+ ///
+ private bool AllInstancesKeptLocalTransformValues()
+ {
+ var authorityObjectLocalPosition = m_AuthorityChildObject.transform.localPosition;
+ var authorityObjectLocalRotation = m_AuthorityChildObject.transform.localRotation.eulerAngles;
+ var authorityObjectLocalScale = m_AuthorityChildObject.transform.localScale;
+
+ foreach (var childInstance in ChildObjectComponent.Instances)
+ {
+ var childLocalPosition = childInstance.transform.localPosition;
+ var childLocalRotation = childInstance.transform.localRotation.eulerAngles;
+ var childLocalScale = childInstance.transform.localScale;
+ // Adjust approximation based on precision
+ if (m_Precision == Precision.Half)
+ {
+ m_CurrentHalfPrecision = k_HalfPrecisionPosScale;
+ }
+ if (!Approximately(childLocalPosition, authorityObjectLocalPosition))
+ {
+ return false;
+ }
+ if (!Approximately(childLocalScale, authorityObjectLocalScale))
+ {
+ return false;
+ }
+ // Adjust approximation based on precision
+ if (m_Precision == Precision.Half || m_RotationCompression == RotationCompression.QuaternionCompress)
+ {
+ m_CurrentHalfPrecision = k_HalfPrecisionRot;
+ }
+ if (!ApproximatelyEuler(childLocalRotation, authorityObjectLocalRotation))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private enum ChildrenTransformCheckType
+ {
+ Connected_Clients,
+ Late_Join_Client
+ }
+
+ ///
+ /// Handles validating the local space values match the original local space values.
+ /// If not, it generates a message containing the axial values that did not match
+ /// the target/start local space values.
+ ///
+ private IEnumerator AllChildrenLocalTransformValuesMatch(bool useSubChild, ChildrenTransformCheckType checkType)
+ {
+ yield return WaitForConditionOrTimeOut(AllInstancesKeptLocalTransformValues);
+
+ var infoMessage = new StringBuilder($"[{checkType}][{useSubChild}] Timed out waiting for all children to have the correct local space values:\n");
+ var authorityObjectLocalPosition = useSubChild ? m_AuthoritySubChildObject.transform.localPosition : m_AuthorityChildObject.transform.localPosition;
+ var authorityObjectLocalRotation = useSubChild ? m_AuthoritySubChildObject.transform.localRotation.eulerAngles : m_AuthorityChildObject.transform.localRotation.eulerAngles;
+ var authorityObjectLocalScale = useSubChild ? m_AuthoritySubChildObject.transform.localScale : m_AuthorityChildObject.transform.localScale;
+ var success = !s_GlobalTimeoutHelper.TimedOut;
+ if (s_GlobalTimeoutHelper.TimedOut)
+ {
+ // If we timed out, then wait for a full range of ticks (plus 1) to assure it sent synchronization data.
+ for (int j = 0; j < m_ServerNetworkManager.NetworkConfig.TickRate; j++)
+ {
+ var instances = useSubChild ? ChildObjectComponent.SubInstances : ChildObjectComponent.Instances;
+ foreach (var childInstance in ChildObjectComponent.Instances)
+ {
+ var childLocalPosition = childInstance.transform.localPosition;
+ var childLocalRotation = childInstance.transform.localRotation.eulerAngles;
+ var childLocalScale = childInstance.transform.localScale;
+ // Adjust approximation based on precision
+ if (m_Precision == Precision.Half || m_RotationCompression == RotationCompression.QuaternionCompress)
+ {
+ m_CurrentHalfPrecision = k_HalfPrecisionPosScale;
+ }
+ if (!Approximately(childLocalPosition, authorityObjectLocalPosition))
+ {
+ infoMessage.AppendLine($"[{childInstance.name}] Child's Local Position ({childLocalPosition}) | Authority Local Position ({authorityObjectLocalPosition})");
+ success = false;
+ }
+ if (!Approximately(childLocalScale, authorityObjectLocalScale))
+ {
+ infoMessage.AppendLine($"[{childInstance.name}] Child's Local Scale ({childLocalScale}) | Authority Local Scale ({authorityObjectLocalScale})");
+ success = false;
+ }
+
+ // Adjust approximation based on precision
+ if (m_Precision == Precision.Half || m_RotationCompression == RotationCompression.QuaternionCompress)
+ {
+ m_CurrentHalfPrecision = k_HalfPrecisionRot;
+ }
+ if (!ApproximatelyEuler(childLocalRotation, authorityObjectLocalRotation))
+ {
+ infoMessage.AppendLine($"[{childInstance.name}] Child's Local Rotation ({childLocalRotation}) | Authority Local Rotation ({authorityObjectLocalRotation})");
+ success = false;
+ }
+ }
+ yield return s_DefaultWaitForTick;
+ }
+
+ if (!success)
+ {
+ Assert.True(success, infoMessage.ToString());
+ }
+ }
+ }
+
+ private NetworkObject m_AuthorityParentObject;
+ private NetworkTransformTestComponent m_AuthorityParentNetworkTransform;
+ private NetworkObject m_AuthorityChildObject;
+ private NetworkObject m_AuthoritySubChildObject;
+ private ChildObjectComponent m_AuthorityChildNetworkTransform;
+
+ private ChildObjectComponent m_AuthoritySubChildNetworkTransform;
+
+ ///
+ /// Validates that transform values remain the same when a NetworkTransform is
+ /// parented under another NetworkTransform under all of the possible axial conditions
+ /// as well as when the parent has a varying scale.
+ ///
+ [UnityTest]
+ public IEnumerator ParentedNetworkTransformTest([Values] Precision precision, [Values] Rotation rotation,
+ [Values] RotationCompression rotationCompression, [Values] Interpolation interpolation, [Values] bool worldPositionStays,
+ [Values(0.5f, 1.0f, 5.0f)] float scale)
+ {
+ // Set the precision being used for threshold adjustments
+ m_Precision = precision;
+ m_RotationCompression = rotationCompression;
+
+ // Get the NetworkManager that will have authority in order to spawn with the correct authority
+ var isServerAuthority = m_Authority == Authority.ServerAuthority;
+ var authorityNetworkManager = m_ServerNetworkManager;
+ if (!isServerAuthority)
+ {
+ authorityNetworkManager = m_ClientNetworkManagers[0];
+ }
+
+ // Spawn a parent and children
+ ChildObjectComponent.HasSubChild = true;
+ var serverSideParent = SpawnObject(m_ParentObject.gameObject, authorityNetworkManager).GetComponent();
+ var serverSideChild = SpawnObject(m_ChildObject.gameObject, authorityNetworkManager).GetComponent();
+ var serverSideSubChild = SpawnObject(m_SubChildObject.gameObject, authorityNetworkManager).GetComponent();
+
+ // Assure all of the child object instances are spawned before proceeding to parenting
+ yield return WaitForConditionOrTimeOut(AllChildObjectInstancesAreSpawned);
+ AssertOnTimeout("Timed out waiting for all child instances to be spawned!");
+
+ // Get the authority parent and child instances
+ m_AuthorityParentObject = NetworkTransformTestComponent.AuthorityInstance.NetworkObject;
+ m_AuthorityChildObject = ChildObjectComponent.AuthorityInstance.NetworkObject;
+ m_AuthoritySubChildObject = ChildObjectComponent.AuthoritySubInstance.NetworkObject;
+
+ // The child NetworkTransform will use world space when world position stays and
+ // local space when world position does not stay when parenting.
+ ChildObjectComponent.AuthorityInstance.InLocalSpace = !worldPositionStays;
+ ChildObjectComponent.AuthorityInstance.UseHalfFloatPrecision = precision == Precision.Half;
+ ChildObjectComponent.AuthorityInstance.UseQuaternionSynchronization = rotation == Rotation.Quaternion;
+ ChildObjectComponent.AuthorityInstance.UseQuaternionCompression = rotationCompression == RotationCompression.QuaternionCompress;
+
+ ChildObjectComponent.AuthoritySubInstance.InLocalSpace = !worldPositionStays;
+ ChildObjectComponent.AuthoritySubInstance.UseHalfFloatPrecision = precision == Precision.Half;
+ ChildObjectComponent.AuthoritySubInstance.UseQuaternionSynchronization = rotation == Rotation.Quaternion;
+ ChildObjectComponent.AuthoritySubInstance.UseQuaternionCompression = rotationCompression == RotationCompression.QuaternionCompress;
+
+ // Set whether we are interpolating or not
+ m_AuthorityParentNetworkTransform = m_AuthorityParentObject.GetComponent();
+ m_AuthorityParentNetworkTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
+ m_AuthorityChildNetworkTransform = m_AuthorityChildObject.GetComponent();
+ m_AuthorityChildNetworkTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
+ m_AuthoritySubChildNetworkTransform = m_AuthoritySubChildObject.GetComponent();
+ m_AuthoritySubChildNetworkTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
+
+
+ // Apply a scale to the parent object to make sure the scale on the child is properly updated on
+ // non-authority instances.
+ var halfScale = scale * 0.5f;
+ m_AuthorityParentObject.transform.localScale = GetRandomVector3(scale - halfScale, scale + halfScale);
+ m_AuthorityChildObject.transform.localScale = GetRandomVector3(scale - halfScale, scale + halfScale);
+ m_AuthoritySubChildObject.transform.localScale = GetRandomVector3(scale - halfScale, scale + halfScale);
+
+ // Allow one tick for authority to update these changes
+
+ yield return WaitForConditionOrTimeOut(PositionRotationScaleMatches);
+
+ AssertOnTimeout("All transform values did not match prior to parenting!");
+
+ // Parent the child under the parent with the current world position stays setting
+ Assert.True(serverSideChild.TrySetParent(serverSideParent.transform, worldPositionStays), "[Server-Side Child] Failed to set child's parent!");
+
+ // Parent the sub-child under the child with the current world position stays setting
+ Assert.True(serverSideSubChild.TrySetParent(serverSideChild.transform, worldPositionStays), "[Server-Side SubChild] Failed to set sub-child's parent!");
+
+ // This waits for all child instances to be parented
+ yield return WaitForConditionOrTimeOut(AllChildObjectInstancesHaveChild);
+ AssertOnTimeout("Timed out waiting for all instances to have parented a child!");
+ var latencyWait = new WaitForSeconds(k_Latency * 0.001f);
+ // Wait for at least the designated latency period
+ yield return latencyWait;
+
+ // This validates each child instance has preserved their local space values
+ yield return AllChildrenLocalTransformValuesMatch(false, ChildrenTransformCheckType.Connected_Clients);
+
+ // This validates each sub-child instance has preserved their local space values
+ yield return AllChildrenLocalTransformValuesMatch(true, ChildrenTransformCheckType.Connected_Clients);
+
+ // Verify that a late joining client will synchronize to the parented NetworkObjects properly
+ yield return CreateAndStartNewClient();
+
+ // Assure all of the child object instances are spawned (basically for the newly connected client)
+ yield return WaitForConditionOrTimeOut(AllChildObjectInstancesAreSpawned);
+ AssertOnTimeout("Timed out waiting for all child instances to be spawned!");
+
+ // This waits for all child instances to be parented
+ yield return WaitForConditionOrTimeOut(AllChildObjectInstancesHaveChild);
+ AssertOnTimeout("Timed out waiting for all instances to have parented a child!");
+
+ // Wait for at least the designated latency period
+ yield return latencyWait;
+
+ // This validates each child instance has preserved their local space values
+ yield return AllChildrenLocalTransformValuesMatch(false, ChildrenTransformCheckType.Late_Join_Client);
+
+ // This validates each sub-child instance has preserved their local space values
+ yield return AllChildrenLocalTransformValuesMatch(true, ChildrenTransformCheckType.Late_Join_Client);
+ }
+
+ ///
+ /// Validates that moving, rotating, and scaling the authority side with a single
+ /// tick will properly synchronize the non-authoritative side with the same values.
+ ///
+ private void MoveRotateAndScaleAuthority(Vector3 position, Vector3 rotation, Vector3 scale, OverrideState overrideState)
+ {
+ switch (overrideState)
+ {
+ case OverrideState.SetState:
+ {
+ var authoritativeRotation = m_AuthoritativeTransform.GetSpaceRelativeRotation();
+ authoritativeRotation.eulerAngles = rotation;
+ if (m_Authority == Authority.OwnerAuthority)
+ {
+ // Under the scenario where the owner is not the server, and non-auth is the server we set the state from the server
+ // to be updated to the owner.
+ if (m_AuthoritativeTransform.IsOwner && !m_AuthoritativeTransform.IsServer && m_NonAuthoritativeTransform.IsServer)
+ {
+ m_NonAuthoritativeTransform.SetState(position, authoritativeRotation, scale);
+ }
+ else
+ {
+ m_AuthoritativeTransform.SetState(position, authoritativeRotation, scale);
+ }
+ }
+ else
+ {
+ m_AuthoritativeTransform.SetState(position, authoritativeRotation, scale);
+ }
+
+ break;
+ }
+ case OverrideState.Update:
+ default:
+ {
+ m_AuthoritativeTransform.transform.position = position;
+
+ var authoritativeRotation = m_AuthoritativeTransform.GetSpaceRelativeRotation();
+ authoritativeRotation.eulerAngles = rotation;
+ m_AuthoritativeTransform.transform.rotation = authoritativeRotation;
+ m_AuthoritativeTransform.transform.localScale = scale;
+ break;
+ }
+ }
+ }
+
+ // The number of iterations to change position, rotation, and scale for NetworkTransformMultipleChangesOverTime
+ private const int k_PositionRotationScaleIterations = 3;
+ private const int k_PositionRotationScaleIterations3Axis = 8;
+
+ protected override void OnNewClientCreated(NetworkManager networkManager)
+ {
+ networkManager.NetworkConfig.Prefabs = m_ServerNetworkManager.NetworkConfig.Prefabs;
+ networkManager.NetworkConfig.TickRate = k_TickRate;
+ base.OnNewClientCreated(networkManager);
+ }
+
+ private Precision m_Precision = Precision.Full;
+ private RotationCompression m_RotationCompression = RotationCompression.None;
+ private float m_CurrentHalfPrecision = 0.0f;
+ private const float k_HalfPrecisionPosScale = 0.041f;
+ private const float k_HalfPrecisionRot = 0.725f;
+
+ protected override float GetDeltaVarianceThreshold()
+ {
+ if (m_Precision == Precision.Half || m_RotationCompression == RotationCompression.QuaternionCompress)
+ {
+ return m_CurrentHalfPrecision;
+ }
+ return base.GetDeltaVarianceThreshold();
+ }
+
+
+ private Axis m_CurrentAxis;
+
+ private bool m_AxisExcluded;
+
+ ///
+ /// Randomly determine if an axis should be excluded.
+ /// If so, then randomly pick one of the axis to be excluded.
+ ///
+ private Vector3 RandomlyExcludeAxis(Vector3 delta)
+ {
+ if (Random.Range(0.0f, 1.0f) >= 0.5f)
+ {
+ m_AxisExcluded = true;
+ var axisToIgnore = Random.Range(0, 2);
+ switch (axisToIgnore)
+ {
+ case 0:
+ {
+ delta.x = 0;
+ break;
+ }
+ case 1:
+ {
+ delta.y = 0;
+ break;
+ }
+ case 2:
+ {
+ delta.z = 0;
+ break;
+ }
+ }
+ }
+ return delta;
+ }
+
+ ///
+ /// This validates that multiple changes can occur within the same tick or over
+ /// several ticks while still keeping non-authoritative instances synchronized.
+ ///
+ ///
+ /// When testing < 3 axis: Interpolation is disabled and only 3 delta updates are applied per unique test
+ /// When testing 3 axis: Interpolation is enabled, sometimes an axis is intentionally excluded during a
+ /// delta update, and it runs through 8 delta updates per unique test.
+ ///
+ [UnityTest]
+ public IEnumerator NetworkTransformMultipleChangesOverTime([Values] TransformSpace testLocalTransform, [Values] OverrideState overideState,
+ [Values] Precision precision, [Values] Rotation rotationSynch, [Values] Axis axis)
+ {
+ yield return s_DefaultWaitForTick;
+
+ var tickRelativeTime = new WaitForSeconds(1.0f / m_ServerNetworkManager.NetworkConfig.TickRate);
+ m_AuthoritativeTransform.InLocalSpace = testLocalTransform == TransformSpace.Local;
+ bool axisX = axis == Axis.X || axis == Axis.XY || axis == Axis.XZ || axis == Axis.XYZ;
+ bool axisY = axis == Axis.Y || axis == Axis.XY || axis == Axis.YZ || axis == Axis.XYZ;
+ bool axisZ = axis == Axis.Z || axis == Axis.XZ || axis == Axis.YZ || axis == Axis.XYZ;
+
+ var axisCount = axisX ? 1 : 0;
+ axisCount += axisY ? 1 : 0;
+ axisCount += axisZ ? 1 : 0;
+
+ m_AuthoritativeTransform.StatePushed = false;
+ // Enable interpolation when all 3 axis are selected to make sure we are synchronizing properly
+ // when interpolation is enabled.
+ m_AuthoritativeTransform.Interpolate = axisCount == 3 ? true : false;
+
+ m_CurrentAxis = axis;
+
+ // Authority dictates what is synchronized and what the precision is going to be
+ // so we only need to set this on the authoritative side.
+ m_AuthoritativeTransform.UseHalfFloatPrecision = precision == Precision.Half;
+ m_AuthoritativeTransform.UseQuaternionSynchronization = rotationSynch == Rotation.Quaternion;
+ m_Precision = precision;
+
+ m_AuthoritativeTransform.SyncPositionX = axisX;
+ m_AuthoritativeTransform.SyncPositionY = axisY;
+ m_AuthoritativeTransform.SyncPositionZ = axisZ;
+
+ if (!m_AuthoritativeTransform.UseQuaternionSynchronization)
+ {
+ m_AuthoritativeTransform.SyncRotAngleX = axisX;
+ m_AuthoritativeTransform.SyncRotAngleY = axisY;
+ m_AuthoritativeTransform.SyncRotAngleZ = axisZ;
+ }
+ else
+ {
+ // This is not required for usage (setting the value should not matter when quaternion synchronization is enabled)
+ // but is required for this test so we don't get a failure on an axis that is marked to not be synchronized when
+ // validating the authority's values on non-authority instances.
+ m_AuthoritativeTransform.SyncRotAngleX = true;
+ m_AuthoritativeTransform.SyncRotAngleY = true;
+ m_AuthoritativeTransform.SyncRotAngleZ = true;
+ }
+
+ m_AuthoritativeTransform.SyncScaleX = axisX;
+ m_AuthoritativeTransform.SyncScaleY = axisY;
+ m_AuthoritativeTransform.SyncScaleZ = axisZ;
+
+ var positionStart = GetRandomVector3(0.25f, 1.75f);
+ var rotationStart = GetRandomVector3(1f, 15f);
+ var scaleStart = GetRandomVector3(0.25f, 2.0f);
+ var position = positionStart;
+ var rotation = rotationStart;
+ var scale = scaleStart;
+ var success = false;
+
+
+ // Wait for the deltas to be pushed
+ yield return WaitForConditionOrTimeOut(() => m_AuthoritativeTransform.StatePushed);
+
+ // Just in case we drop the first few state updates
+ if (s_GlobalTimeoutHelper.TimedOut)
+ {
+ // Set the local state to not reflect the authority state's local space settings
+ // to trigger the state update (it would eventually get there, but this is an integration test)
+ var state = m_AuthoritativeTransform.LocalAuthoritativeNetworkState;
+ state.InLocalSpace = !m_AuthoritativeTransform.InLocalSpace;
+ m_AuthoritativeTransform.LocalAuthoritativeNetworkState = state;
+ // Wait for the deltas to be pushed
+ yield return WaitForConditionOrTimeOut(() => m_AuthoritativeTransform.StatePushed);
+ }
+ AssertOnTimeout("State was never pushed!");
+
+ // Allow the precision settings to propagate first as changing precision
+ // causes a teleport event to occur
+ yield return s_DefaultWaitForTick;
+ yield return s_DefaultWaitForTick;
+ yield return s_DefaultWaitForTick;
+ yield return s_DefaultWaitForTick;
+ yield return s_DefaultWaitForTick;
+ var iterations = axisCount == 3 ? k_PositionRotationScaleIterations3Axis : k_PositionRotationScaleIterations;
+
+ // Move and rotate within the same tick, validate the non-authoritative instance updates
+ // to each set of changes. Repeat several times.
+ for (int i = 0; i < iterations; i++)
+ {
+ // Always reset this per delta update pass
+ m_AxisExcluded = false;
+ var deltaPositionDelta = GetRandomVector3(-1.5f, 1.5f);
+ var deltaRotationDelta = GetRandomVector3(-3.5f, 3.5f);
+ var deltaScaleDelta = GetRandomVector3(-0.5f, 0.5f);
+
+ m_NonAuthoritativeTransform.StateUpdated = false;
+ m_AuthoritativeTransform.StatePushed = false;
+
+ // With two or more axis, excluding one of them while chaging another will validate that
+ // full precision updates are maintaining their target state value(s) to interpolate towards
+ if (axisCount == 3)
+ {
+ position += RandomlyExcludeAxis(deltaPositionDelta);
+ rotation += RandomlyExcludeAxis(deltaRotationDelta);
+ scale += RandomlyExcludeAxis(deltaScaleDelta);
+ }
+ else
+ {
+ position += deltaPositionDelta;
+ rotation += deltaRotationDelta;
+ scale += deltaScaleDelta;
+ }
+
+ // Apply delta between ticks
+ MoveRotateAndScaleAuthority(position, rotation, scale, overideState);
+
+ // Wait for the deltas to be pushed (unlike the original test, we don't wait for state to be updated as that could be dropped here)
+ yield return WaitForConditionOrTimeOut(() => m_AuthoritativeTransform.StatePushed);
+ AssertOnTimeout($"[Non-Interpolate {i}] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed})!");
+
+ // For 3 axis, we will skip validating that the non-authority interpolates to its target point at least once.
+ // This will validate that non-authoritative updates are maintaining their target state axis values if only 2
+ // of the axis are being updated to assure interpolation maintains the targeted axial value per axis.
+ // For 2 and 1 axis tests we always validate per delta update
+ if (m_AxisExcluded || axisCount < 3)
+ {
+ // Wait for deltas to synchronize on non-authoritative side
+ yield return WaitForConditionOrTimeOut(PositionRotationScaleMatches);
+ // Provide additional debug info about what failed (if it fails)
+ if (s_GlobalTimeoutHelper.TimedOut)
+ {
+ Debug.Log("[Synch Issue Start - 1]");
+ // If we timed out, then wait for a full range of ticks (plus 1) to assure it sent synchronization data.
+ for (int j = 0; j < m_ServerNetworkManager.NetworkConfig.TickRate * 2; j++)
+ {
+ success = PositionRotationScaleMatches();
+ if (success)
+ {
+ // If we matched, then something was dropped and recovered when synchronized
+ break;
+ }
+ yield return s_DefaultWaitForTick;
+ }
+
+ // Only if we still didn't match
+ if (!success)
+ {
+ m_EnableVerboseDebug = true;
+ success = PositionRotationScaleMatches();
+ m_EnableVerboseDebug = false;
+ Debug.Log("[Synch Issue END - 1]");
+ AssertOnTimeout($"[Non-Interpolate {i}] Timed out waiting for non-authority to match authority's position or rotation");
+ }
+ }
+ }
+ }
+
+ if (axisCount == 3)
+ {
+ // As a final test, wait for deltas to synchronize on non-authoritative side to assure it interpolates to th
+ yield return WaitForConditionOrTimeOut(PositionRotationScaleMatches);
+ // Provide additional debug info about what failed (if it fails)
+ if (s_GlobalTimeoutHelper.TimedOut)
+ {
+ Debug.Log("[Synch Issue Start - 2]");
+ // If we timed out, then wait for a full range of ticks (plus 1) to assure it sent synchronization data.
+ for (int j = 0; j < m_ServerNetworkManager.NetworkConfig.TickRate * 2; j++)
+ {
+ success = PositionRotationScaleMatches();
+ if (success)
+ {
+ // If we matched, then something was dropped and recovered when synchronized
+ break;
+ }
+ yield return s_DefaultWaitForTick;
+ }
+
+ // Only if we still didn't match
+ if (!success)
+ {
+ m_EnableVerboseDebug = true;
+ PositionRotationScaleMatches();
+ m_EnableVerboseDebug = false;
+ Debug.Log("[Synch Issue END - 2]");
+ AssertOnTimeout("Timed out waiting for non-authority to match authority's position or rotation");
+
+ }
+ }
+
+ }
+ }
+
+ ///
+ /// Tests changing all axial values one at a time.
+ /// These tests are performed:
+ /// - While in local space and world space
+ /// - While interpolation is enabled and disabled
+ /// - Using the TryCommitTransformToServer "override" that can be used
+ /// from a child derived or external class.
+ ///
+ [UnityTest]
+ public IEnumerator TestAuthoritativeTransformChangeOneAtATime([Values] TransformSpace testLocalTransform, [Values] Interpolation interpolation, [Values] OverrideState overideState)
+ {
+ var overrideUpdate = overideState == OverrideState.CommitToTransform;
+ m_AuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
+ m_NonAuthoritativeTransform.Interpolate = interpolation == Interpolation.EnableInterpolate;
+
+ m_AuthoritativeTransform.InLocalSpace = testLocalTransform == TransformSpace.Local;
+
+ // test position
+ var authPlayerTransform = overrideUpdate ? m_OwnerTransform.transform : m_AuthoritativeTransform.transform;
+
+ Assert.AreEqual(Vector3.zero, m_NonAuthoritativeTransform.transform.position, "server side pos should be zero at first"); // sanity check
+
+ m_AuthoritativeTransform.StatePushed = false;
+ var nextPosition = GetRandomVector3(2f, 30f);
+ m_AuthoritativeTransform.transform.position = nextPosition;
+ if (overideState != OverrideState.SetState)
+ {
+ authPlayerTransform.position = nextPosition;
+ m_OwnerTransform.CommitToTransform();
+ }
+ else
+ {
+ m_OwnerTransform.SetState(nextPosition, null, null, m_AuthoritativeTransform.Interpolate);
+ }
+
+ if (overideState != OverrideState.Update)
+ {
+ // Wait for the deltas to be pushed
+ yield return WaitForConditionOrTimeOut(() => m_AuthoritativeTransform.StatePushed);
+ AssertOnTimeout($"[Position] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed})!");
+ }
+
+ yield return WaitForConditionOrTimeOut(() => PositionsMatch());
+ AssertOnTimeout($"Timed out waiting for positions to match {m_AuthoritativeTransform.transform.position} | {m_NonAuthoritativeTransform.transform.position}");
+
+ // test rotation
+ Assert.AreEqual(Quaternion.identity, m_NonAuthoritativeTransform.transform.rotation, "wrong initial value for rotation"); // sanity check
+
+ m_AuthoritativeTransform.StatePushed = false;
+ var nextRotation = Quaternion.Euler(GetRandomVector3(5, 60)); // using euler angles instead of quaternions directly to really see issues users might encounter
+ if (overideState != OverrideState.SetState)
+ {
+ authPlayerTransform.rotation = nextRotation;
+ m_OwnerTransform.CommitToTransform();
+ }
+ else
+ {
+ m_OwnerTransform.SetState(null, nextRotation, null, m_AuthoritativeTransform.Interpolate);
+ }
+ if (overideState != OverrideState.Update)
+ {
+ // Wait for the deltas to be pushed
+ yield return WaitForConditionOrTimeOut(() => m_AuthoritativeTransform.StatePushed);
+ AssertOnTimeout($"[Rotation] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed})!");
+ }
+
+ // Make sure the values match
+ yield return WaitForConditionOrTimeOut(() => RotationsMatch());
+ AssertOnTimeout($"Timed out waiting for rotations to match");
+
+ m_AuthoritativeTransform.StatePushed = false;
+ var nextScale = GetRandomVector3(1, 6);
+ if (overrideUpdate)
+ {
+ authPlayerTransform.localScale = nextScale;
+ m_OwnerTransform.CommitToTransform();
+ }
+ else
+ {
+ m_OwnerTransform.SetState(null, null, nextScale, m_AuthoritativeTransform.Interpolate);
+ }
+ if (overideState != OverrideState.Update)
+ {
+ // Wait for the deltas to be pushed
+ yield return WaitForConditionOrTimeOut(() => m_AuthoritativeTransform.StatePushed);
+ AssertOnTimeout($"[Rotation] Timed out waiting for state to be pushed ({m_AuthoritativeTransform.StatePushed})!");
+ }
+
+ // Make sure the scale values match
+ yield return WaitForConditionOrTimeOut(() => ScaleValuesMatch());
+ AssertOnTimeout($"Timed out waiting for scale values to match");
+ }
+
+ private bool PositionRotationScaleMatches()
+ {
+ return RotationsMatch() && PositionsMatch() && ScaleValuesMatch();
+ }
+
+ private bool RotationsMatch(bool printDeltas = false)
+ {
+ m_CurrentHalfPrecision = k_HalfPrecisionRot;
+ var authorityEulerRotation = m_AuthoritativeTransform.GetSpaceRelativeRotation().eulerAngles;
+ var nonAuthorityEulerRotation = m_NonAuthoritativeTransform.GetSpaceRelativeRotation().eulerAngles;
+ var xIsEqual = ApproximatelyEuler(authorityEulerRotation.x, nonAuthorityEulerRotation.x) || !m_AuthoritativeTransform.SyncRotAngleX;
+ var yIsEqual = ApproximatelyEuler(authorityEulerRotation.y, nonAuthorityEulerRotation.y) || !m_AuthoritativeTransform.SyncRotAngleY;
+ var zIsEqual = ApproximatelyEuler(authorityEulerRotation.z, nonAuthorityEulerRotation.z) || !m_AuthoritativeTransform.SyncRotAngleZ;
+ if (!xIsEqual || !yIsEqual || !zIsEqual)
+ {
+ VerboseDebug($"[{m_AuthoritativeTransform.gameObject.name}][X-{xIsEqual} | Y-{yIsEqual} | Z-{zIsEqual}][{m_CurrentAxis}]" +
+ $"[Sync: X-{m_AuthoritativeTransform.SyncRotAngleX} | Y-{m_AuthoritativeTransform.SyncRotAngleY} | Z-{m_AuthoritativeTransform.SyncRotAngleZ}] Authority rotation {authorityEulerRotation} != [{m_NonAuthoritativeTransform.gameObject.name}] NonAuthority rotation {nonAuthorityEulerRotation}");
+ }
+ if (printDeltas)
+ {
+ Debug.Log($"[Rotation Match] Euler Delta {EulerDelta(authorityEulerRotation, nonAuthorityEulerRotation)}");
+ }
+ return xIsEqual && yIsEqual && zIsEqual;
+ }
+
+ private bool PositionsMatch(bool printDeltas = false)
+ {
+ m_CurrentHalfPrecision = k_HalfPrecisionPosScale;
+ var authorityPosition = m_AuthoritativeTransform.GetSpaceRelativePosition();
+ var nonAuthorityPosition = m_NonAuthoritativeTransform.GetSpaceRelativePosition();
+ var xIsEqual = Approximately(authorityPosition.x, nonAuthorityPosition.x) || !m_AuthoritativeTransform.SyncPositionX;
+ var yIsEqual = Approximately(authorityPosition.y, nonAuthorityPosition.y) || !m_AuthoritativeTransform.SyncPositionY;
+ var zIsEqual = Approximately(authorityPosition.z, nonAuthorityPosition.z) || !m_AuthoritativeTransform.SyncPositionZ;
+ if (!xIsEqual || !yIsEqual || !zIsEqual)
+ {
+ VerboseDebug($"[{m_AuthoritativeTransform.gameObject.name}] Authority position {authorityPosition} != [{m_NonAuthoritativeTransform.gameObject.name}] NonAuthority position {nonAuthorityPosition}");
+ }
+ return xIsEqual && yIsEqual && zIsEqual;
+ }
+
+ private bool ScaleValuesMatch(bool printDeltas = false)
+ {
+ m_CurrentHalfPrecision = k_HalfPrecisionPosScale;
+ var authorityScale = m_AuthoritativeTransform.transform.localScale;
+ var nonAuthorityScale = m_NonAuthoritativeTransform.transform.localScale;
+ var xIsEqual = Approximately(authorityScale.x, nonAuthorityScale.x) || !m_AuthoritativeTransform.SyncScaleX;
+ var yIsEqual = Approximately(authorityScale.y, nonAuthorityScale.y) || !m_AuthoritativeTransform.SyncScaleY;
+ var zIsEqual = Approximately(authorityScale.z, nonAuthorityScale.z) || !m_AuthoritativeTransform.SyncScaleZ;
+ if (!xIsEqual || !yIsEqual || !zIsEqual)
+ {
+ VerboseDebug($"[{m_AuthoritativeTransform.gameObject.name}] Authority scale {authorityScale} != [{m_NonAuthoritativeTransform.gameObject.name}] NonAuthority scale {nonAuthorityScale}");
+ }
+ return xIsEqual && yIsEqual && zIsEqual;
+ }
+ }
+}
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformPacketLossTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformPacketLossTests.cs.meta
new file mode 100644
index 0000000000..c363dc6cc9
--- /dev/null
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformPacketLossTests.cs.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: e0f584e8eb891d5459373e96e54fe821
+timeCreated: 1620872927
\ No newline at end of file
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs
index 2669c233a9..f2512ae3c2 100644
--- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformStateTests.cs
@@ -230,6 +230,11 @@ public void TestSyncAxes([Values] SynchronizationType synchronizationType, [Valu
var gameObject = new GameObject($"Test-{nameof(NetworkTransformStateTests)}.{nameof(TestSyncAxes)}");
var networkObject = gameObject.AddComponent();
var networkTransform = gameObject.AddComponent();
+
+ var manager = new GameObject($"Test-{nameof(NetworkManager)}.{nameof(TestSyncAxes)}");
+ var networkManager = manager.AddComponent();
+ networkObject.NetworkManagerOwner = networkManager;
+
networkTransform.enabled = false; // do not tick `FixedUpdate()` or `Update()`
var initialPosition = Vector3.zero;
@@ -269,6 +274,7 @@ public void TestSyncAxes([Values] SynchronizationType synchronizationType, [Valu
if (syncPosX || syncPosY || syncPosZ || syncRotX || syncRotY || syncRotZ || syncScaX || syncScaY || syncScaZ)
{
+ Assert.NotNull(networkTransform.NetworkManager, "NetworkManager is NULL!");
Assert.IsTrue(networkTransform.ApplyTransformToNetworkState(ref networkTransformState, 0, networkTransform.transform));
}
}
@@ -714,6 +720,7 @@ public void TestSyncAxes([Values] SynchronizationType synchronizationType, [Valu
}
Object.DestroyImmediate(gameObject);
+ Object.DestroyImmediate(manager);
}
diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs
index a29c9630e6..6111581e14 100644
--- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs
+++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformTests.cs
@@ -337,6 +337,10 @@ protected override void OnTimeTravelServerAndClientsConnected()
m_AuthoritativeTransform = m_AuthoritativePlayer.GetComponent();
m_NonAuthoritativeTransform = m_NonAuthoritativePlayer.GetComponent();
+ // All of these tests will validate not using unreliable network delivery
+ m_AuthoritativeTransform.UseUnreliableDeltas = false;
+ m_NonAuthoritativeTransform.UseUnreliableDeltas = false;
+
m_OwnerTransform = m_AuthoritativeTransform.IsOwner ? m_AuthoritativeTransform : m_NonAuthoritativeTransform;
// Wait for the client-side to notify it is finished initializing and spawning.
@@ -436,7 +440,7 @@ private bool AllInstancesKeptLocalTransformValues()
return false;
}
// Adjust approximation based on precision
- if (m_Precision == Precision.Half)
+ if (m_Precision == Precision.Half || m_RotationCompression == RotationCompression.QuaternionCompress)
{
m_CurrentHalfPrecision = k_HalfPrecisionRot;
}
@@ -448,15 +452,21 @@ private bool AllInstancesKeptLocalTransformValues()
return true;
}
+ private enum ChildrenTransformCheckType
+ {
+ Connected_Clients,
+ Late_Join_Client
+ }
+
///
/// Handles validating the local space values match the original local space values.
/// If not, it generates a message containing the axial values that did not match
/// the target/start local space values.
///
- private void AllChildrenLocalTransformValuesMatch(bool useSubChild)
+ private void AllChildrenLocalTransformValuesMatch(bool useSubChild, ChildrenTransformCheckType checkType)
{
- var success = WaitForConditionOrTimeOutWithTimeTravel(AllInstancesKeptLocalTransformValues);
- var infoMessage = new StringBuilder($"Timed out waiting for all children to have the correct local space values:\n");
+ var success = WaitForConditionOrTimeOutWithTimeTravel(AllInstancesKeptLocalTransformValues, k_TickRate * 2);
+ var infoMessage = new StringBuilder($"[{checkType}][{useSubChild}] Timed out waiting for all children to have the correct local space values:\n");
var authorityObjectLocalPosition = useSubChild ? m_AuthoritySubChildObject.transform.localPosition : m_AuthorityChildObject.transform.localPosition;
var authorityObjectLocalRotation = useSubChild ? m_AuthoritySubChildObject.transform.localRotation.eulerAngles : m_AuthorityChildObject.transform.localRotation.eulerAngles;
var authorityObjectLocalScale = useSubChild ? m_AuthoritySubChildObject.transform.localScale : m_AuthorityChildObject.transform.localScale;
@@ -470,7 +480,7 @@ private void AllChildrenLocalTransformValuesMatch(bool useSubChild)
var childLocalRotation = childInstance.transform.localRotation.eulerAngles;
var childLocalScale = childInstance.transform.localScale;
// Adjust approximation based on precision
- if (m_Precision == Precision.Half)
+ if (m_Precision == Precision.Half || m_RotationCompression == RotationCompression.QuaternionCompress)
{
m_CurrentHalfPrecision = k_HalfPrecisionPosScale;
}
@@ -486,7 +496,7 @@ private void AllChildrenLocalTransformValuesMatch(bool useSubChild)
}
// Adjust approximation based on precision
- if (m_Precision == Precision.Half)
+ if (m_Precision == Precision.Half || m_RotationCompression == RotationCompression.QuaternionCompress)
{
m_CurrentHalfPrecision = k_HalfPrecisionRot;
}
@@ -523,6 +533,7 @@ public void ParentedNetworkTransformTest([Values] Precision precision, [Values]
{
// Set the precision being used for threshold adjustments
m_Precision = precision;
+ m_RotationCompression = rotationCompression;
// Get the NetworkManager that will have authority in order to spawn with the correct authority
var isServerAuthority = m_Authority == Authority.ServerAuthority;
@@ -577,6 +588,9 @@ public void ParentedNetworkTransformTest([Values] Precision precision, [Values]
// Allow one tick for authority to update these changes
TimeTravelToNextTick();
+ success = WaitForConditionOrTimeOutWithTimeTravel(PositionRotationScaleMatches);
+
+ Assert.True(success, "All transform values did not match prior to parenting!");
// Parent the child under the parent with the current world position stays setting
Assert.True(serverSideChild.TrySetParent(serverSideParent.transform, worldPositionStays), "[Server-Side Child] Failed to set child's parent!");
@@ -590,10 +604,10 @@ public void ParentedNetworkTransformTest([Values] Precision precision, [Values]
TimeTravelToNextTick();
// This validates each child instance has preserved their local space values
- AllChildrenLocalTransformValuesMatch(false);
+ AllChildrenLocalTransformValuesMatch(false, ChildrenTransformCheckType.Connected_Clients);
// This validates each sub-child instance has preserved their local space values
- AllChildrenLocalTransformValuesMatch(true);
+ AllChildrenLocalTransformValuesMatch(true, ChildrenTransformCheckType.Connected_Clients);
// Verify that a late joining client will synchronize to the parented NetworkObjects properly
CreateAndStartNewClientWithTimeTravel();
@@ -607,10 +621,10 @@ public void ParentedNetworkTransformTest([Values] Precision precision, [Values]
Assert.True(success, "Timed out waiting for all instances to have parented a child!");
// This validates each child instance has preserved their local space values
- AllChildrenLocalTransformValuesMatch(false);
+ AllChildrenLocalTransformValuesMatch(false, ChildrenTransformCheckType.Late_Join_Client);
// This validates each sub-child instance has preserved their local space values
- AllChildrenLocalTransformValuesMatch(true);
+ AllChildrenLocalTransformValuesMatch(true, ChildrenTransformCheckType.Late_Join_Client);
}
///
@@ -689,13 +703,14 @@ protected override void OnNewClientCreated(NetworkManager networkManager)
}
private Precision m_Precision = Precision.Full;
+ private RotationCompression m_RotationCompression = RotationCompression.None;
private float m_CurrentHalfPrecision = 0.0f;
private const float k_HalfPrecisionPosScale = 0.041f;
private const float k_HalfPrecisionRot = 0.725f;
protected override float GetDeltaVarianceThreshold()
{
- if (m_Precision == Precision.Half)
+ if (m_Precision == Precision.Half || m_RotationCompression == RotationCompression.QuaternionCompress)
{
return m_CurrentHalfPrecision;
}