882 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			882 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using FishNet.Object;
 | |
| using FishNet.Transporting;
 | |
| using FishNet.Utility.Extension;
 | |
| using System.Collections.Generic;
 | |
| using System.Runtime.CompilerServices;
 | |
| using UnityEngine;
 | |
| 
 | |
| 
 | |
| namespace FishNet.Component.Prediction
 | |
| {
 | |
|     internal class PredictedObjectSpectatorSmoother
 | |
|     {
 | |
|         #region Types.
 | |
|         /// <summary>
 | |
|         /// Data on a goal to move towards.
 | |
|         /// </summary>
 | |
|         private class GoalData
 | |
|         {
 | |
|             public bool IsActive;
 | |
|             /// <summary>
 | |
|             /// LocalTick this data is for.
 | |
|             /// </summary>
 | |
|             public uint LocalTick;
 | |
|             /// <summary>
 | |
|             /// Data on how fast to move to transform values.
 | |
|             /// </summary>
 | |
|             public RateData Rates = new RateData();
 | |
|             /// <summary>
 | |
|             /// Transform values to move towards.
 | |
|             /// </summary> 
 | |
|             public TransformData Transforms = new TransformData();
 | |
| 
 | |
|             public GoalData() { }
 | |
|             /// <summary>
 | |
|             /// Resets values for re-use.
 | |
|             /// </summary>
 | |
|             public void Reset()
 | |
|             {
 | |
|                 LocalTick = 0;
 | |
|                 Transforms.Reset();
 | |
|                 Rates.Reset();
 | |
|                 IsActive = false;
 | |
|             }
 | |
| 
 | |
|             /// <summary>
 | |
|             /// Updates values using a GoalData.
 | |
|             /// </summary>
 | |
|             public void Update(GoalData gd)
 | |
|             {
 | |
|                 LocalTick = gd.LocalTick;
 | |
|                 Rates.Update(gd.Rates);
 | |
|                 Transforms.Update(gd.Transforms);
 | |
|                 IsActive = true;
 | |
|             }
 | |
| 
 | |
|             public void Update(uint localTick, RateData rd, TransformData td)
 | |
|             {
 | |
|                 LocalTick = localTick;
 | |
|                 Rates = rd;
 | |
|                 Transforms = td;
 | |
|                 IsActive = true;
 | |
|             }
 | |
|         }
 | |
|         /// <summary>
 | |
|         /// How fast to move to values.
 | |
|         /// </summary>
 | |
|         private class RateData
 | |
|         {
 | |
|             /// <summary>
 | |
|             /// Rate for position after smart calculations.
 | |
|             /// </summary>
 | |
|             public float Position;
 | |
|             /// <summary>
 | |
|             /// Rate for rotation after smart calculations.
 | |
|             /// </summary>
 | |
|             public float Rotation;
 | |
|             /// <summary>
 | |
|             /// Number of ticks the rates are calculated for.
 | |
|             /// If TickSpan is 2 then the rates are calculated under the assumption the transform changed over 2 ticks.
 | |
|             /// </summary>
 | |
|             public uint TickSpan;
 | |
|             /// <summary>
 | |
|             /// Time remaining until transform is expected to reach it's goal.
 | |
|             /// </summary>
 | |
|             internal float TimeRemaining;
 | |
| 
 | |
|             public RateData() { }
 | |
| 
 | |
|             /// <summary>
 | |
|             /// Resets values for re-use.
 | |
|             /// </summary>
 | |
|             public void Reset()
 | |
|             {
 | |
|                 Position = 0f;
 | |
|                 Rotation = 0f;
 | |
|                 TickSpan = 0;
 | |
|                 TimeRemaining = 0f;
 | |
|             }
 | |
| 
 | |
|             [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | |
|             public void Update(RateData rd)
 | |
|             {
 | |
|                 Update(rd.Position, rd.Rotation, rd.TickSpan, rd.TimeRemaining);
 | |
|             }
 | |
| 
 | |
|             /// <summary>
 | |
|             /// Updates rates.
 | |
|             /// </summary>
 | |
|             public void Update(float position, float rotation, uint tickSpan, float timeRemaining)
 | |
|             {
 | |
|                 Position = position;
 | |
|                 Rotation = rotation;
 | |
|                 TickSpan = tickSpan;
 | |
|                 TimeRemaining = timeRemaining;
 | |
|             }
 | |
|         }
 | |
|         /// <summary>
 | |
|         /// Data about where a transform should move towards.
 | |
|         /// </summary>
 | |
|         private class TransformData
 | |
|         {
 | |
|             /// <summary>
 | |
|             /// Position of the transform.
 | |
|             /// </summary>
 | |
|             public Vector3 Position;
 | |
|             /// <summary>
 | |
|             /// Rotation of the transform.
 | |
|             /// </summary>
 | |
|             public Quaternion Rotation;
 | |
| 
 | |
|             public void Reset()
 | |
|             {
 | |
|                 Position = Vector3.zero;
 | |
|                 Rotation = Quaternion.identity;
 | |
|             }
 | |
|             /// <summary>
 | |
|             /// Updates this data.
 | |
|             /// </summary>
 | |
|             public void Update(TransformData copy)
 | |
|             {
 | |
|                 Update(copy.Position, copy.Rotation);
 | |
|             }
 | |
|             /// <summary>
 | |
|             /// Updates this data.
 | |
|             /// </summary>
 | |
|             public void Update(Vector3 position, Quaternion rotation)
 | |
|             {
 | |
|                 Position = position;
 | |
|                 Rotation = rotation;
 | |
|             }
 | |
|             /// <summary>
 | |
|             /// Updates this data.
 | |
|             /// </summary>
 | |
|             public void Update(Rigidbody rigidbody)
 | |
|             {
 | |
|                 Position = rigidbody.transform.position;
 | |
|                 Rotation = rigidbody.transform.rotation;
 | |
|             }
 | |
|             /// <summary>
 | |
|             /// Updates this data.
 | |
|             /// </summary>
 | |
|             public void Update(Rigidbody2D rigidbody)
 | |
|             {
 | |
|                 Position = rigidbody.transform.position;
 | |
|                 Rotation = rigidbody.transform.rotation;
 | |
|             }
 | |
|         }
 | |
|         #endregion
 | |
| 
 | |
|         #region Private.
 | |
|         /// <summary>
 | |
|         /// Current GoalData being used.
 | |
|         /// </summary>
 | |
|         private GoalData _currentGoalData = new GoalData();
 | |
|         /// <summary>
 | |
|         /// Object to smooth.
 | |
|         /// </summary>
 | |
|         private Transform _graphicalObject;
 | |
|         /// <summary>
 | |
|         /// Sets GraphicalObject.
 | |
|         /// </summary>
 | |
|         /// <param name="value"></param>
 | |
|         public void SetGraphicalObject(Transform value) => _graphicalObject = value;
 | |
|         /// <summary>
 | |
|         /// True to move towards position goals.
 | |
|         /// </summary>
 | |
|         private bool _smoothPosition;
 | |
|         /// <summary>
 | |
|         /// True to move towards rotation goals.
 | |
|         /// </summary>
 | |
|         private bool _smoothRotation;
 | |
|         /// <summary>
 | |
|         /// How far in the past to keep the graphical object.
 | |
|         /// </summary>
 | |
|         private byte _interpolation = 4;
 | |
|         /// <summary>
 | |
|         /// Sets the interpolation value to use when the owner of this object.
 | |
|         /// </summary>
 | |
|         /// <param name="value"></param>
 | |
|         public void SetInterpolation(byte value) => _interpolation = value;
 | |
|         /// <summary>
 | |
|         /// Sets the number of ticks to ignroe at the end of a replay.
 | |
|         /// </summary>
 | |
|         /// <param name="value"></param>
 | |
|         public void SetIgnoredTicks(uint value) => _ignoredTicks = value;
 | |
|         /// <summary>
 | |
|         /// GoalDatas to move towards.
 | |
|         /// </summary>
 | |
|         private List<GoalData> _goalDatas = new List<GoalData>();
 | |
|         /// <summary>
 | |
|         /// Rigidbody to use.
 | |
|         /// </summary>
 | |
|         private Rigidbody _rigidbody;
 | |
|         /// <summary>
 | |
|         /// Rigidbody2D to use.
 | |
|         /// </summary>
 | |
|         private Rigidbody2D _rigidbody2d;
 | |
|         /// <summary>
 | |
|         /// Transform state during PreTick.
 | |
|         /// </summary>
 | |
|         private TransformData _preTickTransformdata = new TransformData();
 | |
|         /// <summary>
 | |
|         /// Type of rigidbody being used.
 | |
|         /// </summary>
 | |
|         private RigidbodyType _rigidbodyType;
 | |
|         /// <summary>
 | |
|         /// Last tick which a reconcile occured. This is reset at the end of a tick.
 | |
|         /// </summary>
 | |
|         private long _reconcileLocalTick = -1;
 | |
|         /// <summary>
 | |
|         /// Called when this frame receives OnPreTick.
 | |
|         /// </summary>
 | |
|         private bool _preTickReceived;
 | |
|         /// <summary>
 | |
|         /// Start position for graphicalObject at the beginning of the tick.
 | |
|         /// </summary>
 | |
|         private Vector3 _graphicalStartPosition;
 | |
|         /// <summary>
 | |
|         /// Start rotation for graphicalObject at the beginning of the tick.
 | |
|         /// </summary>
 | |
|         private Quaternion _graphicalStartRotation;
 | |
|         /// <summary>
 | |
|         /// How far a distance change must exceed to teleport the graphical object. -1f indicates teleport is not enabled.
 | |
|         /// </summary>
 | |
|         private float _teleportThreshold;
 | |
|         /// <summary>
 | |
|         /// PredictedObject which is using this object.
 | |
|         /// </summary>
 | |
|         private PredictedObject _predictedObject;
 | |
|         /// <summary>
 | |
|         /// Cache of GoalDatas to prevent allocations.
 | |
|         /// </summary>
 | |
|         private static Stack<GoalData> _goalDataCache = new Stack<GoalData>();
 | |
|         /// <summary>
 | |
|         /// Number of ticks to ignore when replaying.
 | |
|         /// </summary>
 | |
|         private uint _ignoredTicks;
 | |
|         /// <summary>
 | |
|         /// Cached localtick for performance.
 | |
|         /// </summary>
 | |
|         private uint _localTick;
 | |
|         /// <summary>
 | |
|         /// Start position of the graphical object in world space.
 | |
|         /// </summary>
 | |
|         private Vector3 _startWorldPosition;
 | |
|         /// <summary>
 | |
|         /// Multiplier to apply towards overflow multiplier when too many entries are in the buffer.
 | |
|         /// </summary>
 | |
|         private float _excessBufferMultiplier;
 | |
|         /// <summary>
 | |
|         /// Sets excessBufferMultiplier value.
 | |
|         /// </summary>
 | |
|         /// <param name="value"></param>
 | |
|         public void SetExcessBufferMultiplier(float value) => _excessBufferMultiplier = value;
 | |
|         #endregion
 | |
| 
 | |
|         #region Const.
 | |
|         /// <summary>
 | |
|         /// Multiplier to apply to movement speed when buffer is over or under interpolation.
 | |
|         /// </summary>
 | |
|         private const float OVERFLOW_MULTIPLIER = 0.1f;
 | |
|         #endregion
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Initializes this for use.
 | |
|         /// </summary>
 | |
|         internal void Initialize(PredictedObject po, RigidbodyType rbType, Rigidbody rb, Rigidbody2D rb2d, Transform graphicalObject
 | |
|             , bool smoothPosition, bool smoothRotation, float teleportThreshold)
 | |
|         {
 | |
|             _predictedObject = po;
 | |
|             _rigidbodyType = rbType;
 | |
| 
 | |
|             _rigidbody = rb;
 | |
|             _rigidbody2d = rb2d;
 | |
|             _graphicalObject = graphicalObject;
 | |
|             _startWorldPosition = _graphicalObject.position;
 | |
|             _smoothPosition = smoothPosition;
 | |
|             _smoothRotation = smoothRotation;
 | |
|             _teleportThreshold = teleportThreshold;
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// <summary>
 | |
|         /// Called every frame.
 | |
|         /// </summary>
 | |
|         [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | |
|         public void ManualUpdate()
 | |
|         {
 | |
|             if (CanSmooth())
 | |
|                 MoveToTarget();
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Called when the TimeManager invokes OnPreTick.
 | |
|         /// </summary>
 | |
|         public void OnPreTick()
 | |
|         {
 | |
|             if (CanSmooth())
 | |
|             {
 | |
|                 _localTick = _predictedObject.TimeManager.LocalTick;
 | |
|                 if (!_preTickReceived)
 | |
|                 {
 | |
|                     uint tick = _predictedObject.TimeManager.LocalTick - 1;
 | |
|                     CreateGoalData(tick, false);
 | |
|                 }
 | |
|                 _preTickReceived = true;
 | |
| 
 | |
|                 if (_rigidbodyType == RigidbodyType.Rigidbody)
 | |
|                     _preTickTransformdata.Update(_rigidbody);
 | |
|                 else
 | |
|                     _preTickTransformdata.Update(_rigidbody2d);
 | |
| 
 | |
|                 _graphicalStartPosition = _graphicalObject.position;
 | |
|                 _graphicalStartRotation = _graphicalObject.rotation;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Called when the TimeManager invokes OnPostTick.
 | |
|         /// </summary>
 | |
|         public void OnPostTick()
 | |
|         {
 | |
|             if (CanSmooth())
 | |
|             {
 | |
|                 if (!_preTickReceived)
 | |
|                 {
 | |
|                     /* During test the Z value for applyImmediately is 5.9.
 | |
|                      * Then increased 1 unit per tick: 6.9, 7.9.
 | |
|                      * 
 | |
|                      * When the spectator smoother initializes 5.9 is shown.
 | |
|                      * Before first starting smoothing the transform needs to be set
 | |
|                      * back to that.
 | |
|                      * 
 | |
|                      * The second issue is the first addition to goal datas seems
 | |
|                      * to occur at 7.9. This would need to be 6.9 to move from the
 | |
|                      * proper 5.9 starting point. It's probably because pretick is not received
 | |
|                      * when OnPostTick is called at the 6.9 position.
 | |
|                      * 
 | |
|                      * Have not validated the above yet but that's the most likely situation since
 | |
|                      * we know this was initialized at 5.9, which means it would be assumed pretick would
 | |
|                      * call at 6.9. Perhaps the following is happening....
 | |
|                      * 
 | |
|                      * - Pretick.
 | |
|                      * - Client gets spawn+applyImmediately.
 | |
|                      * - This also initializes this script at 5.9.
 | |
|                      * - Simulation moves object to 6.9.
 | |
|                      * - PostTick.
 | |
|                      * - This script does not run because _preTickReceived is not set yet.
 | |
|                      * 
 | |
|                      * - Pretick. Sets _preTickReceived.
 | |
|                      * - Simulation moves object to 7.9.
 | |
|                      * - PostTick.
 | |
|                      * - The first goalData is created for 7.9.
 | |
|                      * 
 | |
|                      *  In writing the theory checks out.
 | |
|                      *  Perhaps the solution could be simple as creating a goal
 | |
|                      *  during pretick if _preTickReceived is being set for
 | |
|                      *  the first time. Might need to reduce tick by 1
 | |
|                      *  when setting goalData for this; not sure yet.
 | |
|                      */
 | |
|                     _graphicalObject.SetPositionAndRotation(_startWorldPosition, Quaternion.identity);
 | |
|                     return;
 | |
|                 }
 | |
| 
 | |
|                 _graphicalObject.SetPositionAndRotation(_graphicalStartPosition, _graphicalStartRotation);
 | |
|                 CreateGoalData(_predictedObject.TimeManager.LocalTick, true);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         public void OnPreReplay(uint tick)
 | |
|         {
 | |
|             if (!_preTickReceived)
 | |
|             {
 | |
|                 if (CanSmooth())
 | |
|                 {
 | |
|                     if (_localTick - tick < _ignoredTicks)
 | |
|                         return;
 | |
| 
 | |
|                     CreateGoalData(tick, false);
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Called after a reconcile runs a replay.
 | |
|         /// </summary>
 | |
|         public void OnPostReplay(uint tick)
 | |
|         {
 | |
|             if (CanSmooth())
 | |
|             {
 | |
|                 if (_reconcileLocalTick == -1)
 | |
|                     return;
 | |
| 
 | |
|                 CreateGoalData(tick, false);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Returns if the graphics can be smoothed.
 | |
|         /// </summary>
 | |
|         /// <returns></returns>
 | |
|         private bool CanSmooth()
 | |
|         {
 | |
|             if (_interpolation == 0)
 | |
|                 return false;
 | |
|             if (_predictedObject.IsPredictingOwner() || _predictedObject.IsServer)
 | |
|                 return false;
 | |
| 
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Sets the last tick a reconcile occurred.
 | |
|         /// </summary>
 | |
|         /// <param name="value"></param>
 | |
|         public void SetLocalReconcileTick(long value)
 | |
|         {
 | |
|             _reconcileLocalTick = value;
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Caches a GoalData.
 | |
|         /// </summary>
 | |
|         /// <param name="gd"></param>
 | |
|         private void StoreGoalData(GoalData gd)
 | |
|         {
 | |
|             gd.Reset();
 | |
|             _goalDataCache.Push(gd);
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Returns if this transform matches arguments.
 | |
|         /// </summary>
 | |
|         /// <returns></returns>
 | |
|         private bool GraphicalObjectMatches(Vector3 localPosition, Quaternion localRotation)
 | |
|         {
 | |
|             bool positionMatches = (!_smoothPosition || _graphicalObject.position == localPosition);
 | |
|             bool rotationMatches = (!_smoothRotation || _graphicalObject.rotation == localRotation);
 | |
|             return (positionMatches && rotationMatches);
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Returns if there is any change between two datas.
 | |
|         /// </summary>
 | |
|         private bool HasChanged(TransformData a, TransformData b)
 | |
|         {
 | |
|             return (a.Position != b.Position) ||
 | |
|                 (a.Rotation != b.Rotation);
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Returns if the transform differs from td.
 | |
|         /// </summary>
 | |
|         private bool HasChanged(TransformData td)
 | |
|         {
 | |
|             Transform rigidbodyTransform;
 | |
| 
 | |
|             if (_rigidbodyType == RigidbodyType.Rigidbody)
 | |
|                 rigidbodyTransform = _rigidbody.transform;
 | |
|             else
 | |
|                 rigidbodyTransform = _rigidbody2d.transform;
 | |
| 
 | |
|             bool changed = (td.Position != rigidbodyTransform.position) || (td.Rotation != rigidbodyTransform.rotation);
 | |
| 
 | |
|             return changed;
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Sets CurrentGoalData to the next in queue.
 | |
|         /// </summary>
 | |
|         private void SetCurrentGoalData(bool afterMove)
 | |
|         {
 | |
|             if (_goalDatas.Count == 0)
 | |
|             {
 | |
|                 _currentGoalData.IsActive = false;
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 if (!afterMove && _goalDatas.Count < _interpolation)
 | |
|                     return;
 | |
| 
 | |
|                 //Update current to next.
 | |
|                 _currentGoalData.Update(_goalDatas[0]);
 | |
|                 //Store old and remove it.
 | |
|                 StoreGoalData(_goalDatas[0]);
 | |
|                 _goalDatas.RemoveAt(0);
 | |
|             }
 | |
|         }
 | |
| 
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Moves to a GoalData. Automatically determins if to use data from server or client.
 | |
|         /// </summary>
 | |
|         [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | |
|         private void MoveToTarget(float deltaOverride = -1f)
 | |
|         {
 | |
|             /* If the current goal data is not active then
 | |
|              * try to set a new one. If none are available
 | |
|              * it will remain inactive. */
 | |
|             if (!_currentGoalData.IsActive)
 | |
|             {
 | |
|                 SetCurrentGoalData(false);
 | |
|                 //If still inactive then it could not be updated.
 | |
|                 if (!_currentGoalData.IsActive)
 | |
|                     return;
 | |
|             }
 | |
| 
 | |
|             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. */
 | |
|             TransformData td = _currentGoalData.Transforms;
 | |
|             RateData rd = _currentGoalData.Rates;
 | |
| 
 | |
|             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. */
 | |
| 
 | |
|             float multiplier = 1f;
 | |
|             int countOverInterpolation = (queueCount - _interpolation);
 | |
|             if (countOverInterpolation > 0)
 | |
|                 multiplier += (OVERFLOW_MULTIPLIER * _excessBufferMultiplier);
 | |
|             else if (countOverInterpolation < 0 && queueCount > 1)
 | |
|                 multiplier -= OVERFLOW_MULTIPLIER;
 | |
| 
 | |
|             //Rate to update. Changes per property.
 | |
|             float rate;
 | |
|             Transform t = _graphicalObject;
 | |
| 
 | |
|             //Position.
 | |
|             if (_smoothPosition)
 | |
|             {
 | |
|                 rate = rd.Position;
 | |
|                 Vector3 posGoal = td.Position;
 | |
|                 if (rate == -1f)
 | |
|                     t.position = td.Position;
 | |
|                 else if (rate > 0f)
 | |
|                     t.position = Vector3.MoveTowards(t.position, posGoal, rate * delta * multiplier);
 | |
|             }
 | |
| 
 | |
|             //Rotation.
 | |
|             if (_smoothRotation)
 | |
|             {
 | |
|                 rate = rd.Rotation;
 | |
|                 if (rate == -1f)
 | |
|                     t.rotation = td.Rotation;
 | |
|                 else if (rate > 0f)
 | |
|                     t.rotation = Quaternion.RotateTowards(t.rotation, td.Rotation, rate * delta);
 | |
|             }
 | |
| 
 | |
|             //Subtract time remaining for movement to complete.
 | |
|             if (rd.TimeRemaining > 0f)
 | |
|             {
 | |
|                 float subtractionAmount = (delta * multiplier);
 | |
|                 float timeRemaining = rd.TimeRemaining - subtractionAmount;
 | |
|                 rd.TimeRemaining = timeRemaining;
 | |
|             }
 | |
| 
 | |
|             //If movement shoudl be complete.
 | |
|             if (rd.TimeRemaining <= 0f)
 | |
|             {
 | |
|                 float leftOver = Mathf.Abs(rd.TimeRemaining);
 | |
|                 //Set to next goal data if available.
 | |
|                 SetCurrentGoalData(true);
 | |
| 
 | |
|                 //New data was set.
 | |
|                 if (_currentGoalData.IsActive)
 | |
|                 {
 | |
|                     if (leftOver > 0f)
 | |
|                         MoveToTarget(leftOver);
 | |
|                 }
 | |
|                 //No more in buffer, see if can extrapolate.
 | |
|                 else
 | |
|                 {
 | |
|                     /* Everything should line up when
 | |
|                      * time remaining is <= 0f but incase it's not,
 | |
|                      * such as if the user manipulated the grapihc object
 | |
|                      * somehow, then set goaldata active again to continue
 | |
|                      * moving it until it lines up with the goal. */
 | |
|                     if (!GraphicalObjectMatches(td.Position, td.Rotation))
 | |
|                         _currentGoalData.IsActive = true;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         #region Rates.
 | |
|         /// <summary>
 | |
|         /// Sets move rates which will occur instantly.
 | |
|         /// </summary>
 | |
|         private void SetInstantRates(RateData rd)
 | |
|         {
 | |
|             rd.Update(-1f, -1f, 1, -1f);
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Sets move rates which will occur over time.
 | |
|         /// </summary>
 | |
|         [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | |
|         private void SetCalculatedRates(GoalData prevGoalData, GoalData nextGoalData, Channel channel)
 | |
|         {
 | |
|             /* Only update rates if data has changed.
 | |
|              * When data comes in reliably for eventual consistency
 | |
|              * it's possible that it will be the same as the last
 | |
|              * unreliable packet. When this happens no change has occurred
 | |
|              * and the distance of change woudl also be 0; this prevents
 | |
|              * the NT from moving. Only need to compare data if channel is reliable. */
 | |
|             TransformData nextTd = nextGoalData.Transforms;
 | |
|             if (channel == Channel.Reliable && HasChanged(prevGoalData.Transforms, nextTd))
 | |
|             {
 | |
|                 nextGoalData.Rates.Update(prevGoalData.Rates);
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             uint lastTick = prevGoalData.LocalTick;
 | |
|             /* How much time has passed between last update and current.
 | |
|              * If set to 0 then that means the transform has
 | |
|              * settled. */
 | |
|             if (lastTick == 0)
 | |
|                 lastTick = (nextGoalData.LocalTick - 1);
 | |
| 
 | |
|             uint tickDifference = (nextGoalData.LocalTick - lastTick);
 | |
|             float timePassed = (float)_predictedObject.TimeManager.TicksToTime(tickDifference);
 | |
|             RateData nextRd = nextGoalData.Rates;
 | |
| 
 | |
|             //Distance between properties.
 | |
|             float distance;
 | |
|             //Position.
 | |
|             Vector3 lastPosition = prevGoalData.Transforms.Position;
 | |
|             distance = Vector3.Distance(lastPosition, nextTd.Position);
 | |
|             //If distance teleports assume rest do.
 | |
|             if (_teleportThreshold >= 0f && distance >= _teleportThreshold)
 | |
|             {
 | |
|                 SetInstantRates(nextRd);
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             //Position distance already calculated.
 | |
|             float positionRate = (distance / timePassed);
 | |
|             //Rotation.
 | |
|             distance = prevGoalData.Transforms.Rotation.Angle(nextTd.Rotation, true);
 | |
|             float rotationRate = (distance / timePassed);
 | |
| 
 | |
|             /* If no speed then snap just in case.
 | |
|              * 0f could be from floating errors. */
 | |
|             if (positionRate == 0f)
 | |
|                 positionRate = -1f;
 | |
|             if (rotationRate == 0f)
 | |
|                 rotationRate = -1f;
 | |
| 
 | |
|             nextRd.Update(positionRate, rotationRate, tickDifference, timePassed);
 | |
|         }
 | |
|         #endregion       
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Creates a new goal data for tick. The result will be placed into the goalDatas queue at it's proper position.
 | |
|         /// </summary>
 | |
|         [MethodImpl(MethodImplOptions.AggressiveInlining)]
 | |
|         public void CreateGoalData(uint tick, bool postTick)
 | |
|         {
 | |
|             /* It's possible a removed entry would void further
 | |
|              * logic so remove excess entires first. */
 | |
| 
 | |
|             /* 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 should
 | |
|              * actually get unreasonably high but rather safe than sorry. */
 | |
|             int maximumBufferAllowance = (_interpolation * 8);
 | |
|             int removedBufferCount = (_goalDatas.Count - maximumBufferAllowance);
 | |
|             //If there are some to remove.
 | |
|             if (removedBufferCount > 0)
 | |
|             {
 | |
|                 for (int i = 0; i < removedBufferCount; i++)
 | |
|                     StoreGoalData(_goalDatas[0 + i]);
 | |
|                 _goalDatas.RemoveRange(0, removedBufferCount);
 | |
|             }
 | |
| 
 | |
|             uint currentGoalDataTick = _currentGoalData.LocalTick;
 | |
|             //Tick has already been interpolated past, no reason to process it.
 | |
|             if (tick <= currentGoalDataTick)
 | |
|                 return;
 | |
| 
 | |
|             //GoalData from previous calculation.
 | |
|             GoalData prevGoalData;
 | |
|             int datasCount = _goalDatas.Count;
 | |
|             /* Where to insert next data. This could have value
 | |
|              * somewhere in the middle of goalDatas if the tick
 | |
|              * is a replay rather than post tick. */
 | |
|             int injectionIndex = datasCount + 1;
 | |
|             //If being added at the end of a tick rather than from replay.
 | |
|             if (postTick)
 | |
|             {
 | |
|                 //Becomes true if transform differs from previous data.
 | |
|                 bool changed;
 | |
| 
 | |
|                 //If there is no goal data then create one using pretick data.
 | |
|                 if (datasCount == 0)
 | |
|                 {
 | |
|                     prevGoalData = MakeGoalDataFromPreTickTransform();
 | |
|                     changed = HasChanged(prevGoalData.Transforms);
 | |
|                 }
 | |
|                 //If there's goal datas grab the last, it will always be the tick before.
 | |
|                 else
 | |
|                 {
 | |
|                     prevGoalData = _goalDatas[datasCount - 1];
 | |
|                     /* If the tick is not exactly 1 past the last
 | |
|                      * then there's gaps in the saved values. This can
 | |
|                      * occur if the transform went idle and the buffer
 | |
|                      * hasn't emptied out yet. When this occurs use the
 | |
|                      * preTick data to calculate differences. */
 | |
|                     if (tick - prevGoalData.LocalTick != 1)
 | |
|                         prevGoalData = MakeGoalDataFromPreTickTransform();
 | |
| 
 | |
|                     changed = HasChanged(prevGoalData.Transforms);
 | |
|                 }
 | |
| 
 | |
|                 //Nothing has changed so no further action is required.
 | |
|                 if (!changed)
 | |
|                 {
 | |
|                     if (datasCount > 0 && prevGoalData != _goalDatas[datasCount - 1])
 | |
|                         StoreGoalData(prevGoalData);
 | |
|                     return;
 | |
|                 }
 | |
|             }
 | |
|             //Not post tick so it's from a replay.
 | |
|             else
 | |
|             {
 | |
|                 int prevIndex = -1;
 | |
|                 /* If the tick is 1 past current goalData
 | |
|                  * then it's the next in line for smoothing
 | |
|                  * from the current.
 | |
|                  * When this occurs use currentGoalData as
 | |
|                  * the previous. */
 | |
|                 if (tick == (currentGoalDataTick + 1))
 | |
|                 {
 | |
|                     prevGoalData = _currentGoalData;
 | |
|                     injectionIndex = 0;
 | |
|                 }
 | |
|                 //When not the next in line find out where to place data.
 | |
|                 else
 | |
|                 {
 | |
|                     if (tick > 0)
 | |
|                         prevGoalData = GetGoalData(tick - 1, out prevIndex);
 | |
|                     //Cannot find prevGoalData if tick is 0.
 | |
|                     else
 | |
|                         prevGoalData = null;
 | |
|                 }
 | |
| 
 | |
|                 //If previous goalData was found then inject just past the previous value.
 | |
|                 if (prevIndex != -1)
 | |
|                     injectionIndex = prevIndex + 1;
 | |
| 
 | |
|                 /* Should previous goalData be null then it could not be found.
 | |
|                  * Create a new previous goal data based on rigidbody state
 | |
|                  * during pretick. */
 | |
|                 if (prevGoalData == null)
 | |
|                 {
 | |
|                     //Create a goaldata based on information. If it differs from pretick then throw.
 | |
|                     GoalData gd = RetrieveGoalData();
 | |
|                     gd.Transforms.Update(_preTickTransformdata);
 | |
| 
 | |
|                     if (HasChanged(gd.Transforms))
 | |
|                     {
 | |
|                         prevGoalData = gd;
 | |
|                     }
 | |
|                     else
 | |
|                     {
 | |
|                         StoreGoalData(gd);
 | |
|                         return;
 | |
|                     }
 | |
|                 }
 | |
|                 /* Previous goal data is not active.
 | |
|                  * This should not be possible but this
 | |
|                  * is here as a sanity check anyway. */
 | |
|                 else if (!prevGoalData.IsActive)
 | |
|                 {
 | |
|                     return;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             //Begin building next goal data.
 | |
|             GoalData nextGoalData = RetrieveGoalData();
 | |
|             nextGoalData.LocalTick = tick;
 | |
|             //Set next transform data.
 | |
|             TransformData nextTd = nextGoalData.Transforms;
 | |
|             if (_rigidbodyType == RigidbodyType.Rigidbody)
 | |
|                 nextTd.Update(_rigidbody);
 | |
|             else
 | |
|                 nextTd.Update(_rigidbody2d);
 | |
| 
 | |
|             /* Reset properties if smoothing is not enabled
 | |
|              * for them. It's less checks and easier to do it
 | |
|              * after the nextGoalData is populated. */
 | |
|             if (!_smoothPosition)
 | |
|                 nextTd.Position = _graphicalStartPosition;
 | |
|             if (!_smoothRotation)
 | |
|                 nextTd.Rotation = _graphicalStartRotation;
 | |
| 
 | |
|             //Calculate rates for prev vs next data.
 | |
|             SetCalculatedRates(prevGoalData, nextGoalData, Channel.Unreliable);
 | |
|             /* If injectionIndex would place at the end
 | |
|              * then add. to goalDatas. */
 | |
|             if (injectionIndex >= _goalDatas.Count)
 | |
|                 _goalDatas.Add(nextGoalData);
 | |
|             //Otherwise insert into the proper location.
 | |
|             else
 | |
|                 _goalDatas[injectionIndex].Update(nextGoalData);
 | |
| 
 | |
|             //Makes previous goal data from transforms pretick values.
 | |
|             GoalData MakeGoalDataFromPreTickTransform()
 | |
|             {
 | |
|                 GoalData gd = RetrieveGoalData();
 | |
|                 //RigidbodyData contains the data from preTick.
 | |
|                 gd.Transforms.Update(_preTickTransformdata);
 | |
|                 //No need to update rates because this is just a starting point reference for interpolation.
 | |
|                 return gd;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Returns the GoalData at tick.
 | |
|         /// </summary>
 | |
|         /// <returns></returns>
 | |
|         private GoalData GetGoalData(uint tick, out int index)
 | |
|         {
 | |
|             index = -1;
 | |
|             if (tick == 0)
 | |
|                 return null;
 | |
| 
 | |
|             for (int i = 0; i < _goalDatas.Count; i++)
 | |
|             {
 | |
|                 if (_goalDatas[i].LocalTick == tick)
 | |
|                 {
 | |
|                     index = i;
 | |
|                     return _goalDatas[i];
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             //Not found.
 | |
|             return null;
 | |
|         }
 | |
| 
 | |
| 
 | |
|         /// <summary>
 | |
|         /// Returns a GoalData from the cache.
 | |
|         /// </summary>
 | |
|         /// <returns></returns>
 | |
|         private GoalData RetrieveGoalData()
 | |
|         {
 | |
|             GoalData result = (_goalDataCache.Count > 0) ? _goalDataCache.Pop() : new GoalData();
 | |
|             result.IsActive = true;
 | |
|             return result;
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
| 
 | |
| }
 |