reckfellENGINE

Кривые Безье и движение по пути

Относительные ручки 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
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 задаёт расстояние по запечённому пути в его локальных единицах, 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-примером не подтверждён. Если маршрут редактируется во время игры, пересоздавайте зависимые от кэша данные и проверяйте текущую длину.

Документация Reckfell EngineНаверх ↑
БЫСТРЫЙ ПЕРЕХОДНачало работыEntity и иерархияC# & .NET 10Сцены и префабы
Руководства и полный API Reckfell K
reckfellENGINE
RECKFELL ACCOUNT

Вход в аккаунт

Войдите в аккаунт Reckfell.

Один аккаунт для всей платформы Reckfell.