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);
}
}
}