Кривые Безье и движение по пути
Относительные ручки Curve2D, параметр t, запекание и скорость PathFollow2D по расстоянию.
Не смешивать параметр и расстояние
Кубическая кривая задаётся началом P0, ручками P1/P2 и концом P3. Sample(segment, t) использует параметр t выбранного сегмента; при t = 0 и 1 получаются его концы. Равные приращения t обычно не дают равные расстояния по кривой.
Curve2D.AddPoint принимает позицию точки и относительные смещения входящей/исходящей ручек. Для начала (0, 0) с исходящей ручкой (30, 60) и конца (100, 0) с входящей (−30, 60) абсолютные ручки — (30, 60) и (70, 60). Sample(0, 0.5) даёт (50, 45), а не середину прямой (50, 0).
Запустить маршрут
Создайте отдельный C#-уровень 1.0.0 с корнем Path2D. Добавьте CurveRoute2D.cs и прикрепите к корню; окно/viewport — 640×420. Соберите и нажмите F6. Стрелка движется по замкнутому пути, её локальная ось +X смотрит вдоль касательной. Компонент создаёт Curve и 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 задаёт расстояние по запечённому пути в его локальных единицах, ProgressRatio — долю этой длины. В примере Speed = 100 и delta = 1 увеличивают Progress на 100, а не на один сегмент. При масштабе Path2D, особенно неравномерном, мировая скорость меняется. Это не физическое движение с коллизиями.
Запекание и замыкание
BakeInterval управляет плотностью кэша приближённой длины. Меньше интервал — больше точек и памяти; это не изменение исходных контрольных ручек. SampleBaked(offset) использует расстояние по этому кэшу. CubicInterp = false выбран явно, чтобы PathFollow2D и SampleBaked в проверке использовали одинаковую линейную интерполяцию кэша.
Путь замкнут совпавшими первой и последней позициями, а не одним Loop = true. Loop повторяет расстояние; на открытом пути переход от конца к началу остаётся скачком. Даже совпавшие концы могут дать рывок направления, если не согласовать касательные. При Loop = false нативный Progress ограничивается концами пути.
Что проверить при ошибке
| Симптом | Проверка |
|---|---|
| Объект стоит на месте | У Path2D есть непустая Curve ненулевой длины; PathFollow2D — её ребёнок |
| Движение неровное | Вы меняете расстояние, а не только t; проверьте масштаб и плотность кэша |
| Стрелка смотрит боком | Геометрия маркера ориентирована по +X; Rotates включён |
| Рывок на границе цикла | Совпадают концы и согласованы ручки; Loop сам не соединяет точки |
Для 3D аналогичные роли имеют Curve3D/Path3D/PathFollow3D, но ориентация и up-векторы требуют отдельной настройки. Их полный 3D-проход этим 2D-примером не подтверждён. Если маршрут редактируется во время игры, пересоздавайте зависимые от кэша данные и проверяйте текущую длину.