diff --git a/Experimental/Anticipation Sample/Assets/Scripts/AnticipationSample.cs b/Experimental/Anticipation Sample/Assets/Scripts/AnticipationSample.cs index 9418cb47..690e3f70 100644 --- a/Experimental/Anticipation Sample/Assets/Scripts/AnticipationSample.cs +++ b/Experimental/Anticipation Sample/Assets/Scripts/AnticipationSample.cs @@ -86,24 +86,27 @@ public class AnticipationSample : NetworkBehaviour private const float k_ValueEChangePerSecond = 2.5f; - public override void OnNetworkSpawn() + public override void OnReanticipate(double lastRoundTripTime) { // Initialize the reanticipation for all of the values: // C and D react to a request to reanticipate by simply smoothing between the previous anticipated value // and the new authoritative value. They are not frequently updated and only need any reanticipation action // when the anticipation was wrong. - AnticipatedNetworkVariable.OnReanticipateDelegate smooth = (AnticipatedNetworkVariable variable, in float anticipatedValue, double anticipationTime, in float authoritativeValue, double authoritativeTime) => + + if (ValueC.ShouldReanticipate) { - variable.Smooth(anticipatedValue, authoritativeValue, SmoothTime, Mathf.Lerp); - }; - ValueC.OnReanticipate = smooth; - ValueD.OnReanticipate = smooth; + ValueC.Smooth(ValueC.PreviousAnticipatedValue, ValueC.AuthoritativeValue, SmoothTime, Mathf.Lerp); + } + if (ValueD.ShouldReanticipate) + { + ValueD.Smooth(ValueD.PreviousAnticipatedValue, ValueD.AuthoritativeValue, SmoothTime, Mathf.Lerp); + } // E is actually trying to anticipate the current value of a constantly changing object to hide latency. // It uses the amount of time that has passed since the authoritativetime to gauge the latency of this update // and anticipates a new value based on that delay. The server value is in the past, so the predicted value // attempts to guess what the value is in the present. - ValueE.OnReanticipate = (AnticipatedNetworkVariable variable, in float anticipatedValue, double anticipationTime, in float authoritativeValue, double authoritativeTime) => + if(ValueE.ShouldReanticipate) { // There is an important distinction between the smoothing this is doing and the smoothing the player object // is doing: @@ -113,15 +116,15 @@ public class AnticipationSample : NetworkBehaviour // value - the difference between current time and authoritativeTime represents a full round trip, but the // actual time difference here is only a half round trip, so we multiply by 0.5. // Then, because smoothing adds its own latency, we add the smooth time into the mix. - var secondsBehind = (NetworkManager.LocalTime.Time - authoritativeTime) * 0.5f + SmoothTime; + var secondsBehind = lastRoundTripTime * 0.5f + SmoothTime; - var newAnticipatedValue = (float)(authoritativeValue + k_ValueEChangePerSecond * secondsBehind) % 10; + var newAnticipatedValue = (float)(ValueE.AuthoritativeValue + k_ValueEChangePerSecond * secondsBehind) % 10; // This variable uses a custom interpolation callback that handles the drop from 10 // down to 0. Without this, there is either weird smoothing behavior, or hitching. // This keeps the interpolation going, and handles the case where the interpolated value // goes over 10 and has to jump back to 0. - variable.Smooth(anticipatedValue, newAnticipatedValue, SmoothTime, ((start, end, amount) => + ValueE.Smooth(ValueE.PreviousAnticipatedValue, newAnticipatedValue, SmoothTime, ((start, end, amount) => { if (end < 3 && start > 7) { diff --git a/Experimental/Anticipation Sample/Assets/Scripts/FrameHistory.cs b/Experimental/Anticipation Sample/Assets/Scripts/FrameHistory.cs index 3b574d03..db6113f0 100644 --- a/Experimental/Anticipation Sample/Assets/Scripts/FrameHistory.cs +++ b/Experimental/Anticipation Sample/Assets/Scripts/FrameHistory.cs @@ -53,6 +53,14 @@ namespace DefaultNamespace m_History.RemoveAll(item => item.Time > time); } + /// + /// Remove all items from the history + /// + public void Clear() + { + m_History.Clear();; + } + /// /// Get the full history, useful for iterating through all the values to reapply them when reanticipating. /// diff --git a/Experimental/Anticipation Sample/Assets/Scripts/InputManager.cs b/Experimental/Anticipation Sample/Assets/Scripts/InputManager.cs index 2cab5008..316e4884 100644 --- a/Experimental/Anticipation Sample/Assets/Scripts/InputManager.cs +++ b/Experimental/Anticipation Sample/Assets/Scripts/InputManager.cs @@ -102,5 +102,13 @@ namespace DefaultNamespace { return m_HistoricalInput.GetHistory(); } + + /// + /// Remove all items from the history + /// + public void Clear() + { + m_HistoricalInput.Clear(); + } } } diff --git a/Experimental/Anticipation Sample/Assets/Scripts/PlayerMovableObject.cs b/Experimental/Anticipation Sample/Assets/Scripts/PlayerMovableObject.cs index 443dc4ce..211b704e 100644 --- a/Experimental/Anticipation Sample/Assets/Scripts/PlayerMovableObject.cs +++ b/Experimental/Anticipation Sample/Assets/Scripts/PlayerMovableObject.cs @@ -106,46 +106,66 @@ namespace DefaultNamespace } } - public override void OnNetworkSpawn() + public override void OnReanticipate(double lastRoundTripTime) { - MyTransform.OnReanticipate = (networkTransform, anticipatedValue, anticipationTime, authorityValue, authorityTime) => - { - // Here we re-anticipate the new position of the player based on the updated server position. - // We do this by taking the current authoritative position and replaying every input we have received - // since the reported authority time, re-applying all the movement we have applied since then - // to arrive at a new anticipated player location. - foreach (var item in InputManager.GetHistory()) - { - if (item.Time <= authorityTime) - { - continue; - } + // Have to store the transform's previous state because calls to AnticipateMove() and + // AnticipateRotate() will overwrite it. + var previousState = MyTransform.PreviousAnticipatedState; - Move(item.Item, true); + var authorityTime = NetworkManager.LocalTime.Time - lastRoundTripTime; + // Here we re-anticipate the new position of the player based on the updated server position. + // We do this by taking the current authoritative position and replaying every input we have received + // since the reported authority time, re-applying all the movement we have applied since then + // to arrive at a new anticipated player location. + + foreach (var item in InputManager.GetHistory()) + { + if (item.Time <= authorityTime) + { + continue; } - // Clear out all the input history before the given authority time. We don't need anything before that - // anymore as we won't get any more updates from the server from before this one. We keep the current - // authority time because theoretically another system may need that. - InputManager.RemoveBefore(authorityTime); - // It's not always desirable to smooth the transform. In cases of very large discrepencies in state, - // it can sometimes be desirable to simply teleport to the new position. We use the SmoothDistance - // value (and use SqrMagnitude instead of Distance for efficiency) as a threshold for teleportation. - // This could also use other mechanisms of detection: For example, when the Telport input is included - // in the replay set, we could set a flag to disable smoothing because we know we are teleporting. - if (SmoothTime != 0.0 && Vector3.SqrMagnitude(anticipatedValue.Position - networkTransform.AnticipatedState.Position) < SmoothDistance * SmoothDistance) + + Move(item.Item, true); + } + // Clear out all the input history before the given authority time. We don't need anything before that + // anymore as we won't get any more updates from the server from before this one. We keep the current + // authority time because theoretically another system may need that. + InputManager.RemoveBefore(authorityTime); + // It's not always desirable to smooth the transform. In cases of very large discrepencies in state, + // it can sometimes be desirable to simply teleport to the new position. We use the SmoothDistance + // value (and use SqrMagnitude instead of Distance for efficiency) as a threshold for teleportation. + // This could also use other mechanisms of detection: For example, when the Telport input is included + // in the replay set, we could set a flag to disable smoothing because we know we are teleporting. + if (SmoothTime != 0.0) + { + var sqDist = Vector3.SqrMagnitude(previousState.Position - MyTransform.AnticipatedState.Position); + if (sqDist <= 0.25 * 0.25) + { + // This prevents small amounts of wobble from slight differences. + MyTransform.AnticipateState(previousState); + } + else if (sqDist < SmoothDistance * SmoothDistance) { // Server updates are not necessarily smooth, so applying reanticipation can also result in // hitchy, unsmooth animations. To compensate for that, we call this to smooth from the previous // anticipated state (stored in "anticipatedValue") to the new state (which, because we have used // the "Move" method that updates the anticipated state of the transform, is now the current // transform anticipated state) - networkTransform.Smooth(anticipatedValue, networkTransform.AnticipatedState, SmoothTime); + MyTransform.Smooth(previousState, MyTransform.AnticipatedState, SmoothTime); } - }; - base.OnNetworkSpawn(); + } } + /// + /// When we apply changes to the latency and jitter, it respawns everything. + /// We want to make sure there's no input left over from before that by clearing it. + /// + public override void OnNetworkSpawn() + { + InputManager.Clear(); + } + /// /// Pass client inputs to the server so the server can mirror the client simulation. /// diff --git a/Experimental/Anticipation Sample/Packages/packages-lock.json b/Experimental/Anticipation Sample/Packages/packages-lock.json index 26418160..e58d4e2b 100644 --- a/Experimental/Anticipation Sample/Packages/packages-lock.json +++ b/Experimental/Anticipation Sample/Packages/packages-lock.json @@ -94,7 +94,7 @@ "com.unity.nuget.mono-cecil": "1.10.1", "com.unity.transport": "1.4.0" }, - "hash": "6eec9634c17a2d16fb18dbca56c0a3280ff2c39e" + "hash": "ef792ef4177a4ca5efde322d3adbf79cd68b6ba5" }, "com.unity.nuget.mono-cecil": { "version": "1.10.1",