885 lines
32 KiB
C#
885 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 uint _interpolation = 4;
|
|
/// <summary>
|
|
/// Sets the interpolation value to use when the owner of this object.
|
|
/// </summary>
|
|
/// <param name="value"></param>
|
|
public void SetInterpolation(uint value) => _interpolation = 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>
|
|
/// Cached localtick for performance.
|
|
/// </summary>
|
|
private uint _localTick;
|
|
/// <summary>
|
|
/// Number of ticks to ignore when replaying.
|
|
/// </summary>
|
|
private uint _ignoredTicks;
|
|
/// <summary>
|
|
/// Start position of the graphical object in world space.
|
|
/// </summary>
|
|
private Vector3 _startWorldPosition;
|
|
#endregion
|
|
|
|
#region Const.
|
|
/// <summary>
|
|
/// Multiplier to apply to movement speed when buffer is over interpolation.
|
|
/// </summary>
|
|
private const float OVERFLOW_MULTIPLIER = 0.1f;
|
|
/// <summary>
|
|
/// Multiplier to apply to movement speed when buffer is under interpolation.
|
|
/// </summary>
|
|
private const float UNDERFLOW_MULTIPLIER = 0.02f;
|
|
#endregion
|
|
|
|
public void SetIgnoredTicks(uint value) => _ignoredTicks = value;
|
|
/// <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;
|
|
int countOverInterpolation = (queueCount - (int)_interpolation);
|
|
if (countOverInterpolation > 0)
|
|
{
|
|
float overflowMultiplier = (!_predictedObject.IsOwner) ? OVERFLOW_MULTIPLIER : (OVERFLOW_MULTIPLIER * 1f);
|
|
multiplier = 1f + overflowMultiplier;
|
|
}
|
|
else if (countOverInterpolation < 0)
|
|
{
|
|
float value = (UNDERFLOW_MULTIPLIER * Mathf.Abs(countOverInterpolation));
|
|
const float maximum = 0.9f;
|
|
if (value > maximum)
|
|
value = maximum;
|
|
multiplier = 1f - value;
|
|
}
|
|
else
|
|
{
|
|
multiplier = 1f;
|
|
}
|
|
|
|
//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 = ((int)_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;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
}
|