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 + +}