#if !PREDICTION_1
using FishNet.Managing;
using FishNet.Managing.Timing;
using FishNet.Utility.Extension;
using GameKit.Dependencies.Utilities;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace FishNet.Object.Prediction
{
///
/// This class is under regular development and it's API may change at any time.
///
public sealed class ChildTransformTickSmoother : IResettable
{
#region Types.
private struct TickTransformProperties
{
public uint Tick;
public TransformProperties Properties;
public TickTransformProperties(uint tick, Transform t)
{
Tick = tick;
Properties = new TransformProperties(t.localPosition, t.localRotation, t.localScale);
}
public TickTransformProperties(uint tick, Transform t, Vector3 localScale)
{
Tick = tick;
Properties = new TransformProperties(t.localPosition, t.localRotation, localScale);
}
public TickTransformProperties(uint tick, TransformProperties tp)
{
Tick = tick;
Properties = tp;
}
}
#endregion
#region Private.
///
/// Object to smooth.
///
private Transform _graphicalObject;
///
/// When not MoveRatesCls.UNSET_VALUE the graphical object will teleport into it's next position if the move distance exceeds this value.
///
private float _teleportThreshold;
///
/// How quickly to move towards goal values.
///
private MoveRates _moveRates = new MoveRates(MoveRatesCls.UNSET_VALUE);
///
/// True if a pretick occurred since last postTick.
///
private bool _preTicked;
///
/// World offset values of the graphical from the NetworkObject during initialization.
///
private TransformProperties _gfxInitializedOffsetValues;
///
/// World values of the graphical after it's been aligned to initialized values in PreTick.
///
private TransformProperties _gfxPreSimulateWorldValues;
///
/// TickDelta on the TimeManager.
///
private float _tickDelta;
///
/// How many ticks to interpolate over when not using adaptive.
///
private byte _ownerInterpolation;
///
/// Current interpolation, regardless of if using adaptive or not.
///
private byte _interpolation;
///
/// NetworkObject this is for.
///
private NetworkObject _networkObject;
///
/// Value to multiply movement by. This is used to reduce or increase the rate the movement buffer is consumed.
///
private float _movementMultiplier = 1f;
///
/// TransformProperties to move towards.
///
private BasicQueue _transformProperties;
///
/// Which properties to smooth.
///
private TransformPropertiesFlag _ownerSmoothedProperties;
///
/// Which properties to smooth.
///
private TransformPropertiesFlag _spectatorSmoothedProperties;
///
/// Updates the smoothedProperties value.
///
/// New value.
/// True if updating values for the spectator, false if updating for owner.
public void SetSmoothedProperties(TransformPropertiesFlag value, bool forSpectator)
{
if (forSpectator)
_spectatorSmoothedProperties = value;
else
_ownerSmoothedProperties = value;
}
///
/// Amount of adaptive interpolation to use.
///
private AdaptiveInterpolationType _adaptiveInterpolation = AdaptiveInterpolationType.Low;
///
/// Updates the adaptiveInterpolation value.
///
/// New value.
public void SetAdaptiveInterpolation(AdaptiveInterpolationType adaptiveInterpolation)
{
_adaptiveInterpolation = adaptiveInterpolation;
}
///
/// Set interpolation to use for spectated objects if adaptiveInterpolation is off.
///
private byte _spectatorInterpolation;
///
/// Sets the spectator interpolation value.
///
/// New value.
/// True to also disable adaptive interpolation to use this new value.
public void SetSpectatorInterpolation(byte value, bool disableAdaptiveInterpolation = true)
{
_spectatorInterpolation = value;
if (disableAdaptiveInterpolation)
_adaptiveInterpolation = AdaptiveInterpolationType.Off;
}
///
/// Previous parent the graphical was attached to.
///
private Transform _previousParent;
///
/// True if to detach at runtime.
///
private bool _detach;
///
/// True if were an owner of the NetworkObject during PreTick.
/// This is only used for performance gains.
///
private bool _ownerOnPretick;
///
/// True if adaptive interpolation should be used.
///
private bool _useAdaptiveInterpolation => (!_ownerOnPretick && _adaptiveInterpolation != AdaptiveInterpolationType.Off);
///
/// True if Initialized has been called and settings have not been reset.
///
private bool _initialized;
#endregion
#region Const.
///
/// Maximum allowed entries to be queued over the interpolation amount.
///
private int MAXIMUM_QUEUED_OVER_INTERPOLATION = 3;
#endregion
~ChildTransformTickSmoother()
{
//This is a last resort for if something didnt deinitialize right.
ResetState();
}
///
/// Initializes this smoother; should only be completed once.
///
public void Initialize(NetworkObject nob, Transform graphicalObject, bool detach, float teleportDistance, float tickDelta, byte ownerInterpolation, TransformPropertiesFlag ownerSmoothedProperties, byte spectatorInterpolation, TransformPropertiesFlag specatorSmoothedProperties, AdaptiveInterpolationType adaptiveInterpolation)
{
ResetState();
_detach = detach;
_networkObject = nob;
_transformProperties = CollectionCaches.RetrieveBasicQueue();
_gfxInitializedOffsetValues = nob.transform.GetTransformOffsets(graphicalObject);
_tickDelta = tickDelta;
_graphicalObject = graphicalObject;
_teleportThreshold = teleportDistance;
_ownerInterpolation = ownerInterpolation;
_spectatorInterpolation = spectatorInterpolation;
_ownerSmoothedProperties = ownerSmoothedProperties;
_spectatorSmoothedProperties = specatorSmoothedProperties;
SetAdaptiveInterpolation(adaptiveInterpolation);
UpdateInterpolation(0);
_initialized = true;
}
///
/// Deinitializes this smoother resetting values.
///
public void Deinitialize()
{
ResetState();
}
///
/// Updates interpolation based on localClient latency.
///
private void UpdateInterpolation(uint clientStateTick)
{
if (_networkObject.IsServerStarted || _networkObject.IsOwner)
{
_interpolation = _ownerInterpolation;
}
else
{
if (_adaptiveInterpolation == AdaptiveInterpolationType.Off)
{
_interpolation = _spectatorInterpolation;
}
else
{
float interpolation;
TimeManager tm = _networkObject.TimeManager;
if (clientStateTick == 0)
{
//Not enough data to calculate; guestimate. This should only happen once.
float fRtt = (float)tm.RoundTripTime;
interpolation = (fRtt / 10f);
}
else
{
interpolation = (tm.LocalTick - clientStateTick) + _networkObject.PredictionManager.StateInterpolation;
}
switch (_adaptiveInterpolation)
{
case AdaptiveInterpolationType.VeryLow:
interpolation *= 0.25f;
break;
case AdaptiveInterpolationType.Low:
interpolation *= 0.375f;
break;
case AdaptiveInterpolationType.Medium:
interpolation *= 0.5f;
break;
case AdaptiveInterpolationType.High:
interpolation *= 0.75f;
break;
//Make no changes for maximum.
}
interpolation = Mathf.Clamp(interpolation, 1f, (float)byte.MaxValue);
_interpolation = (byte)Mathf.RoundToInt(interpolation);
}
}
}
internal void OnStartClient()
{
if (!_detach)
return;
_previousParent = _graphicalObject.parent;
TransformProperties gfxWorldProperties = _graphicalObject.GetWorldProperties();
_graphicalObject.SetParent(null);
_graphicalObject.SetWorldProperties(gfxWorldProperties);
}
internal void OnStopClient()
{
if (!_detach || _previousParent == null || _graphicalObject == null)
return;
_graphicalObject.SetParent(_previousParent);
_graphicalObject.SetWorldProperties(GetNetworkObjectWorldPropertiesWithOffset());
}
///
/// Called every frame.
///
internal void Update()
{
if (!CanSmooth())
return;
if (_useAdaptiveInterpolation)
AdaptiveMoveToTarget(Time.deltaTime);
else
BasicMoveToTarget(Time.deltaTime);
}
///
/// Called when the TimeManager invokes OnPreTick.
///
public void OnPreTick()
{
if (!CanSmooth())
return;
_preTicked = true;
_ownerOnPretick = _networkObject.IsOwner;
if (_useAdaptiveInterpolation)
DiscardExcessiveTransformPropertiesQueue();
else
ClearTransformPropertiesQueue();
//These only need to be set if still attached.
if (!_detach)
_gfxPreSimulateWorldValues = _graphicalObject.GetWorldProperties();
}
///
/// Called when the PredictionManager invokes OnPreReconcile.
///
public void OnPreReconcile()
{
UpdateInterpolation(_networkObject.PredictionManager.ClientStateTick);
}
///
/// Called when the TimeManager invokes OnPostReplay.
///
/// Replay tick for the local client.
public void OnPostReplay(uint clientTick)
{
if (_transformProperties.Count == 0)
return;
if (!_useAdaptiveInterpolation)
return;
uint firstTick = _transformProperties.Peek().Tick;
//Already in motion to first entry, or first entry passed tick.
if (clientTick <= firstTick)
return;
ModifyTransformProperties(clientTick, firstTick);
}
///
/// Called when TimeManager invokes OnPostTick.
///
/// Local tick of the client.
public void OnPostTick(uint clientTick)
{
if (!CanSmooth())
return;
//If preticked then previous transform values are known.
if (_preTicked)
{
if (_useAdaptiveInterpolation)
DiscardExcessiveTransformPropertiesQueue();
else
ClearTransformPropertiesQueue();
//Only needs to be put to pretick position if not detached.
if (!_detach)
_graphicalObject.SetWorldProperties(_gfxPreSimulateWorldValues);
AddTransformProperties(clientTick);
}
//If did not pretick then the only thing we can do is snap to instantiated values.
else
{
//Only set to position if not to detach.
if (!_detach)
_graphicalObject.SetWorldProperties(GetNetworkObjectWorldPropertiesWithOffset());
}
}
///
/// Clears the pending movement queue.
///
private void ClearTransformPropertiesQueue()
{
_transformProperties.Clear();
//Also unset move rates since there is no more queue.
_moveRates = new MoveRates(MoveRatesCls.UNSET_VALUE);
}
///
/// Discards datas over interpolation limit from movement queue.
///
private void DiscardExcessiveTransformPropertiesQueue()
{
if (!_useAdaptiveInterpolation)
{
_networkObject.NetworkManager.LogError($"This method should only be called when using adaptive interpolation.");
return;
}
int dequeueCount = (_transformProperties.Count - (_interpolation + MAXIMUM_QUEUED_OVER_INTERPOLATION));
//If there are entries to dequeue.
if (dequeueCount > 0)
{
TickTransformProperties tpp = default;
for (int i = 0; i < dequeueCount; i++)
tpp = _transformProperties.Dequeue();
SetAdaptiveMoveRates(tpp.Properties, _transformProperties[0].Properties);
}
}
///
/// Adds a new transform properties and sets move rates if needed.
///
private void AddTransformProperties(uint tick)
{
TickTransformProperties tpp = new TickTransformProperties(tick, GetNetworkObjectWorldPropertiesWithOffset());
_transformProperties.Enqueue(tpp);
//If first entry then set move rates.
if (_transformProperties.Count == 1)
{
TransformProperties gfxWorldProperties = _graphicalObject.GetWorldProperties();
if (_useAdaptiveInterpolation)
SetAdaptiveMoveRates(gfxWorldProperties, tpp.Properties);
else
SetBasicMoveRates(gfxWorldProperties, tpp.Properties);
}
}
///
/// Modifies a transform property for a tick. This does not error check for empty collections.
///
/// First tick in the queue. If 0 this will be looked up.
private void ModifyTransformProperties(uint clientTick, uint firstTick)
{
uint tick = clientTick;
/*Ticks will always be added incremental by 1 so it's safe to jump ahead the difference
* of tick and firstTick. */
int index = (int)(tick - firstTick);
//Replace with new data.
if (index < _transformProperties.Count)
{
_transformProperties[index] = new TickTransformProperties(tick, _networkObject.transform, _graphicalObject.localScale);
}
else
{
//This should never happen.
}
}
///
/// Returns TransformProperties of the NetworkObject with the graphicals world offset.
///
///
private TransformProperties GetNetworkObjectWorldPropertiesWithOffset() => _networkObject.transform.GetWorldProperties(_gfxInitializedOffsetValues);
///
/// Returns if prediction can be used on this rigidbody.
///
///
private bool CanSmooth()
{
if (_graphicalObject == null)
return false;
return true;
}
///
/// Sets Position and Rotation move rates to reach Target datas.
///
private void SetBasicMoveRates(TransformProperties prevValues, TransformProperties nextValues)
{
byte interpolation = _interpolation;
float duration = (_tickDelta * interpolation);
/* If interpolation is 1 then add on a tiny amount
* of more time to compensate for frame time, so that
* the smoothing does not complete before the next tick,
* as this would result in jitter. */
if (interpolation == 1)
duration += (1 / 55f);
float teleportT = (_teleportThreshold * (float)interpolation);
_moveRates = MoveRates.GetMoveRates(prevValues, nextValues, duration, teleportT);
_moveRates.TimeRemaining = duration;
}
///
/// Sets Position and Rotation move rates to reach Target datas.
///
private void SetAdaptiveMoveRates(TransformProperties prevValues, TransformProperties nextValues)
{
float duration = _tickDelta;
/* If interpolation is 1 then add on a tiny amount
* of more time to compensate for frame time, so that
* the smoothing does not complete before the next tick,
* as this would result in jitter. */
float teleportT = _teleportThreshold;
_moveRates = MoveRates.GetMoveRates(prevValues, nextValues, duration, teleportT);
_moveRates.TimeRemaining = duration;
SetMovementMultiplier();
}
private void SetMovementMultiplier()
{
/* If there's more in queue than interpolation then begin to move faster based on overage.
* Move 5% faster for every overage. */
int overInterpolation = (_transformProperties.Count - _interpolation);
//If needs to be adjusted.
if (overInterpolation != 0f)
{
_movementMultiplier += (0.015f * overInterpolation);
}
//If does not need to be adjusted.
else
{
//If interpolation is 1 then slow down just barely to accomodate for frame delta variance.
if (_interpolation == 1)
_movementMultiplier = 0.99f;
}
_movementMultiplier = Mathf.Clamp(_movementMultiplier, 0.95f, 1.05f);
}
///
/// Moves transform to target values.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void BasicMoveToTarget(float delta)
{
int tpCount = _transformProperties.Count;
//No data.
if (tpCount == 0)
return;
TickTransformProperties ttp = _transformProperties.Peek();
_moveRates.MoveWorldToTarget(_graphicalObject, ttp.Properties, delta);
//if TimeLeft is <= 0f then transform should be at goal.
if (_moveRates.TimeRemaining <= 0f)
ClearTransformPropertiesQueue();
}
///
/// Moves transform to target values.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void AdaptiveMoveToTarget(float delta)
{
int tpCount = _transformProperties.Count;
//No data.
if (tpCount == 0)
return;
/* If buffer is considerably under goal then halt
* movement. This will allow the buffer to grow. */
if ((tpCount - _interpolation) < -4)
return;
TickTransformProperties ttp = _transformProperties.Peek();
TransformPropertiesFlag smoothedProperties = (_ownerOnPretick) ? _ownerSmoothedProperties : _spectatorSmoothedProperties;
_moveRates.MoveWorldToTarget(_graphicalObject, ttp.Properties, smoothedProperties, (delta * _movementMultiplier));
float tRemaining = _moveRates.TimeRemaining;
//if TimeLeft is <= 0f then transform is at goal. Grab a new goal if possible.
if (tRemaining <= 0f)
{
//Dequeue current entry and if there's another call a move on it.
_transformProperties.Dequeue();
//If there are entries left then setup for the next.
if (_transformProperties.Count > 0)
{
SetAdaptiveMoveRates(ttp.Properties, _transformProperties.Peek().Properties);
//If delta is negative then call move again with abs.
if (tRemaining < 0f)
AdaptiveMoveToTarget(Mathf.Abs(tRemaining));
}
//No remaining, set to snap.
else
{
ClearTransformPropertiesQueue();
}
}
}
public void ResetState()
{
if (!_initialized)
return;
_networkObject = null;
if (_graphicalObject != null)
{
if (_networkObject != null)
{
if (_detach)
_graphicalObject.SetParent(_networkObject.transform);
_graphicalObject.SetWorldProperties(GetNetworkObjectWorldPropertiesWithOffset());
_graphicalObject = null;
}
else if (_detach)
{
MonoBehaviour.Destroy(_graphicalObject.gameObject);
}
}
_movementMultiplier = 1f;
CollectionCaches.StoreAndDefault(ref _transformProperties);
_teleportThreshold = default;
_moveRates = default;
_preTicked = default;
_gfxInitializedOffsetValues = default;
_gfxPreSimulateWorldValues = default;
_tickDelta = default;
_interpolation = default;
}
public void InitializeState() { }
}
}
#endif