Bezier curves and path following
Relative Curve2D handles, parameter t, baking and distance-based PathFollow2D motion.
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.
CurveRoute2D.cs
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
| Symptom | Check |
|---|---|
| Object stays still | Path2D has a nonempty, nonzero-length Curve; PathFollow2D is its child |
| Uneven movement | Advance distance rather than only t; check scale and cache density |
| Arrow faces sideways | Marker geometry faces +X; Rotates is enabled |
| Jump at loop boundary | Match 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.