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; }