reckfellENGINE

Bezier curves and path following

Relative Curve2D handles, parameter t, baking and distance-based PathFollow2D motion.

On this page

Distinguish parameter and distance

A cubic curve has start P0, handles P1/P2 and end P3. Sample(segment, t) uses that segment's parameter; t = 0 and 1 give its endpoints. Equal t increments generally do not travel equal distances.

Curve2D.AddPoint takes a point position and relative incoming/outgoing handle offsets. A start at (0, 0) with outgoing (30, 60), and end at (100, 0) with incoming (−30, 60), produce absolute handles (30, 60) and (70, 60). Sample(0, 0.5) gives (50, 45), not the straight-line midpoint (50, 0).

Run the route

Create a separate 1.0.0 C# level with a Path2D root. Add CurveRoute2D.cs and attach it; use a 640×420 window/viewport. Build and press F6. The arrow follows a closed route with local +X along the tangent. The component creates its own Curve and PathFollow2D.

An arrow following a baked closed Bezier path

CurveRoute2D.cs
C#
using System;
using Reckfell;

public partial class CurveRoute2D : Path2D
{
    [Export] public float Speed { get; set; } = 100;
    public PathFollow2D Follower { get; private set; } = null!;
    private Vector2[] _line = [];

    public override void _Ready()
    {
        Curve = new Curve2D { BakeInterval = 3 };
        Curve.AddPoint(new Vector2(100, 220), @out: new Vector2(0, -130));
        Curve.AddPoint(new Vector2(320, 100), @in: new Vector2(-120, 0), @out: new Vector2(150, 0));
        Curve.AddPoint(new Vector2(540, 280), @in: new Vector2(0, -120), @out: new Vector2(0, 100));
        Curve.AddPoint(new Vector2(100, 220), @in: new Vector2(0, 180));
        _line = Curve.GetBakedPoints();
        Follower = new PathFollow2D { Loop = true, Rotates = true, CubicInterp = false };
        AddChild(Follower);
        Follower.AddChild(new Polygon2D
        {
            Polygon = [new Vector2(14, 0), new Vector2(-10, -7), new Vector2(-10, 7)],
            Color = new Color(0.25f, 0.46f, 1)
        });
        bool ru = TranslationServer.GetLocale().StartsWith("ru");
        var title = new Label { Position = new Vector2(24, 16), Text = ru
            ? "Путь Безье: движение по расстоянию" : "Bezier path: distance-based travel" };
        title.AddThemeFontSizeOverride("font_size", 22); AddChild(title);
        AddChild(new Label { Position = new Vector2(24, 52), Text = ru
            ? "Progress — расстояние вдоль запечённого пути; не параметр t."
            : "Progress is distance along the baked path, not the curve parameter t." });
        QueueRedraw();
    }

    public Error Advance(double delta)
    {
        if (Follower is null) return Error.Unconfigured;
        if (!double.IsFinite(delta) || delta < 0 || !float.IsFinite(Speed) || Speed < 0)
            return Error.InvalidParameter;
        double length = Curve.GetBakedLength();
        double distance = Follower.Progress + Speed * delta;
        if (length <= 0 || !double.IsFinite(distance)) return Error.InvalidData;
        Follower.Progress = (float)(distance % length);
        return Error.Ok;
    }

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

    public override void _Draw()
    {
        DrawRect(new Rect2(0, 0, 640, 420), new Color(0.035f, 0.04f, 0.05f));
        if (_line.Length > 1) DrawPolyline(_line, new Color(0.7f, 0.72f, 0.78f), 2, true);
        if (Curve is null) return;
        for (int i = 0; i < Curve.PointCount; i++) DrawCircle(Curve.GetPointPosition(i), 4, Colors.White);
    }
}

Progress is distance along the baked path in local units; ProgressRatio is a fraction of its length. Speed = 100 and delta = 1 advance Progress by 100, not one segment. Scaling Path2D, especially nonuniformly, changes world-space speed. This is not collision-aware physics movement.

Baking and closing

BakeInterval controls the density of the approximate-length cache. A smaller interval uses more points/memory; it does not change control handles. SampleBaked(offset) samples distance along that cache. CubicInterp = false is explicit so PathFollow2D and the checked SampleBaked use the same linear cache interpolation.

The route closes by matching first and last positions, not merely setting Loop = true. Loop repeats distance; an open route still jumps from end to start. Even matching endpoints can turn abruptly when tangents disagree. With Loop = false, native Progress clamps to the path endpoints.

Diagnose a problem

SymptomCheck
Object stays stillPath2D has a nonempty, nonzero-length Curve; PathFollow2D is its child
Uneven movementAdvance distance rather than only t; check scale and cache density
Arrow faces sidewaysMarker geometry faces +X; Rotates is enabled
Jump at loop boundaryMatch endpoints and tangents; Loop does not connect points

Curve3D/Path3D/PathFollow3D have corresponding roles, but orientation and up vectors need separate configuration. This 2D example does not verify a complete 3D workflow. For runtime route edits, rebuild cache-dependent data and validate current length.

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.