Files
PrinceOfGlory/Packages/com.firstgeargames.fishnet/Runtime/Object/Prediction/AdaptiveInterpolationSmootherFixed.cs
2026-03-03 03:15:46 +08:00

590 lines
22 KiB
C#

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.
/// <summary>
/// Data on a goal to move towards.
/// </summary>
private class GoalData : IResettable
{
/// <summary>
/// True if this GoalData is valid.
/// </summary>
public bool IsValid;
/// <summary>
/// Tick of the data this GoalData is for.
/// </summary>
public uint DataTick;
/// <summary>
/// Transform values to move towards.
/// </summary>
public TransformPropertiesCls TransformProperties = new TransformPropertiesCls();
/// <summary>
/// Time remaining to move towards goal.
/// </summary>
public float TimeRemaining;
public GoalData() { }
public void InitializeState() { }
public void ResetState()
{
DataTick = 0;
TimeRemaining = 0f;
TransformProperties.ResetState();
IsValid = false;
}
/// <summary>
/// Updates values using a GoalData.
/// </summary>
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.
/// <summary>
/// Offsets of the root object during PreTick or PreReplicateReplay.
/// </summary>
private TransformProperties _rootPreSimulateWorldValues;
/// <summary>
/// Offsets of the graphical object during PreTick or PreReplicateReplay.
/// </summary>
private TransformProperties _graphicalPreSimulateWorldValues;
/// <summary>
/// SmoothingData to use.
/// </summary>
private AdaptiveInterpolationSmoothingData _smoothingData;
/// <summary>
/// Current interpolation value. This changes based on ping and settings.
/// </summary>
public long _currentInterpolation = 15;
/// <summary>
/// Current GoalData being used.
/// </summary>
private GoalData _currentGoalData = new GoalData();
/// <summary>
/// MoveRates for currentGoalData.
/// </summary>
private MoveRates _currentMoveRates;
/// <summary>
/// GoalDatas to move towards.
/// </summary>
//private RingBuffer<GoalData> _goalDatas = new RingBuffer<GoalData>();
private List<GoalData> _goalDatas = new List<GoalData>();
/// <summary>
/// Cached NetworkObject reference in SmoothingData for performance.
/// </summary>
private NetworkObject _networkObject;
/// <summary>
/// Cached tickDelta on the TimeManager.
/// </summary>
private float _tickDelta;
/// <summary>
/// Multiplier to apply towards movements. This is used to speed up and slow down buffer as needed.
/// </summary>
private float _rateMultiplier = 1f;
#endregion
#region Const.
/// <summary>
/// Multiplier to apply to movement speed when buffer is over interpolation.
/// </summary>
private const float OVERFLOW_MULTIPLIER = 10f;
/// <summary>
/// Multiplier to apply to movement speed when buffer is under interpolation.
/// </summary>
private const float UNDERFLOW_MULTIPLIER = 1f;
#endregion
public AdaptiveInterpolationSmootherFixed()
{
/* Initialize for up to 50
* goal datas. Anything beyond that
* is unreasonable. */
//_goalDatas.Initialize(50);
}
/// <summary>
/// Initializes this for use.
/// </summary>
internal void Initialize(AdaptiveInterpolationSmoothingData data)
{
_smoothingData = data;
_networkObject = data.NetworkObject;
_tickDelta = (float)_networkObject.TimeManager.TickDelta;
SetGraphicalObject(data.GraphicalObject);
}
/// <summary>
/// <summary>
/// Called every frame.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Update()
{
if (CanSmooth())
MoveToTarget();
}
/// <summary>
/// Called when the TimeManager invokes OnPreTick.
/// </summary>
public void OnPreTick()
{
if (CanSmooth())
{
_graphicalPreSimulateWorldValues = _smoothingData.GraphicalObject.GetWorldProperties();
_rootPreSimulateWorldValues.Update(_networkObject.transform);
}
}
/// <summary>
/// Called when the TimeManager invokes OnPostTick.
/// </summary>
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);
}
}
/// <summary>
/// Called before a reconcile runs a replay.
/// </summary>
public void OnPreReplicateReplay(uint clientTick, uint serverTick)
{
//Update the last post simulate data.
if (CanSmooth())
_rootPreSimulateWorldValues.Update(_networkObject.transform);
}
/// <summary>
/// Called after a reconcile runs a replay.
/// </summary>
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}");
}
}
/// <summary>
/// Sets GraphicalObject.
/// </summary>
/// <param name="value"></param>
public void SetGraphicalObject(Transform value)
{
_smoothingData.GraphicalObject = value;
_graphicalPreSimulateWorldValues.Update(value);
}
/// <summary>
/// Returns if the graphics can be smoothed.
/// </summary>
/// <returns></returns>
private bool CanSmooth()
{
if (_networkObject.IsOwner)
return false;
if (_networkObject.IsServerOnly || _networkObject.IsHost)
return false;
return true;
}
/// <summary>
/// Returns if this transform matches arguments.
/// </summary>
/// <returns></returns>
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);
}
/// <summary>
/// Sets CurrentGoalData to the next in queue. Returns if was set successfully.
/// </summary>
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<GoalData>.Store(prev);
//Assign new current.
_currentGoalData = next;
Debug.LogWarning($"Set CurrentGoalData on tick {_currentGoalData.DataTick}, remaining {_goalDatas.Count}");
return true;
}
}
/// <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 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.
/// <summary>
/// Sets move rates which will occur over time.
/// </summary>
[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
/// <summary>
/// Removes GoalDatas which make the queue excessive.
/// This could cause teleportation but would rarely occur, only potentially during sever network issues.
/// </summary>
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<GoalData>.Store(_goalDatas[0 + i]);
// //_goalDatas.RemoveRange(true, removedBufferCount);
// _goalDatas.RemoveRange(0, removedBufferCount);
//}
}
/// <summary>
/// Creates a GoalData after a simulate.
/// </summary>
/// <param name="postTick">True if being created for OnPostTick.</param>
[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<GoalData>.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
}
}