using GameKit.Utilities; using FishNet.Utility.Extension; using System.Runtime.CompilerServices; using UnityEngine; using System.Collections.Generic; namespace FishNet.Object.Prediction { internal class AdaptiveInterpolationSmootherFixed { #if PREDICTION_V2 #region Types. /// /// Data on a goal to move towards. /// private class GoalData : IResettable { /// /// True if this GoalData is valid. /// public bool IsValid; /// /// Tick of the data this GoalData is for. /// public uint DataTick; /// /// Transform values to move towards. /// public TransformPropertiesCls TransformProperties = new TransformPropertiesCls(); /// /// Time remaining to move towards goal. /// public float TimeRemaining; public GoalData() { } public void InitializeState() { } public void ResetState() { DataTick = 0; TimeRemaining = 0f; TransformProperties.ResetState(); IsValid = false; } /// /// Updates values using a GoalData. /// public void Update(GoalData gd) { DataTick = gd.DataTick; TransformProperties.Update(gd.TransformProperties); TimeRemaining = gd.TimeRemaining; IsValid = true; } public void Update(uint dataTick, TransformPropertiesCls tp, float timeRemaining) { DataTick = dataTick; TransformProperties = tp; TimeRemaining = timeRemaining; IsValid = true; } } #endregion #region Private. /// /// Offsets of the root object during PreTick or PreReplicateReplay. /// private TransformProperties _rootPreSimulateWorldValues; /// /// Offsets of the graphical object during PreTick or PreReplicateReplay. /// private TransformProperties _graphicalPreSimulateWorldValues; /// /// SmoothingData to use. /// private AdaptiveInterpolationSmoothingData _smoothingData; /// /// Current interpolation value. This changes based on ping and settings. /// public long _currentInterpolation = 15; /// /// Current GoalData being used. /// private GoalData _currentGoalData = new GoalData(); /// /// MoveRates for currentGoalData. /// private MoveRates _currentMoveRates; /// /// GoalDatas to move towards. /// //private RingBuffer _goalDatas = new RingBuffer(); private List _goalDatas = new List(); /// /// Cached NetworkObject reference in SmoothingData for performance. /// private NetworkObject _networkObject; /// /// Cached tickDelta on the TimeManager. /// private float _tickDelta; /// /// Multiplier to apply towards movements. This is used to speed up and slow down buffer as needed. /// private float _rateMultiplier = 1f; #endregion #region Const. /// /// Multiplier to apply to movement speed when buffer is over interpolation. /// private const float OVERFLOW_MULTIPLIER = 10f; /// /// Multiplier to apply to movement speed when buffer is under interpolation. /// private const float UNDERFLOW_MULTIPLIER = 1f; #endregion public AdaptiveInterpolationSmootherFixed() { /* Initialize for up to 50 * goal datas. Anything beyond that * is unreasonable. */ //_goalDatas.Initialize(50); } /// /// Initializes this for use. /// internal void Initialize(AdaptiveInterpolationSmoothingData data) { _smoothingData = data; _networkObject = data.NetworkObject; _tickDelta = (float)_networkObject.TimeManager.TickDelta; SetGraphicalObject(data.GraphicalObject); } /// /// /// Called every frame. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Update() { if (CanSmooth()) MoveToTarget(); } /// /// Called when the TimeManager invokes OnPreTick. /// public void OnPreTick() { if (CanSmooth()) { _graphicalPreSimulateWorldValues = _smoothingData.GraphicalObject.GetWorldProperties(); _rootPreSimulateWorldValues.Update(_networkObject.transform); } } /// /// Called when the TimeManager invokes OnPostTick. /// public void OnPostTick() { if (CanSmooth()) { //Reset graphics to start graphicals transforms properties. _smoothingData.GraphicalObject.SetPositionAndRotation(_graphicalPreSimulateWorldValues.Position, _graphicalPreSimulateWorldValues.Rotation); //Create a goal data for new transform position. uint tick = _networkObject.LastUnorderedReplicateTick; CreatePostSimulateGoalData(tick, true); } } /// /// Called before a reconcile runs a replay. /// public void OnPreReplicateReplay(uint clientTick, uint serverTick) { //Update the last post simulate data. if (CanSmooth()) _rootPreSimulateWorldValues.Update(_networkObject.transform); } /// /// Called after a reconcile runs a replay. /// public void OnPostReplicateReplay(uint clientTick, uint serverTick) { if (CanSmooth()) { /* Create new goal data from the replay. * This must be done every replay. If a desync * did occur then the goaldatas would be different * from what they were previously. */ uint tick = _networkObject.LastUnorderedReplicateTick; CreatePostSimulateGoalData(tick, false); } } public void OnPostReconcile(uint clientReconcileTick, uint serverReconcileTick) { if (CanSmooth()) { _rootPreSimulateWorldValues.Update(_networkObject.transform); int countOverInterpolation = (_goalDatas.Count - (int)_currentInterpolation); //Debug.Log($"{Time.frameCount}. CountOver {countOverInterpolation}"); } } /// /// Sets GraphicalObject. /// /// public void SetGraphicalObject(Transform value) { _smoothingData.GraphicalObject = value; _graphicalPreSimulateWorldValues.Update(value); } /// /// Returns if the graphics can be smoothed. /// /// private bool CanSmooth() { if (_networkObject.IsOwner) return false; if (_networkObject.IsServerOnly || _networkObject.IsHost) return false; return true; } /// /// Returns if this transform matches arguments. /// /// private bool GraphicalObjectMatches(Vector3 position, Quaternion rotation) { bool positionMatches = (!_smoothingData.SmoothPosition || (_smoothingData.GraphicalObject.position == position)); bool rotationMatches = (!_smoothingData.SmoothRotation || (_smoothingData.GraphicalObject.rotation == rotation)); return (positionMatches && rotationMatches); } /// /// Sets CurrentGoalData to the next in queue. Returns if was set successfully. /// private bool SetCurrentGoalData() { if (_goalDatas.Count == 0) { _currentGoalData.IsValid = false; Debug.LogError("No more goal datas."); return false; } else { /* Previous will always be current since * we are getting next in queue. We * later check if current is valid to determine * if instant rates should be set or normal rates. * If current is not valie then instant rates are set * to teleport graphics to their starting position, and * future sets will have a valid current. */ GoalData prev = _currentGoalData; //Set next and make valid. GoalData next = _goalDatas[0]; //Remove from goalDatas. _goalDatas.RemoveAt(0); if (prev != null && prev.IsValid) SetCurrentMoveRates(prev.DataTick, next.DataTick, prev.TransformProperties, next.TransformProperties); else _currentMoveRates.SetInstantRates(); //Store previous. if (prev != null) ResettableObjectCaches.Store(prev); //Assign new current. _currentGoalData = next; Debug.LogWarning($"Set CurrentGoalData on tick {_currentGoalData.DataTick}, remaining {_goalDatas.Count}"); return true; } } /// /// Moves to a GoalData. Automatically determins if to use data from server or client. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void MoveToTarget(float deltaOverride = -1f) { /* If the current goal data is not valid then * try to set a new one. If none are available * it will remain inactive. */ if (!_currentGoalData.IsValid) { if (!SetCurrentGoalData()) return; } GoalData currentGd = _currentGoalData; float delta = (deltaOverride != -1f) ? deltaOverride : Time.deltaTime; /* Once here it's safe to assume the object will be moving. * Any checks which would stop it from moving be it client * auth and owner, or server controlled and server, ect, * would have already been run. */ TransformPropertiesCls td = currentGd.TransformProperties; MoveRates mr = _currentMoveRates; //How much multiplier should change in either direction over a second. float multiplierChangeRate = 0.3f; int queueCount = _goalDatas.Count; /* Begin moving even if interpolation buffer isn't * met to provide more real-time interactions but * speed up when buffer is too large. This should * provide a good balance of accuracy. */ int countOverInterpolation = (queueCount - (int)_currentInterpolation); string debugPrint = string.Empty; //Really high over interpolation, snap to datas. if (countOverInterpolation > (_currentInterpolation * 30)) { debugPrint = $"OverInterpolation {countOverInterpolation}. Teleporting."; mr.SetInstantRates(); //Setting to -1 will force it to go negative, which will clear next goal data for teleport as well. currentGd.TimeRemaining = -1f; } else if (countOverInterpolation > 0) { debugPrint = $"OverInterpolation {countOverInterpolation}. Increasing."; _rateMultiplier += (multiplierChangeRate * delta); } else if (countOverInterpolation < 0) { debugPrint = $"OverInterpolation {countOverInterpolation}. Slowing."; _rateMultiplier -= (multiplierChangeRate * delta); } else { _rateMultiplier = Mathf.MoveTowards(_rateMultiplier, 1f, (multiplierChangeRate * delta)); } //Clamp multiplier. const float maximumMultiplier = 1.1f; const float minimumMultiplier = 0.95f; _rateMultiplier = Mathf.Clamp(_rateMultiplier, minimumMultiplier, maximumMultiplier); //Apply multiplier to delta. delta *= _rateMultiplier; // if (debugPrint != string.Empty && _networkObject.TimeManager.FrameTicked) // Debug.Log($"{debugPrint}. Multiplier {_rateMultiplier}"); //multiplier = 1f; //delta = Time.deltaTime; //Rate to update. Changes per property. float rate; Transform t = _smoothingData.GraphicalObject; //Position. if (_smoothingData.SmoothPosition) { rate = mr.Position; Vector3 posGoal = td.Position; if (rate == MoveRatesCls.INSTANT_VALUE) t.position = td.Position; else if (rate > 0f) t.position = Vector3.MoveTowards(t.position, posGoal, rate * delta); } //Rotation. if (_smoothingData.SmoothRotation) { rate = mr.Rotation; if (rate == MoveRatesCls.INSTANT_VALUE) t.rotation = td.Rotation; else if (rate > 0f) t.rotation = Quaternion.RotateTowards(t.rotation, td.Rotation, rate * delta); } if (currentGd.TimeRemaining > 0f) currentGd.TimeRemaining -= delta; if (currentGd.TimeRemaining <= 0f) { bool graphicsMatch = GraphicalObjectMatches(td.Position, td.Rotation); if (graphicsMatch) { float leftOver = Mathf.Abs(currentGd.TimeRemaining); if (SetCurrentGoalData()) MoveToTarget(leftOver); } } } #region Rates. /// /// Sets move rates which will occur over time. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetCurrentMoveRates(uint prevTick, uint tick, TransformPropertiesCls prevTp, TransformPropertiesCls nextTp) { long lngTicksPassed = (tick - prevTick); //Should not happen. if (lngTicksPassed <= 0) { _networkObject.NetworkManager.LogError($"Ticks passed returned negative as {lngTicksPassed}. Instant rates are being set."); _currentMoveRates.SetInstantRates(); return; } //More than 1 tick, also unusual. else if (lngTicksPassed > 1) { _networkObject.NetworkManager.LogError($"Ticks passed are not equal to 1, passed value is {lngTicksPassed}"); // lngTicksPassed = 1; } uint ticksPassed = (uint)lngTicksPassed; float delta = _tickDelta; float distance; float rate; const float v3Tolerance = 0.0001f; const float qTolerance = 0.2f; //Position. rate = prevTp.Position.GetRate(nextTp.Position, delta, out distance, ticksPassed); //If distance teleports assume rest do. if (_smoothingData.TeleportThreshold != MoveRates.UNSET_VALUE && distance >= _smoothingData.TeleportThreshold) { Debug.Log($"Teleporting threshhold."); _currentMoveRates.SetInstantRates(); return; } float positionRate = rate.SetIfUnderTolerance(v3Tolerance, MoveRates.INSTANT_VALUE); //Rotation. rate = prevTp.Rotation.GetRate(nextTp.Rotation, delta, out _, ticksPassed); float rotationRate = rate.SetIfUnderTolerance(qTolerance, MoveRates.INSTANT_VALUE); _currentMoveRates.Update(positionRate, rotationRate, MoveRates.INSTANT_VALUE); } #endregion /// /// Removes GoalDatas which make the queue excessive. /// This could cause teleportation but would rarely occur, only potentially during sever network issues. /// private void RemoveExcessiveGoalDatas() { if (_goalDatas.Count > 100) Debug.LogError($"Whoa getting kind of high with count of {_goalDatas.Count}"); ///* Remove entries which are excessive to the buffer. //* This could create a starting jitter but it will ensure //* the buffer does not fill too much. The buffer next sho0..uld //* actually get unreasonably high but rather safe than sorry. */ //int maximumBufferAllowance = ((int)_currentInterpolation * 8); //int removedBufferCount = (_goalDatas.Count - maximumBufferAllowance); ////If there are some to remove. //if (removedBufferCount > 0) //{ // for (int i = 0; i < removedBufferCount; i++) // ResettableObjectCaches.Store(_goalDatas[0 + i]); // //_goalDatas.RemoveRange(true, removedBufferCount); // _goalDatas.RemoveRange(0, removedBufferCount); //} } /// /// Creates a GoalData after a simulate. /// /// True if being created for OnPostTick. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CreatePostSimulateGoalData(uint tick, bool postTick) { RemoveExcessiveGoalDatas(); int dataIndex = -1; bool useUpdate = false; /* Post ticks always go on the end. * The tick will be wrong for the post tick, so set it * to the last entry tick + 1. */ int datasCount = _goalDatas.Count; if (postTick) { if (datasCount > 0) tick = _goalDatas[datasCount - 1].DataTick + 1; else tick = _currentGoalData.DataTick + 1; dataIndex = datasCount; } else { /* There is no need to create a goaldata * if the tick is previous to currentGoalData. * This would indicate the graphics have already * moved past tick. */ if (tick < _currentGoalData.DataTick) { //Debug.LogWarning($"Frame {Time.frameCount}. Skipping tick {tick}. Current {_currentGoalData.DataTick}. PostTick? {postTick}. QueueCount {_goalDatas.Count}. StatesCount {_networkObject.PredictionManager._recievedStates.Count}"); return; } //If current tick then let current play out and do nothing. else if (tick == _currentGoalData.DataTick) { return; } uint prevArrTick = 0; for (int i = 0; i < datasCount; i++) { uint arrTick = _goalDatas[i].DataTick; if (tick == arrTick) { dataIndex = i; useUpdate = true; break; } else if (i > 0 && tick > prevArrTick && tick < arrTick) { dataIndex = i; break; } prevArrTick = arrTick; } if (dataIndex == -1) { //Insert at beginning. if (datasCount > 0 && tick < _goalDatas[0].DataTick) dataIndex = 0; //Insert at end. else dataIndex = datasCount; } } Transform rootT = _networkObject.transform; //Begin building next goal data. GoalData nextGd = ResettableObjectCaches.Retrieve(); nextGd.DataTick = tick; nextGd.TimeRemaining = _tickDelta; nextGd.IsValid = true; //Set next transform data. TransformPropertiesCls nextTp = nextGd.TransformProperties; //Position. if (!_smoothingData.SmoothPosition) nextTp.Position = _graphicalPreSimulateWorldValues.Position; else nextTp.Position = rootT.position; //ROtation. if (!_smoothingData.SmoothRotation) nextTp.Rotation = _graphicalPreSimulateWorldValues.Rotation; else nextTp.Rotation = rootT.rotation; //Vector3 lineDist = new Vector3(0f, 3f, 0f); //if (!postTick) // Debug.DrawLine(rootT.position + lineDist, rootT.position, Color.red, 2f); //else // Debug.DrawLine(rootT.position + lineDist + new Vector3(1f, 0f, 0f), rootT.position, Color.blue, 2f); if (useUpdate) _goalDatas[dataIndex].Update(nextGd); else _goalDatas.Insert(dataIndex, nextGd); } #endif } }