Skip to content

Conversation

belplaton
Copy link

@belplaton belplaton commented Sep 25, 2025

Earlier, I created a Feature Request where I describe the problem in detail: #970

Why

Interframe smoothing can’t account for external motion (elevators, conveyors, cutscenes, parent moves). As a result, the graphical child can lag behind its target/root, causing visible separation and jitter (as in the screenshot).

This PR lets systems provide per-tick fixed offsets (TransformProperties: pos/rot/scale) that are not interpolated. The fixed part is applied immediately; only the residual is smoothed. This keeps the graphical object aligned under external forces and removes drift/jitter. Additionally, a target-based registry allows pushing the same offset to all smoothers bound to the same target.

Summary

Add per-tick fixed offsets (TransformProperties: position/rotation/scale) that are not interpolated. The fixed part is applied immediately within the same tick; only the residual is smoothed. Also add a NetworkObject registry to fetch all smoothers bound to a given TargetTransform.

Changes

  • UniversalTickSmoother

    • Per-tick ring buffer of fixed offsets as TransformProperties.

    • AddFixedOffset(uint tick, in TransformProperties delta) to accumulate world-space deltas.

    • In OnPostTick:

      • Clamp fixed delta against total tick delta (AxiswiseClamp).

      • Apply clamped part immediately to graphics (partial snap).

      • Enqueue goal with the clamped part subtracted (smooth only the residual).

      • Recompute MoveRates after the snap to avoid pullback/jitter.

  • AxiswiseClamp(TransformProperties fixed, TransformProperties total)

    • Position/Scale: per-axis clamp (preserve sign; don’t exceed magnitude).

    • Rotation: clamp along total’s axis (angle-axis projection + signed magnitude clamp).

  • TickSmootherController ↔ NetworkObject

    • Auto register/unregister controller by TargetTransform when initialized/deinitialized (_initializingNetworkBehaviour != null).
  • NetworkObject

    • bool TryGetTickSmootherControllersByTarget(Transform target, out IReadOnlyList tickSmootherControllers);

Minimal API

        мoid UniversalTickSmoother.AddFixedOffset(uint tick, in TransformProperties delta);

        bool TryGetTickSmootherControllersByTarget(Transform target,
                    out IReadOnlyList<TickSmootherController> tickSmootherControllers);

Example of usage

        public void ExternalVelocityApply(Vector3 velocity, float deltaTime)
        {
            if (NetworkObject.TryGetTickSmootherControllersByTarget(
                   transform, out var tickSmootherControllers))
            {
                if (deltaTime >= 1e-6f)
                {
                    var offset = velocity * deltaTime;
                    for (var i = 0; i < tickSmootherControllers.Count; i++)
                    {
                        var tickSmoother = tickSmootherControllers[i];
                        tickSmoother.UniversalSmoother.AddFixedOffset(
                            TimeManager.LocalTick, new TransformProperties(offset, default, default));
                    }
                }
            }
        }

this methods i call only in code which executing in OnTick, so all FixedOffsets will apply after that in OnPostTick

Files changed

  • UniversalTickSmoother.cs

  • TickSmootherController.cs

  • NetworkObject.Prediction.cs

  • TransformProperties.cs

@belplaton
Copy link
Author

@FirstGearGames

@FirstGearGames
Copy link
Owner

I checked out the code and I think this could be done more easily, but I want to make sure the situation fits first.

I assume this happens when your object (eg)evelvator has very fast smoothing, such as over 1 or 2 ticks, while the player object has adaptive smoothing, or more set smoothing larger than the (eg)elevator value?

@belplaton
Copy link
Author

belplaton commented Sep 28, 2025

@FirstGearGames

Hello! The elevator does not have interpolation.

The player's adaptive interpolation is turned off because it causes even more jitter during normal movement.

In addition to the player's elevator, any other objects on the map can potentially push the player.

Not all of these objects may have the same interpolation, and they may not necessarily have it.

In situations where interpolation is difficult to add or may not be applicable, instead of trying to precisely match the interpolation between different objects (and all of them, so that there is no visual intersection of different objects), we can specify an instantaneous offset for graphicalObject for some objects without interpolation.

In my case, this allows the KinematicCharacterSystem, which handles any collisions, external forces, or clipping to moving platforms, to ensure that a single method call can immediately remove the lag between the player's graphicalObject and changes to its real position and external objects that could affect it.

If necessary, if we have interpolation set up (for example, for an elevator), we can add not the entire fixedOffset, but only the part that would be missing after interpolation to reach the final position.

@FirstGearGames
Copy link
Owner

FirstGearGames commented Sep 28, 2025 via email

@belplaton
Copy link
Author

@FirstGearGames

When setting identical interpolation between the player, the elevator, and all other objects that I dragged into the elevator, a problem appeared where, despite the coordination between the player and the elevator, other objects (with NetworkTransform) began to pass through the elevator's View.

FixedOffset is useful when we are absolutely sure that one object should move relative to another object by no more and no less than X distance. And if used correctly, taking into account the interpolation of the object that produces the external movement, you can leave the interpolation on the elevator, the player, and other objects, simply passing fixedOffset * currentInterpolatedRatioToAllOffset

In addition, I would say that changing the interpolation at runtime is unreliable, because then the player will have a different expirience of the gameplay on the moving platform and outside of it, sometimes worse, sometimes better.

@belplaton
Copy link
Author

belplaton commented Sep 28, 2025

It is simple a utility that is not required to use, but can be helpful when used correctly.

@FirstGearGames
Copy link
Owner

The NetworkTransform isn't to be used on the same objects our other smoothers are handling. I'm still thinking what I suggested would work perfectly but I definitely want to make sure I'm understanding the situation fully.

The root object is sitting on the platform correctly, right? Even if you are not smoothing the platform at all an interpolation of 1 on our smoothers shouldn't show any desync and will clean up movement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants