diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 065f0a09ea..219fd15124 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -13,6 +13,8 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed +- Fixed issue where the authority instance of NetworkTransform could check for state updates more than one time in a frame if the frame rate is greater than the tick frequency. (#3413) +- Fixed issue where the new interpolator types were blocking after the first consumption of a sequence of buffered state updates. (#3413) - Fixed issue where root level in-scene placed `NetworkObject`s would only allow the ownership permission to be no less than distributable or sessionowner. (#3407) ### Changed diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs index f96bb55392..f5f2413ed2 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs @@ -192,6 +192,8 @@ public void Reset(T currentValue) TimeToTargetValue = 0.0f; DeltaTime = 0.0f; m_CurrentDeltaTime = 0.0f; + MaxDeltaTime = 0.0f; + LastRemainingTime = 0.0f; } } @@ -292,13 +294,9 @@ private void TryConsumeFromBuffer(double renderTime, double minDeltaTime, double var potentialItemNeedsProcessing = false; // In the event there is nothing left in the queue (i.e. motion/change stopped), we still need to determine if the target has been reached. - if (!noStateSet && m_BufferQueue.Count == 0) + if (!noStateSet && !InterpolateState.TargetReached) { - if (!InterpolateState.TargetReached) - { - InterpolateState.TargetReached = IsApproximately(InterpolateState.CurrentValue, InterpolateState.Target.Value.Item, GetPrecision()); - } - return; + InterpolateState.TargetReached = IsApproximately(InterpolateState.CurrentValue, InterpolateState.Target.Value.Item, GetPrecision()); } // Continue to process any remaining state updates in the queue (if any) @@ -314,14 +312,10 @@ private void TryConsumeFromBuffer(double renderTime, double minDeltaTime, double if (!noStateSet) { potentialItemNeedsProcessing = ((potentialItem.TimeSent <= renderTime) && potentialItem.TimeSent > InterpolateState.Target.Value.TimeSent); - if (!InterpolateState.TargetReached) - { - InterpolateState.TargetReached = IsApproximately(InterpolateState.CurrentValue, InterpolateState.Target.Value.Item, GetPrecision()); - } } // If we haven't set a target or we have another item that needs processing. - if (noStateSet || potentialItemNeedsProcessing) + if ((noStateSet && (potentialItem.TimeSent <= renderTime)) || potentialItemNeedsProcessing) { if (m_BufferQueue.TryDequeue(out BufferedItem target)) { diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs index 10e95c6017..05eeb22cfe 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs @@ -935,6 +935,12 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade #region PROPERTIES AND GENERAL METHODS + /// + /// Used on the authority side only. + /// This is the current network tick and is set within . + /// + internal static int CurrentTick; + /// /// Pertains to Owner Authority and Interpolation
/// When enabled (default), 1 additional tick is added to the total number of ticks used to calculate the tick latency ("ticks ago") as a time. @@ -1878,6 +1884,8 @@ private void TryCommitTransform(ref Transform transformToCommit, bool synchroniz // If the state was explicitly set, then update the network tick to match the locally calculate tick if (m_LocalAuthoritativeNetworkState.ExplicitSet) { + // For explicit set, we use the current ServerTime.Tick and not CurrentTick since this is a SetState specific flow + // that is outside of the normal internal tick flow. m_LocalAuthoritativeNetworkState.NetworkTick = m_CachedNetworkManager.NetworkTickSystem.ServerTime.Tick; } @@ -2011,7 +2019,7 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, ref Tra // send a full frame synch. var isAxisSync = false; // We compare against the NetworkTickSystem version since ServerTime is set when updating ticks - if (UseUnreliableDeltas && !isSynchronization && m_DeltaSynch && m_NextTickSync <= m_CachedNetworkManager.NetworkTickSystem.ServerTime.Tick) + if (UseUnreliableDeltas && !isSynchronization && m_DeltaSynch && m_NextTickSync <= CurrentTick) { // Increment to the next frame synch tick position for this instance m_NextTickSync += (int)m_CachedNetworkManager.NetworkConfig.TickRate; @@ -2179,7 +2187,7 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, ref Tra { // If we are teleporting then we can skip the delta threshold check isPositionDirty = networkState.IsTeleportingNextFrame || isAxisSync || forceState; - if (m_HalfFloatTargetTickOwnership > m_CachedNetworkManager.ServerTime.Tick) + if (m_HalfFloatTargetTickOwnership > CurrentTick) { isPositionDirty = true; } @@ -2225,7 +2233,7 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, ref Tra networkState.NetworkDeltaPosition = m_HalfPositionState; // If ownership offset is greater or we are doing an axial synchronization then synchronize the base position - if ((m_HalfFloatTargetTickOwnership > m_CachedNetworkManager.ServerTime.Tick || isAxisSync) && !networkState.IsTeleportingNextFrame) + if ((m_HalfFloatTargetTickOwnership > CurrentTick || isAxisSync) && !networkState.IsTeleportingNextFrame) { networkState.SynchronizeBaseHalfFloat = true; } @@ -2409,7 +2417,7 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, ref Tra if (enabled) { // We use the NetworkTickSystem version since ServerTime is set when updating ticks - networkState.NetworkTick = m_CachedNetworkManager.NetworkTickSystem.ServerTime.Tick; + networkState.NetworkTick = CurrentTick; } } @@ -2440,7 +2448,7 @@ private void OnNetworkTick(bool isCalledFromParent = false) } // If we are nested and have already sent a state update this tick, then exit early (otherwise check for any changes in state) - if (IsNested && m_LocalAuthoritativeNetworkState.NetworkTick == m_CachedNetworkManager.ServerTime.Tick) + if (IsNested && m_LocalAuthoritativeNetworkState.NetworkTick == CurrentTick) { return; } @@ -4047,15 +4055,24 @@ public double GetPositionLastRemainingTime() { return m_PositionInterpolator.InterpolateState.LastRemainingTime; } -#endif - +#else + internal BufferedLinearInterpolatorVector3 GetPositionInterpolator() + { + return m_PositionInterpolator; + } + internal BufferedLinearInterpolatorQuaternion GetRotationInterpolator() + { + return m_RotationInterpolator; + } +#endif // Non-Authority private void UpdateInterpolation() { AdjustForChangeInTransformSpace(); - var timeSystem = m_CachedNetworkManager.ServerTime; + // Select the time system relative to the type of NetworkManager instance. + var timeSystem = m_CachedNetworkManager.IsServer ? m_CachedNetworkManager.ServerTime : m_CachedNetworkManager.LocalTime; var currentTime = timeSystem.Time; #if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D var cachedDeltaTime = m_UseRigidbodyForMotion ? m_CachedNetworkManager.RealTimeProvider.FixedDeltaTime : m_CachedNetworkManager.RealTimeProvider.DeltaTime; @@ -4501,6 +4518,15 @@ private void UpdateTransformState() #region NETWORK TICK REGISTRATOIN AND HANDLING private static Dictionary s_NetworkTickRegistration = new Dictionary(); + + internal static void UpdateNetworkTick(NetworkManager networkManager) + { + if (s_NetworkTickRegistration.ContainsKey(networkManager)) + { + s_NetworkTickRegistration[networkManager].TickUpdate(); + } + } + /// /// Adjusts the over-all tick offset (i.e. how many ticks ago) and how wide of a maximum delta time will be used for the /// various . @@ -4562,9 +4588,8 @@ private static void RemoveTickUpdate(NetworkManager 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 + internal class NetworkTransformTickRegistration { - private Action m_NetworkTickUpdate; private NetworkManager m_NetworkManager; public HashSet NetworkTransforms = new HashSet(); @@ -4576,8 +4601,6 @@ private void OnNetworkManagerStopped(bool value) public void Remove() { - m_NetworkManager.NetworkTickSystem.Tick -= m_NetworkTickUpdate; - m_NetworkTickUpdate = null; NetworkTransforms.Clear(); RemoveTickUpdate(m_NetworkManager); } @@ -4586,10 +4609,10 @@ public void Remove() /// Invoked once per network tick, this will update any registered /// authority instances. ///
- private void TickUpdate() + internal void TickUpdate() { // TODO FIX: The local NetworkTickSystem can invoke with the same network tick as before - if (m_NetworkManager.ServerTime.Tick <= m_LastTick) + if (CurrentTick <= m_LastTick) { return; } @@ -4600,13 +4623,11 @@ private void TickUpdate() networkTransform.OnNetworkTick(); } } - m_LastTick = m_NetworkManager.ServerTime.Tick; + m_LastTick = CurrentTick; } public NetworkTransformTickRegistration(NetworkManager networkManager) { m_NetworkManager = networkManager; - m_NetworkTickUpdate = new Action(TickUpdate); - networkManager.NetworkTickSystem.Tick += m_NetworkTickUpdate; if (networkManager.IsServer) { networkManager.OnServerStopped += OnNetworkManagerStopped; diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index f0e388d72e..2009de87bc 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -9,6 +9,7 @@ #endif using UnityEngine.SceneManagement; using Debug = UnityEngine.Debug; +using Unity.Netcode.Components; namespace Unity.Netcode { @@ -386,7 +387,19 @@ public void NetworkUpdate(NetworkUpdateStage updateStage) #endif case NetworkUpdateStage.PreUpdate: { + var currentTick = ServerTime.Tick; NetworkTimeSystem.UpdateTime(); + if (ServerTime.Tick != currentTick) + { + // If we have a lower than expected frame rate and our number of ticks that have passed since the last + // frame is greater than 1, then use the first next tick as opposed to the last tick when checking for + // changes in transform state. + // Note: This is an adjustment from using the NetworkTick event as that can be invoked more than once in + // a single frame under the above condition and since any changes to the transform are frame driven there + // is no need to check for changes to the transform more than once per frame. + NetworkTransform.CurrentTick = (ServerTime.Tick - currentTick) > 1 ? currentTick + 1 : ServerTime.Tick; + NetworkTransform.UpdateNetworkTick(this); + } AnticipationSystem.Update(); } break; diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/InterpolationStopAndStartMotionTest.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/InterpolationStopAndStartMotionTest.cs new file mode 100644 index 0000000000..99df6b2f16 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/InterpolationStopAndStartMotionTest.cs @@ -0,0 +1,235 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Unity.Netcode.Components; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + [TestFixture(HostOrServer.Host, NetworkTransform.InterpolationTypes.Lerp)] + [TestFixture(HostOrServer.Host, NetworkTransform.InterpolationTypes.SmoothDampening)] + [TestFixture(HostOrServer.DAHost, NetworkTransform.InterpolationTypes.Lerp)] + [TestFixture(HostOrServer.DAHost, NetworkTransform.InterpolationTypes.SmoothDampening)] + internal class InterpolationStopAndStartMotionTest : IntegrationTestWithApproximation + { + protected override int NumberOfClients => 2; + + private GameObject m_TestPrefab; + + private TestStartStopTransform m_AuthorityInstance; + private List m_NonAuthorityInstances = new List(); + + private NetworkTransform.InterpolationTypes m_InterpolationType; + private List m_NetworkManagers = new List(); + private NetworkManager m_AuthorityNetworkManager; + + private int m_NumberOfUpdates; + private Vector3 m_Direction; + + public InterpolationStopAndStartMotionTest(HostOrServer hostOrServer, NetworkTransform.InterpolationTypes interpolationType) : base(hostOrServer) + { + m_InterpolationType = interpolationType; + } + + protected override void OnServerAndClientsCreated() + { + m_TestPrefab = CreateNetworkObjectPrefab("TestObj"); + var testStartStopTransform = m_TestPrefab.AddComponent(); + testStartStopTransform.PositionInterpolationType = m_InterpolationType; + base.OnServerAndClientsCreated(); + } + + private bool WaitForInstancesToSpawn() + { + foreach (var networkManager in m_NetworkManagers) + { + if (networkManager == m_AuthorityNetworkManager) + { + continue; + } + + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_AuthorityInstance.NetworkObjectId)) + { + return false; + } + } + return true; + } + + private bool WaitForInstancesToFinishInterpolation() + { + m_NonAuthorityInstances.Clear(); + foreach (var networkManager in m_NetworkManagers) + { + if (networkManager == m_AuthorityNetworkManager) + { + continue; + } + + if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(m_AuthorityInstance.NetworkObjectId)) + { + return false; + } + + var nonAuthority = networkManager.SpawnManager.SpawnedObjects[m_AuthorityInstance.NetworkObjectId].GetComponent(); + + // Each non-authority instance needs to have reached their final target and reset waiting for the + // object to start moving again. + var positionInterpolator = nonAuthority.GetPositionInterpolator(); + if (positionInterpolator.InterpolateState.Target.HasValue) + { + return false; + } + + if (!Approximately(nonAuthority.transform.position, m_AuthorityInstance.transform.position)) + { + return false; + } + + m_NonAuthorityInstances.Add(nonAuthority); + } + return true; + } + + [UnityTest] + public IEnumerator StopAndStartMotion() + { + m_NetworkManagers.AddRange(m_ClientNetworkManagers); + if (!UseCMBService()) + { + m_NetworkManagers.Insert(0, m_ServerNetworkManager); + } + m_AuthorityNetworkManager = m_NetworkManagers[0]; + + m_AuthorityInstance = SpawnObject(m_TestPrefab, m_AuthorityNetworkManager).GetComponent(); + // Wait for all clients to spawn the instance + yield return WaitForConditionOrTimeOut(WaitForInstancesToSpawn); + AssertOnTimeout($"Not all clients spawned {m_AuthorityInstance.name}!"); + + ////// Start Motion + // Have authority move in a direction for a short period of time + m_Direction = GetRandomVector3(-10, 10).normalized; + m_NumberOfUpdates = 0; + m_AuthorityNetworkManager.NetworkTickSystem.Tick += NetworkTickSystem_Tick; + + yield return WaitForConditionOrTimeOut(() => m_NumberOfUpdates >= 10); + AssertOnTimeout($"Timed out waiting for all updates to be applied to the authority instance!"); + + ////// Finish interpolating and wait for each interpolator to detect a stop in the motion + // Wait for all non-authority instances to finish interpolating to the final destination point. + yield return WaitForConditionOrTimeOut(WaitForInstancesToFinishInterpolation); + AssertOnTimeout($"Not all clients finished interpolating {m_AuthorityInstance.name}!"); + + // Start recording the state updates on the non-authority instances + foreach (var testTransform in m_NonAuthorityInstances) + { + testTransform.CheckStateUpdates = true; + } + + ////// Stop to Start motion begins here + m_Direction = GetRandomVector3(-10, 10).normalized; + m_NumberOfUpdates = 0; + m_AuthorityNetworkManager.NetworkTickSystem.Tick += NetworkTickSystem_Tick; + + yield return WaitForConditionOrTimeOut(() => m_NumberOfUpdates >= 10); + AssertOnTimeout($"Timed out waiting for all updates to be applied to the authority instance!"); + + // Wait for all non-authority instances to finish interpolating to the final destination point. + yield return WaitForConditionOrTimeOut(WaitForInstancesToFinishInterpolation); + AssertOnTimeout($"Not all clients finished interpolating {m_AuthorityInstance.name}!"); + + // Checks that the time between the first and second state update is approximately the tick frequency + foreach (var testTransform in m_NonAuthorityInstances) + { + var deltaVariance = testTransform.GetTimeDeltaVarience(); + Assert.True(Approximately(deltaVariance, s_DefaultWaitForTick.waitTime), $"{testTransform.name}'s delta variance was {deltaVariance} when it should have been approximately {s_DefaultWaitForTick.waitTime}!"); + } + } + + /// + /// Moves the authority instance once per tick to simulate a change in transform state that occurs + /// every tick. + /// + private void NetworkTickSystem_Tick() + { + m_NumberOfUpdates++; + m_AuthorityInstance.transform.position = m_AuthorityInstance.transform.position + m_Direction * 2; + if (m_NumberOfUpdates >= 10) + { + m_AuthorityNetworkManager.NetworkTickSystem.Tick -= NetworkTickSystem_Tick; + } + } + + internal class TestStartStopTransform : NetworkTransform + { + + public bool CheckStateUpdates; + + private BufferedLinearInterpolatorVector3 m_PosInterpolator; + + private Dictionary m_StatesProcessed = new Dictionary(); + + public struct StateEntry + { + public float TimeAdded; + public BufferedLinearInterpolator.CurrentState State; + } + + protected override void Awake() + { + base.Awake(); + m_PosInterpolator = GetPositionInterpolator(); + } + + /// + /// Checks the time that passed between the first and second state updates. + /// + /// time passed as a float + public float GetTimeDeltaVarience() + { + var stateKeys = m_StatesProcessed.Keys.ToList(); + var firstState = m_StatesProcessed[stateKeys[0]]; + var secondState = m_StatesProcessed[stateKeys[1]]; + + var firstAndSecondTimeDelta = secondState.TimeAdded - firstState.TimeAdded; + + // Get the delta time between the two times of both the first and second state. + // Then add the time it should have taken to get to the second state, and this should be the total time to interpolate + // from the current position to the target position of the second state update. + var stateDelta = (float)(secondState.State.Target.Value.TimeSent - firstState.State.Target.Value.TimeSent + secondState.State.TimeToTargetValue); + // Return the time detla between the time that passed and the time that should have passed processing the states. + return Mathf.Abs(stateDelta - firstAndSecondTimeDelta); + } + + public override void OnUpdate() + { + base.OnUpdate(); + + // If we are checking the state updates, then we want to track each unique state update + if (CheckStateUpdates) + { + // Make sure we have a valid target + if (m_PosInterpolator.InterpolateState.Target.HasValue) + { + // If the state update's identifier is different + var itemId = m_PosInterpolator.InterpolateState.Target.Value.ItemId; + if (!m_StatesProcessed.ContainsKey(itemId)) + { + // Add it to the table of state updates + var stateEntry = new StateEntry() + { + TimeAdded = Time.realtimeSinceStartup, + State = m_PosInterpolator.InterpolateState, + }; + + m_StatesProcessed.Add(itemId, stateEntry); + } + } + } + } + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/InterpolationStopAndStartMotionTest.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/InterpolationStopAndStartMotionTest.cs.meta new file mode 100644 index 0000000000..9e04350607 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/InterpolationStopAndStartMotionTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 098d70b36b853da4c98b12d30298be9d \ No newline at end of file