diff --git a/Assets/FishNet/Runtime/Generated/Component/TickSmoothing/TickSmootherController.cs b/Assets/FishNet/Runtime/Generated/Component/TickSmoothing/TickSmootherController.cs
index 517e2a06..b0b1cce7 100644
--- a/Assets/FishNet/Runtime/Generated/Component/TickSmoothing/TickSmootherController.cs
+++ b/Assets/FishNet/Runtime/Generated/Component/TickSmoothing/TickSmootherController.cs
@@ -75,10 +75,16 @@ public void Initialize(InitializationSettings initializationSettings, MovementSe
_initializedOffline = initializationSettings.InitializingNetworkBehaviour == null;
_isInitialized = true;
+
+ // Register in NetworkObject by target when online.
+ RegisterInNetworkObject();
+
}
public void OnDestroy()
{
+ UnregisterInNetworkObject();
+
ChangeSubscriptions(false);
StoreSmoother();
_destroyed = true;
@@ -272,8 +278,43 @@ private void ChangeSubscriptions(bool subscribe)
}
}
+ ///
+ /// Registers this controller into its NetworkObject index by target transform.
+ /// Only when initialized online (_initializingNetworkBehaviour != null) and _isInitialized == true.
+ ///
+ private void RegisterInNetworkObject()
+ {
+ if (!_isInitialized || _initializingNetworkBehaviour == null)
+ return;
+ var no = _initializingNetworkBehaviour.NetworkObject;
+ if (no == null)
+ return;
+ var target = _initializationSettings.TargetTransform;
+ if (target == null)
+ return;
+ no.RegisterTickSmootherController(target, this);
+ }
+
+ ///
+ /// Unregisters this controller from its NetworkObject index by target transform.
+ ///
+ private void UnregisterInNetworkObject()
+ {
+ if (_initializingNetworkBehaviour == null)
+ return;
+ var no = _initializingNetworkBehaviour.NetworkObject;
+ if (no == null)
+ return;
+ var target = _initializationSettings.TargetTransform;
+ if (target == null)
+ return;
+ no.UnregisterTickSmootherController(target, this);
+ }
+
public void ResetState()
{
+ UnregisterInNetworkObject();
+
_initializationSettings = default;
_ownerMovementSettings = default;
_spectatorMovementSettings = default;
diff --git a/Assets/FishNet/Runtime/Generated/Component/TickSmoothing/UniversalTickSmoother.cs b/Assets/FishNet/Runtime/Generated/Component/TickSmoothing/UniversalTickSmoother.cs
index cebc9539..59dd3842 100644
--- a/Assets/FishNet/Runtime/Generated/Component/TickSmoothing/UniversalTickSmoother.cs
+++ b/Assets/FishNet/Runtime/Generated/Component/TickSmoothing/UniversalTickSmoother.cs
@@ -21,30 +21,13 @@ private struct TickTransformProperties
{
public readonly uint Tick;
public readonly TransformProperties Properties;
+ public readonly TransformProperties FixedOffset;
- public TickTransformProperties(uint tick, Transform t)
+ public TickTransformProperties(uint tick, TransformProperties properties, TransformProperties fixedOffset)
{
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;
+ Properties = properties;
+ FixedOffset = fixedOffset;
}
}
#endregion
@@ -74,6 +57,10 @@ public TickTransformProperties(uint tick, TransformProperties tp, Vector3 localS
///
private TransformProperties _graphicsPreTickWorldValues;
///
+ /// Fixed Offset of World values of the graphical after it's been accumulated in PostTick.
+ ///
+ private TransformProperties _graphicsPostTickFixedOffsetWorldValues;
+ ///
/// Cached value of adaptive interpolation value.
///
private AdaptiveInterpolationType _cachedAdaptiveInterpolationValue;
@@ -123,6 +110,108 @@ public TickTransformProperties(uint tick, TransformProperties tp, Vector3 localS
/// TimeManager tickDelta.
///
private float _tickDelta;
+ ///
+ /// Ring buffer entry for externally provided (non-interpolated) fixed offsets per tick.
+ ///
+ private struct FixedOffsetEntry { public uint Tick; public TransformProperties Sum; }
+ ///
+ /// Buffer of fixed offsets. Size should cover interpolation window + network jitter.
+ ///
+ private FixedOffsetEntry[] _fixedOffsets;
+ private TransformProperties _currentAccumulatedOffset = new(default, Quaternion.identity, default);
+
+ ///
+ /// Adds a world-space offset that must NOT be smoothed for the specified tick.
+ /// Offsets are accumulated within a tick.
+ ///
+ public void AddFixedOffset(uint tick, TransformProperties worldDelta)
+ {
+ // Ignore when tick is invalid.
+ if (tick == TimeManager.UNSET_TICK)
+ return;
+ int i = (int)(tick % (uint)_fixedOffsets.Length);
+ if (_fixedOffsets[i].Tick != tick)
+ _fixedOffsets[i] = new FixedOffsetEntry { Tick = tick, Sum = worldDelta };
+ else
+ _fixedOffsets[i].Sum += worldDelta;
+ }
+
+ ///
+ /// Consumes (reads and clears) accumulated fixed offset for a tick.
+ ///
+ private TransformProperties ConsumeFixedOffset(uint tick)
+ {
+ if (tick == TimeManager.UNSET_TICK)
+ return default;
+ int i = (int)(tick % (uint)_fixedOffsets.Length);
+ if (_fixedOffsets[i].Tick == tick)
+ {
+ TransformProperties v = _fixedOffsets[i].Sum;
+ _fixedOffsets[i].Sum = new(default, Quaternion.identity, default);
+ return v.IsValid ? v : new(default, Quaternion.identity, default);
+ }
+ return default;
+ }
+
+ ///
+ /// Axis-wise clamps fixedDelta against totalDelta: does not flip sign and does not exceed magnitude per axis.
+ /// Components of totalDelta that are ~0 cause clamping to 0 on that axis.
+ ///
+ private static TransformProperties AxiswiseClamp(TransformProperties fixedDelta, TransformProperties totalDelta)
+ {
+ var pos = new Vector3(
+ Clamp1(fixedDelta.Position.x, totalDelta.Position.x),
+ Clamp1(fixedDelta.Position.y, totalDelta.Position.y),
+ Clamp1(fixedDelta.Position.z, totalDelta.Position.z));
+
+
+ var rf = fixedDelta.Rotation;
+ var rt = totalDelta.Rotation;
+ if (rf == default) rf = Quaternion.identity;
+ if (rt == default) rt = Quaternion.identity;
+
+ rf.ToAngleAxis(out var aF, out var axF);
+ rt.ToAngleAxis(out var aT, out var axT);
+
+ aF = Mathf.DeltaAngle(0f, aF);
+ aT = Mathf.DeltaAngle(0f, aT);
+
+ Quaternion rotDelta;
+ if (Mathf.Abs(aT) < 1e-6f || axT == Vector3.zero)
+ {
+ rotDelta = Quaternion.identity;
+ }
+ else
+ {
+ var sign = Mathf.Sign(Vector3.Dot(axF, axT));
+ var signedF = aF * sign;
+
+ var clampedAngle = Clamp1(signedF, aT);
+ if (Mathf.Abs(clampedAngle) < 1e-6f)
+ rotDelta = Quaternion.identity;
+ else
+ {
+ var s = Mathf.Sign(clampedAngle);
+ rotDelta = Quaternion.AngleAxis(Mathf.Abs(clampedAngle), axT * s);
+ }
+ }
+
+ var scale = new Vector3(
+ Clamp1(fixedDelta.Scale.x, totalDelta.Scale.x),
+ Clamp1(fixedDelta.Scale.y, totalDelta.Scale.y),
+ Clamp1(fixedDelta.Scale.z, totalDelta.Scale.z)
+ );
+
+ return new TransformProperties(pos, rotDelta, scale);
+
+ static float Clamp1(float f, float t)
+ {
+ if (Mathf.Abs(t) < 1e-6f) return 0f;
+ if (Math.Abs(Mathf.Sign(f) - Mathf.Sign(t)) >= 1e-6f) return 0f;
+ return Mathf.Sign(t) * Mathf.Min(Mathf.Abs(f), Mathf.Abs(t));
+ }
+ }
+
///
/// NetworkBehaviour this is initialized for. Value may be null.
///
@@ -289,6 +378,8 @@ public void Initialize(InitializationSettings initializationSettings, MovementSe
if (!TransformsAreValid(graphicalTransform, targetTransform))
return;
+ _fixedOffsets = CollectionCaches.RetrieveArray();
+ Array.Resize(ref _fixedOffsets, 128);
_transformProperties = CollectionCaches.RetrieveBasicQueue();
_controllerMovementSettings = ownerSettings;
_spectatorMovementSettings = spectatorSettings;
@@ -553,18 +644,27 @@ public void OnPostTick(uint clientTick)
return;
if (clientTick <= _teleportedTick)
return;
-
+
//If preticked then previous transform values are known.
if (_preTicked)
{
- DiscardExcessiveTransformPropertiesQueue();
-
+ var trackerProps = GetTrackerWorldProperties();
+ var fixedOffset = default(TransformProperties);
+
+ // Apply non-interpolated fixed offsets for this tick (if any).
+ var fixedDelta = ConsumeFixedOffset(clientTick);
+ // Total delta for the tick (tracker vs pre-tick graphics).
+ var totalDelta = trackerProps - _graphicsPreTickWorldValues;
+ var clamped = AxiswiseClamp(fixedDelta, totalDelta);
+ fixedOffset += clamped;
+
//Only needs to be put to pretick position if not detached.
if (!_detachOnStart)
- _graphicalTransform.SetWorldProperties(_graphicsPreTickWorldValues);
-
+ _graphicalTransform.SetWorldProperties(_graphicsPreTickWorldValues + fixedOffset);
+
+ DiscardExcessiveTransformPropertiesQueue();
//SnapNonSmoothedProperties();
- AddTransformProperties(clientTick);
+ AddTransformProperties(clientTick, trackerProps, fixedOffset);
}
//If did not pretick then the only thing we can do is snap to instantiated values.
else
@@ -632,6 +732,7 @@ public void Teleport()
private void ClearTransformPropertiesQueue()
{
_transformProperties.Clear();
+ _currentAccumulatedOffset = new(default, Quaternion.identity, default);
//Also unset move rates since there is no more queue.
_moveRates = new(MoveRates.UNSET_VALUE);
}
@@ -647,22 +748,27 @@ private void DiscardExcessiveTransformPropertiesQueue()
//If there are entries to dequeue.
if (dequeueCount > 0)
{
- TickTransformProperties tpp = default;
+ TickTransformProperties ttp = default;
for (int i = 0; i < dequeueCount; i++)
- tpp = _transformProperties.Dequeue();
+ {
+ ttp = _transformProperties.Dequeue();
+ _currentAccumulatedOffset -= ttp.FixedOffset;
+ }
- SetMoveRates(tpp.Properties);
+ var nextValues = ttp.Properties + ttp.FixedOffset;
+ SetMoveRates(nextValues);
}
}
///
/// Adds a new transform properties and sets move rates if needed.
///
- private void AddTransformProperties(uint tick)
+ private void AddTransformProperties(uint tick, TransformProperties properties, TransformProperties fixedOffset)
{
- TickTransformProperties tpp = new(tick, GetTrackerWorldProperties());
- _transformProperties.Enqueue(tpp);
-
+ TickTransformProperties ttp = new(tick, properties, fixedOffset);
+ _transformProperties.Enqueue(ttp);
+ _currentAccumulatedOffset += fixedOffset;
+
//If first entry then set move rates.
if (_transformProperties.Count == 1)
{
@@ -715,7 +821,9 @@ private void ModifyTransformProperties(uint clientTick, uint firstTick)
newProperties.Scale = Vector3.Lerp(oldProperties.Scale, newProperties.Scale, easePercent);
}
- _transformProperties[index] = new(tick, newProperties);
+ _currentAccumulatedOffset -= _transformProperties[index].FixedOffset;
+ // TODO: set fixedOffset to default maybe can be a problem
+ _transformProperties[index] = new(tick, newProperties, default);
}
}
else
@@ -761,7 +869,8 @@ private void SetMoveRates(in TransformProperties prevValues)
return;
}
- TransformProperties nextValues = _transformProperties.Peek().Properties;
+ TickTransformProperties ttp = _transformProperties.Peek();
+ TransformProperties nextValues = ttp.Properties + ttp.FixedOffset;
float duration = _tickDelta;
@@ -839,10 +948,11 @@ private void MoveToTarget(float delta)
}
TickTransformProperties ttp = _transformProperties.Peek();
-
+ TransformProperties properties = ttp.Properties + _currentAccumulatedOffset;
+
TransformPropertiesFlag smoothedProperties = _cachedSmoothedProperties;
- _moveRates.Move(_graphicalTransform, ttp.Properties, smoothedProperties, delta * _movementMultiplier, useWorldSpace: true);
+ _moveRates.Move(_graphicalTransform, 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.
@@ -850,11 +960,12 @@ private void MoveToTarget(float delta)
{
//Dequeue current entry and if there's another call a move on it.
_transformProperties.Dequeue();
+ _currentAccumulatedOffset -= ttp.FixedOffset;
//If there are entries left then setup for the next.
if (_transformProperties.Count > 0)
{
- SetMoveRates(ttp.Properties);
+ SetMoveRates(properties);
//If delta is negative then call move again with abs.
if (tRemaining < 0f)
MoveToTarget(Mathf.Abs(tRemaining));
@@ -919,7 +1030,9 @@ public void ResetState()
_teleportedTick = TimeManager.UNSET_TICK;
_movementMultiplier = 1f;
+ CollectionCaches.StoreAndDefault(ref _fixedOffsets, 128);
CollectionCaches.StoreAndDefault(ref _transformProperties);
+ _currentAccumulatedOffset = new(default, Quaternion.identity, default);
_moveRates = default;
_preTicked = default;
_queuedTrackerProperties = null;
@@ -934,4 +1047,4 @@ public void ResetState()
public void InitializeState() { }
}
-}
\ No newline at end of file
+}
diff --git a/Assets/FishNet/Runtime/Object/NetworkObject/NetworkObject.Prediction.cs b/Assets/FishNet/Runtime/Object/NetworkObject/NetworkObject.Prediction.cs
index fb378c31..c5b0fa40 100644
--- a/Assets/FishNet/Runtime/Object/NetworkObject/NetworkObject.Prediction.cs
+++ b/Assets/FishNet/Runtime/Object/NetworkObject/NetworkObject.Prediction.cs
@@ -7,6 +7,7 @@
using FishNet.Object.Prediction;
using GameKit.Dependencies.Utilities;
using System.Collections.Generic;
+using FishNet.Component.Transforming.Beta;
using FishNet.Connection;
using FishNet.Managing.Server;
using UnityEngine;
@@ -40,6 +41,21 @@ internal enum PredictionType : byte
///
[Obsolete("This field will be removed in v5. Instead reference NetworkTickSmoother on each graphical object used.")]
public TransformTickSmoother PredictionSmoother { get; private set; }
+
+ public bool TryGetTickSmootherControllersByTarget(Transform target,
+ out IReadOnlyList tickSmootherControllers)
+ {
+ if (_tickSmoothersByTarget.TryGetValue(target, out var tickSmootherControllersRaw))
+ {
+ tickSmootherControllers = tickSmootherControllersRaw;
+ return true;
+ }
+
+ tickSmootherControllers = null;
+ return false;
+ }
+
+
#endregion
#region Internal.
@@ -48,6 +64,38 @@ internal enum PredictionType : byte
///
public RigidbodyPauser RigidbodyPauser => _rigidbodyPauser;
private RigidbodyPauser _rigidbodyPauser;
+
+ ///
+ /// Registers a TickSmootherController for a given target transform.
+ /// Called from TickSmootherController when it becomes initialized online.
+ ///
+ internal void RegisterTickSmootherController(Transform target, TickSmootherController controller)
+ {
+ if (target == null || controller == null)
+ return;
+ if (!_tickSmoothersByTarget.TryGetValue(target, out var list))
+ {
+ list = new List();
+ _tickSmoothersByTarget[target] = list;
+ }
+ if (!list.Contains(controller))
+ list.Add(controller);
+ }
+
+ ///
+ /// Unregisters a TickSmootherController for a given target transform.
+ ///
+ internal void UnregisterTickSmootherController(Transform target, TickSmootherController controller)
+ {
+ if (target == null || controller == null)
+ return;
+ if (_tickSmoothersByTarget.TryGetValue(target, out var list))
+ {
+ if (list.Remove(controller) && list.Count == 0)
+ _tickSmoothersByTarget.Remove(target);
+ }
+ }
+
#endregion
#region Serialized.
@@ -158,6 +206,9 @@ public void SetGraphicalObject(Transform t)
/// NetworkBehaviours which use prediction.
///
private List _predictionBehaviours = new();
+
+ private readonly Dictionary> _tickSmoothersByTarget = new();
+
#endregion
private void TimeManager_OnUpdate_Prediction()
@@ -308,7 +359,7 @@ private void InvokeStopCallbacks_Prediction(bool asServer)
if (!asServer)
{
- if (TimeManager != null)
+ if (TimeManager)
TimeManager.OnUpdate -= TimeManager_Update;
if (PredictionSmoother != null)
PredictionSmoother.OnStopClient();
diff --git a/Assets/FishNet/Runtime/Object/TransformProperties.cs b/Assets/FishNet/Runtime/Object/TransformProperties.cs
index 7ee1d7ca..b933de15 100644
--- a/Assets/FishNet/Runtime/Object/TransformProperties.cs
+++ b/Assets/FishNet/Runtime/Object/TransformProperties.cs
@@ -143,6 +143,34 @@ public TransformProperties(Vector3 position, Quaternion rotation, Vector3 localS
///
public static TransformProperties GetTransformDefault() => new(Vector3.zero, Quaternion.identity, Vector3.one);
+ public static TransformProperties operator +(TransformProperties a, TransformProperties b)
+ {
+ if (!a.IsValid) return b;
+ if (!b.IsValid) return a;
+ return new TransformProperties(
+ a.Position + b.Position,
+ a.Rotation * b.Rotation,
+ a.Scale + b.Scale);
+ }
+
+ public static TransformProperties operator -(TransformProperties a, TransformProperties b)
+ {
+ if (!a.IsValid) return -b;
+ if (!b.IsValid) return a;
+ return new TransformProperties(
+ a.Position - b.Position,
+ a.Rotation * Quaternion.Inverse(b.Rotation),
+ a.Scale - b.Scale);
+ }
+
+ public static TransformProperties operator -(TransformProperties a)
+ {
+ return new TransformProperties(
+ -a.Position,
+ Quaternion.Inverse(a.Rotation),
+ -a.Scale);
+ }
+
public override string ToString()
{
return $"Position: {Position.ToString()}, Rotation {Rotation.ToString()}, Scale {Scale.ToString()}";
@@ -212,4 +240,5 @@ public bool ValuesEquals(TransformProperties properties)
return Position == properties.Position && Rotation == properties.Rotation && Scale == properties.Scale;
}
}
-}
\ No newline at end of file
+
+}