using FishNet.Managing; using FishNet.Managing.Logging; using FishNet.Managing.Timing; using FishNet.Object; using FishNet.Object.Prediction; using FishNet.Utility.Extension; using GameKit.Dependencies.Utilities; using UnityEngine; namespace FishNet.Component.Transforming { /// /// Detaches the object which this component resides and follows another. /// public class DetachableNetworkTickSmoother : NetworkBehaviour { #region Serialized. /// /// True to attach the object to it's original parent when OnStopClient is called. /// [Tooltip("True to attach the object to it's original parent when OnStopClient is called.")] [SerializeField] private bool _attachOnStop = true; /// /// Object to follow, and smooth towards. /// [Tooltip("Object to follow, and smooth towards.")] [SerializeField] private Transform _followObject; /// /// How many ticks to interpolate over. /// [Tooltip("How many ticks to interpolate over.")] [Range(1, byte.MaxValue)] [SerializeField] private byte _interpolation = 1; /// /// True to enable teleport threshhold. /// [Tooltip("True to enable teleport threshold.")] [SerializeField] private bool _enableTeleport; /// /// How far the object must move between ticks to teleport rather than smooth. /// [Tooltip("How far the object must move between ticks to teleport rather than smooth.")] [Range(0f, ushort.MaxValue)] [SerializeField] private float _teleportThreshold; /// /// True to synchronize the position of the followObject. /// [Tooltip("True to synchronize the position of the followObject.")] [SerializeField] private bool _synchronizePosition = true; /// /// True to synchronize the rotation of the followObject. /// [Tooltip("True to synchronize the rotation of the followObject.")] [SerializeField] private bool _synchronizeRotation; /// /// True to synchronize the scale of the followObject. /// [Tooltip("True to synchronize the scale of the followObject.")] [SerializeField] private bool _synchronizeScale; #endregion #region Private. /// /// TimeManager subscribed to. /// private TimeManager _timeManager; /// /// Parent of the object prior to detaching. /// private Transform _parent; /// /// Local properties of the graphical during instantation. /// private TransformProperties _transformInstantiatedLocalProperties; /// /// World properties of the followObject during post tick. /// private TransformProperties _postTickFollowObjectWorldProperties; /// /// How quickly to move towards target. /// private MoveRates _moveRates = new(MoveRates.INSTANT_VALUE); /// /// True if initialized. /// private bool _initialized; /// /// Cached TickDelta of the TimeManager. /// private float _tickDelta; #endregion private void Awake() { _transformInstantiatedLocalProperties = transform.GetLocalProperties(); } private void OnDestroy() { ChangeSubscription(false); } public override void OnStartClient() { bool error = false; if (transform.parent == null) { NetworkManagerExtensions.LogError($"{GetType().Name} on gameObject {gameObject.name} requires a parent to detach from."); error = true; } if (_followObject == null) { NetworkManagerExtensions.LogError($"{GetType().Name} on gameObject {gameObject}, root {transform.root} requires followObject to be set."); error = true; } if (error) return; _parent = transform.parent; transform.SetParent(null); SetTimeManager(base.TimeManager); //Unsub first in the rare chance we already subbed such as a stop callback issue. ChangeSubscription(false); ChangeSubscription(true); _postTickFollowObjectWorldProperties = _followObject.GetWorldProperties(); _tickDelta = (float)base.TimeManager.TickDelta; _initialized = true; } public override void OnStopClient() { #if UNITY_EDITOR if (ApplicationState.IsQuitting()) return; #endif //Reattach to parent. if (_attachOnStop && _parent != null) { //Reparent transform.SetParent(_parent); //Set to instantiated local values. transform.SetLocalProperties(_transformInstantiatedLocalProperties); } _postTickFollowObjectWorldProperties.ResetState(); ChangeSubscription(false); _initialized = false; } [Client(Logging = LoggingType.Off)] private void Update() { MoveTowardsFollowTarget(); } /// /// Called after a tick completes. /// private void _timeManager_OnPostTick() { if (!_initialized) return; _postTickFollowObjectWorldProperties.Update(_followObject); //Unset values if not following the transform property. if (!_synchronizePosition) _postTickFollowObjectWorldProperties.Position = transform.position; if (!_synchronizeRotation) _postTickFollowObjectWorldProperties.Rotation = transform.rotation; if (!_synchronizeScale) _postTickFollowObjectWorldProperties.Scale = transform.localScale; SetMoveRates(); } /// /// Sets a new PredictionManager to use. /// /// private void SetTimeManager(TimeManager tm) { if (tm == _timeManager) return; //Unsub from current. ChangeSubscription(false); //Sub to newest. _timeManager = tm; ChangeSubscription(true); } /// /// Changes the subscription to the TimeManager. /// private void ChangeSubscription(bool subscribe) { if (_timeManager == null) return; if (subscribe) _timeManager.OnPostTick += _timeManager_OnPostTick; else _timeManager.OnPostTick -= _timeManager_OnPostTick; } /// /// Moves towards targetObject. /// private void MoveTowardsFollowTarget() { if (!_initialized) return; _moveRates.Move(transform, _postTickFollowObjectWorldProperties, Time.deltaTime, useWorldSpace: true); } private void SetMoveRates() { if (!_initialized) return; 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 += Mathf.Max(Time.deltaTime, (1f / 50f)); float teleportT = (_enableTeleport) ? _teleportThreshold : MoveRates.UNSET_VALUE; _moveRates = MoveRates.GetWorldMoveRates(transform, _followObject, duration, teleportT); } } }