#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