reckfellENGINE

Interpolation: speed, smoothing and rotation

When to use MoveToward, time-based Lerp weights and the distinction from physics interpolation.

On this page

Choose the behavior

NeedOperationParameter meaning
Move at most speed × deltaMoveTowardMaximum step distance
Obtain a fraction between A and BLerpWeight, not distance or seconds
Halve the remaining gap over a chosen half-lifeLerp with weight 1 − 0.5^(delta / halfLife)halfLife is seconds to halve the gap for a constant target
Rotate smoothlyLerpAngle or normalized Quaternion.SlerpFraction of angular transition

Lerp at weight 0 gives A; at 1, B. Out-of-range weights can extrapolate: (0, 0) toward (10, 0) at weight 2 gives (20, 0). Clamp weight when required, or use MoveToward for the intended behavior.

Compare movement

Attach SmoothFollow2D.cs to an Entity2D root in a separate 1.0.0 C# level, set the window/viewport to 640×420, build and press F6. The upper white point travels at 120 pixels/s; the lower blue point halves its remaining gap every 0.3 s. White rings mark targets.

Constant speed above and exponential smoothing below

SmoothFollow2D.cs
C#
using System;
using Reckfell;

public partial class SmoothFollow2D : Entity2D
{
    [Export] public float Speed { get; set; } = 120;
    [Export] public float HalfLife { get; set; } = 0.3f;
    [Export] public float TargetX { get; set; } = 520;
    public Vector2 LinearPosition { get; private set; } = new(80, 190);
    public Vector2 SmoothPosition { get; private set; } = new(80, 300);

    public override void _Ready()
    {
        bool ru = TranslationServer.GetLocale().StartsWith("ru");
        var title = new Label { Position = new Vector2(24, 20), Text = ru
            ? "Постоянная скорость и сглаживание" : "Constant speed and smoothing" };
        title.AddThemeFontSizeOverride("font_size", 22); AddChild(title);
        AddChild(new Label { Position = new Vector2(24, 65), Text = ru
            ? "MoveToward: 120 пикселей/с. Lerp: половина остатка за 0,3 с."
            : "MoveToward: 120 pixels/s. Lerp: half the remaining gap per 0.3 s." });
    }

    public static float SmoothingWeight(float halfLife, double delta)
    {
        if (!float.IsFinite(halfLife) || halfLife <= 0 || !double.IsFinite(delta) || delta < 0)
            throw new ArgumentOutOfRangeException(nameof(halfLife));
        return (float)(1 - Math.Pow(0.5, delta / halfLife));
    }

    public void Reset()
    {
        LinearPosition = new Vector2(80, 190);
        SmoothPosition = new Vector2(80, 300);
        QueueRedraw();
    }

    public Error Advance(double delta)
    {
        if (!double.IsFinite(delta) || delta < 0 || !float.IsFinite(Speed) || Speed < 0 ||
            !float.IsFinite(HalfLife) || HalfLife <= 0 || !float.IsFinite(TargetX) || TargetX < 40 || TargetX > 600)
            return Error.InvalidParameter;
        float step = (float)Math.Min(Speed * delta, float.MaxValue);
        LinearPosition = LinearPosition.MoveToward(new Vector2(TargetX, 190), step);
        SmoothPosition = SmoothPosition.Lerp(new Vector2(TargetX, 300), SmoothingWeight(HalfLife, delta));
        QueueRedraw();
        return Error.Ok;
    }

    public override void _Process(double delta)
    {
        if (Advance(delta) == Error.Ok) return;
        SetProcess(false);
        ReckfellApi.PrintErr("SmoothFollow2D: invalid settings");
    }

    public override void _Draw()
    {
        DrawRect(new Rect2(0, 0, 640, 420), new Color(0.035f, 0.04f, 0.05f));
        DrawLine(new Vector2(80, 190), new Vector2(TargetX, 190), new Color(0.25f, 0.25f, 0.25f), 2);
        DrawLine(new Vector2(80, 300), new Vector2(TargetX, 300), new Color(0.25f, 0.25f, 0.25f), 2);
        DrawCircle(new Vector2(TargetX, 190), 11, Colors.White, false, 2, true);
        DrawCircle(new Vector2(TargetX, 300), 11, Colors.White, false, 2, true);
        DrawCircle(LinearPosition, 8, Colors.White);
        DrawCircle(SmoothPosition, 8, new Color(0.25f, 0.46f, 1));
    }
}

After 0.3 s from X = 80 toward 520, the upper point is at X = 116 and the lower near 300. MoveToward does not overshoot. Smoothing is fast initially and slows near the target; it is not constant speed. Use a tolerance and explicit snap when an exact stop is needed.

Frame-rate dependence

A fixed weight such as 0.1 each _Process gives different one-second results at different callback counts. A time-based weight removes that dependence for a constant target under this exponential model, within float tolerance. The fixture compares 30 and 60 steps over a second. This does not promise identical outcomes for moving targets, different input times, collisions or arbitrary simulation.

delta is the gameplay timestep here. Do not multiply the calculated weight by delta again. A very large step can move close to the target abruptly; pause, step limits and time scaling must match gameplay rules.

Rotation and transforms

Angles near +π and −π are geometrically close. LerpAngle takes a short angular path; ordinary numeric Lerp may rotate through zero. For arbitrary 3D rotation, use unit Quaternions and Slerp instead of averaging Euler components.

Transform3D.InterpolateWith interpolates translation and a basis decomposition, not collision-aware physics motion. Degenerate scale, reflections and arbitrary shear need separate handling; do not expect it to preserve every geometry. This position smoothing also does not replace physics interpolation between simulation ticks.

Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.