Интерполяция: скорость, сглаживание и поворот
Когда нужен MoveToward, как вычислять вес Lerp по времени и почему это не физическая интерполяция.
Выбрать поведение
| Нужно | Операция | Смысл параметра |
|---|---|---|
| Пройти не более speed × delta | MoveToward | Максимальное расстояние шага |
| Получить долю пути между A и B | Lerp | Вес, а не расстояние и не секунды |
| Каждый шаг уменьшать остаток с заданным периодом полураспада | Lerp с весом 1 − 0,5^(delta / halfLife) | halfLife — секунды до половины остатка при постоянной цели |
| Плавно повернуться | LerpAngle или нормализованные Quaternion.Slerp | Доля углового перехода |
Lerp с весом 0 даёт A, с 1 — B. Значение за диапазоном может экстраполировать: от (0, 0) к (10, 0) с весом 2 получается (20, 0). Если переход должен остановиться, ограничьте вес или используйте MoveToward по задаче.
Сравнить движение
Добавьте SmoothFollow2D.cs к корню Entity2D отдельного C#-уровня 1.0.0, задайте окно/viewport 640×420, соберите и нажмите F6. Верхняя белая точка идёт со скоростью 120 пикселей/с, нижняя синяя сокращает оставшийся путь вдвое за 0,3 с. Белые кольца отмечают цели.
SmoothFollow2D.cs
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));
}
}Через 0,3 с при исходном X = 80 и цели 520 верхняя точка имеет X = 116, нижняя — около 300. MoveToward не перескакивает цель. Сглаживание быстро идёт в начале и замедляется вблизи цели; это не постоянная скорость. Если нужен окончательный останов, задайте допуск и явно поставьте значение в цель.
Что зависит от частоты кадров
Постоянный вес вроде 0,1 в каждом _Process даёт разный итог за секунду при разном числе вызовов. Вес по времени устраняет эту зависимость для неизменной цели и выбранной экспоненциальной модели, с допуском float. В стенде 30 и 60 шагов на одну секунду дают близкий результат. Это не обещание одинакового результата для движущейся цели, разных моментов ввода, столкновений и произвольной симуляции.
delta здесь — время игрового шага. Не умножайте уже рассчитанный вес на delta повторно. Очень большой шаг может резко приблизить к цели; пауза, ограничения шага и шкала времени должны соответствовать правилам вашей игры.
Поворот и преобразование
Углы около +π и −π близки геометрически. LerpAngle выбирает короткий угловой путь; обычный Lerp чисел может повернуть через ноль. Для произвольного 3D-поворота используйте единичные Quaternion и Slerp, а не покомпонентное усреднение Euler.
Transform3D.InterpolateWith интерполирует перенос и разложение базиса, а не физическое движение с коллизиями. Вырожденный масштаб, отражения и произвольный shear требуют отдельного решения; нельзя ожидать, что такая интерполяция сохранит любую геометрию. Сглаживание позиции в этом руководстве также не заменяет физическую интерполяцию между игровыми шагами.