using System; using FishNet.Managing; using FishNet.Managing.Timing; using FishNet.Object; using FishNet.Object.Prediction; using FishNet.Utility.Extension; using GameKit.Dependencies.Utilities; using UnityEngine; using UnityEngine.Scripting; namespace FishNet.Component.Transforming { /// /// This class is under regular development and it's API may change at any time. /// [Obsolete("This class will be removed in version 5.")] public sealed class TransformTickSmoother : IResettable { #region Types. private enum InitializeType { /// /// Not initialized. /// Unset, /// /// Initialized for network use. /// Networked, /// /// Initialized for non-network use. /// NonNetworked, } [Preserve] private struct TickTransformProperties { public uint Tick; public TransformProperties Properties; public TickTransformProperties(uint tick, Transform t) { Tick = tick; Properties = new(t.localPosition, t.localRotation, t.localScale); } public TickTransformProperties(uint tick, Transform t, Vector3 localScale) { Tick = tick; Properties = new(t.localPosition, t.localRotation, localScale); } public TickTransformProperties(uint tick, TransformProperties tp) { Tick = tick; Properties = tp; } public TickTransformProperties(uint tick, TransformProperties tp, Vector3 localScale) { Tick = tick; tp.Scale = localScale; Properties = tp; } } #endregion #region Private. /// /// Object to smooth. /// private Transform _graphicalObject; /// /// When not MoveRates.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.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.VeryLow; /// /// 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 _useOwnerSmoothing; /// /// True if Initialized has been called and settings have not been reset. /// private InitializeType _initializeType = InitializeType.Unset; /// /// Last tick this was teleported on. /// private uint _teleportedTick = TimeManager.UNSET_TICK; /// /// Last local tick a reconcile callback was received. /// private uint _lastReconcileTick = TimeManager.UNSET_TICK; /// /// Transform to track movement on. /// private Transform _rootTransform; /// /// Frame which smoothing can start. /// private int _startFrame; #endregion #region Const. /// /// Default expected interval for reconciles. /// private const int RECONCILE_INTERVAL_DEFAULT = int.MaxValue; /// /// Maximum allowed entries to be queued over the interpolation amount. /// private const int MAXIMUM_QUEUED_OVER_INTERPOLATION = 3; #endregion [Preserve] public TransformTickSmoother() { } ~TransformTickSmoother() { //This is a last resort for if something didnt deinitialize right. ResetState(); } /// /// Initializes this smoother for owner and spectator. This should only occur once. /// public void InitializeNetworked(NetworkObject nob, Transform graphicalObject, bool detach, float teleportDistance, float tickDelta, byte ownerInterpolation, TransformPropertiesFlag ownerSmoothedProperties, byte spectatorInterpolation, TransformPropertiesFlag specatorSmoothedProperties, AdaptiveInterpolationType adaptiveInterpolation) { ResetState(); _networkObject = nob; _spectatorInterpolation = spectatorInterpolation; _spectatorSmoothedProperties = specatorSmoothedProperties; Initialize_Internal(nob.transform, graphicalObject, detach, teleportDistance, tickDelta, ownerInterpolation, ownerSmoothedProperties, forNetworked: true); SetAdaptiveInterpolation(adaptiveInterpolation); UpdateInterpolation(0); } private void Initialize_Internal(Transform rootTransform, Transform graphicalObject, bool detach, float teleportDistance, float tickDelta, byte ownerInterpolation, TransformPropertiesFlag ownerSmoothedProperties, bool forNetworked) { _rootTransform = rootTransform; _detach = detach; _transformProperties = CollectionCaches.RetrieveBasicQueue(); _gfxInitializedOffsetValues = rootTransform.GetTransformOffsets(graphicalObject); _tickDelta = tickDelta; _graphicalObject = graphicalObject; _teleportThreshold = teleportDistance; _ownerInterpolation = ownerInterpolation; _ownerSmoothedProperties = ownerSmoothedProperties; _initializeType = (forNetworked) ? InitializeType.Networked : InitializeType.NonNetworked; } /// /// Initializes this smoother non-networked use. /// public void Initialize(Transform rootTransform, Transform graphicalObject, bool detach, float teleportDistance, float tickDelta, byte ownerInterpolation, TransformPropertiesFlag ownerSmoothedProperties) { ResetState(); Initialize_Internal(rootTransform, graphicalObject, detach, teleportDistance, tickDelta, ownerInterpolation, ownerSmoothedProperties, false); SetAdaptiveInterpolation(AdaptiveInterpolationType.Off); } /// /// Deinitializes this smoother resetting values. /// public void Deinitialize() { ResetState(); } /// /// Updates interpolation based on localClient latency. /// private void UpdateInterpolation(uint clientStateTick) { /* Use owner interpolation if: * - Owner. * - Server, since server always interpolates over 1 tick; this only applies as clientHost. * - Not networked, use specified 'owner' interpolation. */ if (_initializeType == InitializeType.NonNetworked || _networkObject.IsServerInitialized || _networkObject.Owner.IsLocalClient) { _interpolation = _ownerInterpolation; } //Not using owner interpolation. 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); } interpolation *= GetInterpolationMultiplier(); interpolation = Mathf.Clamp(interpolation, 2f, (float)byte.MaxValue); _interpolation = (byte)Mathf.CeilToInt(interpolation); float GetInterpolationMultiplier() { switch (_adaptiveInterpolation) { case AdaptiveInterpolationType.ExtremelyLow: return 0.2f; case AdaptiveInterpolationType.VeryLow: return 0.45f; case AdaptiveInterpolationType.Low: return 0.8f; case AdaptiveInterpolationType.Moderate: return 1.05f; case AdaptiveInterpolationType.High: return 1.25f; case AdaptiveInterpolationType.VeryHigh: return 1.5f; //Make no changes for maximum. default: _networkObject.NetworkManager.LogError($"AdaptiveInterpolationType {_adaptiveInterpolation} is unhandled."); return 1f; } } } } } 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 OnUpdate() { if (!CanSmooth()) return; MoveToTarget(Time.deltaTime); } /// /// Called when the TimeManager invokes OnPreTick. /// public void OnPreTick() { if (!CanSmooth()) return; _preTicked = true; _useOwnerSmoothing = (_networkObject == null || _networkObject.IsOwner); DiscardExcessiveTransformPropertiesQueue(); //These only need to be set if still attached. if (!_detach) _gfxPreSimulateWorldValues = _graphicalObject.GetWorldProperties(); } /// /// Called when the PredictionManager invokes OnPreReconcile. /// public void OnPreReconcile() { if (!_networkObject.IsObjectReconciling) return; if (_networkObject.IsOwner || _adaptiveInterpolation == AdaptiveInterpolationType.Off) return; uint clientStateTick = _networkObject.PredictionManager.ClientStateTick; _lastReconcileTick = clientStateTick; UpdateInterpolation(clientStateTick); } /// /// Called when the TimeManager invokes OnPostReplay. /// /// Replay tick for the local client. public void OnPostReplicateReplay(uint clientTick) { if (_networkObject.IsOwner || _adaptiveInterpolation == AdaptiveInterpolationType.Off) return; if (_transformProperties.Count == 0) return; if (clientTick <= _teleportedTick) 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 (clientTick <= _teleportedTick) return; //If preticked then previous transform values are known. if (_preTicked) { DiscardExcessiveTransformPropertiesQueue(); //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()); } } /// /// Teleports the graphical to it's starting position and clears the internal movement queue. /// public void Teleport() { if (_networkObject == null) return; _teleportedTick = _networkObject.TimeManager.LocalTick; ClearTransformPropertiesQueue(); TransformProperties startProperties = _networkObject.transform.GetWorldProperties(); startProperties.Add(_gfxInitializedOffsetValues); _graphicalObject.SetWorldProperties(startProperties); } /// /// Clears the pending movement queue. /// private void ClearTransformPropertiesQueue() { _transformProperties.Clear(); //Also unset move rates since there is no more queue. _moveRates = new(MoveRates.UNSET_VALUE); } /// /// Discards datas over interpolation limit from movement queue. /// private void DiscardExcessiveTransformPropertiesQueue() { int propertiesCount = _transformProperties.Count; int dequeueCount = (propertiesCount - (_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(); SetMoveRates(tpp.Properties); } } /// /// Adds a new transform properties and sets move rates if needed. /// private void AddTransformProperties(uint tick) { TickTransformProperties tpp = new(tick, GetNetworkObjectWorldPropertiesWithOffset()); _transformProperties.Enqueue(tpp); //If first entry then set move rates. if (_transformProperties.Count == 1) { TransformProperties gfxWorldProperties = _graphicalObject.GetWorldProperties(); SetMoveRates(gfxWorldProperties); _startFrame = Time.frameCount + 1; } } /// /// 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) { if (tick != _transformProperties[index].Tick) { //Should not be possible. } else { _transformProperties[index] = new(tick, GetNetworkObjectWorldPropertiesWithOffset(), _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; if (_networkObject != null && _networkObject.EnablePrediction && !_networkObject.EnableStateForwarding && !_networkObject.IsController) return false; if (_networkObject.IsServerOnlyStarted) return false; return true; } /// /// Sets new rates based on next entries in transformProperties queue, against a supplied TransformProperties. /// private void SetMoveRates(in TransformProperties prevValues) { if (_transformProperties.Count == 0) { _moveRates = new(MoveRates.UNSET_VALUE); return; } TransformProperties nextValues = _transformProperties.Peek().Properties; float duration = _tickDelta; 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 != 0) { _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 = 1f; } _movementMultiplier = Mathf.Clamp(_movementMultiplier, 0.95f, 1.05f); } /// /// Moves transform to target values. /// private void MoveToTarget(float delta) { if (Time.frameCount < _startFrame) return; 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 = (_useOwnerSmoothing) ? _ownerSmoothedProperties : _spectatorSmoothedProperties; _moveRates.Move(_graphicalObject, ttp.Properties, smoothedProperties, (delta * _movementMultiplier), useWorldSpace: true); 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) { SetMoveRates(ttp.Properties); //If delta is negative then call move again with abs. if (tRemaining < 0f) MoveToTarget(Mathf.Abs(tRemaining)); } //No remaining, set to snap. else { ClearTransformPropertiesQueue(); } } } public void ResetState() { if (_initializeType == InitializeType.Unset) return; if (_graphicalObject != null) { if (_rootTransform != null) { //Check isQuitting for UnityEditor fix //https://github.com/FirstGearGames/FishNet/issues/818 if (_detach && !ApplicationState.IsQuitting()) _graphicalObject.SetParent(_rootTransform); _graphicalObject.SetWorldProperties(GetNetworkObjectWorldPropertiesWithOffset()); _graphicalObject = null; } else if (_detach) { UnityEngine.Object.Destroy(_graphicalObject.gameObject); } } _networkObject = null; _teleportedTick = TimeManager.UNSET_TICK; _lastReconcileTick = TimeManager.UNSET_TICK; _movementMultiplier = 1f; CollectionCaches.StoreAndDefault(ref _transformProperties); _teleportThreshold = default; _moveRates = default; _preTicked = default; _gfxInitializedOffsetValues = default; _gfxPreSimulateWorldValues = default; _tickDelta = default; _interpolation = default; } public void InitializeState() { } } }