О чем этот пример

В игровой физике часто требуется не просто резко остановить объект, а сделать это плавно и реалистично. Пример демонстрирует, как с помощью метода `lerp` (линейной интерполяции) для вектора скорости можно создать эффект постепенного замедления объекта при приближении к цели. Это полезно для создания более "живого" поведения снарядов, врагов или игровых предметов, которые должны мягко прибывать в конечную точку, а не резко останавливаться.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    distanceText;
    source;
    target;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('flower', 'assets/sprites/flower-exo.png');
        this.load.image('cursor', 'assets/sprites/drawcursor.png');
    }

    create ()
    {
        this.target = this.add.image(0, 0, 'flower').setAlpha(0);

        this.source = this.physics.add.image(100, 300, 'flower');

        this.distanceText = this.add.text(10, 10, 'Click to set target', { fill: 'lime' });

        this.input.on('pointerdown', (pointer) =>
        {
            this.target.copyPosition(pointer).setAlpha(0.5);

            this.physics.moveToObject(this.source, this.target, 200);
        });
    }

    update ()
    {
        const distance = Phaser.Math.Distance.BetweenPoints(this.source.body.center, this.target);

        this.distanceText.setText(`Distance: ${distance.toFixed(3)} Speed: ${this.source.body.speed.toFixed(3)}`);

        if (this.source.body.speed > 0)
        {
            // Set a maximum velocity toward the target
            this.physics.moveToObject(this.source, this.target, 200);

            // Interpolate velocity toward (0, 0), starting at 10px away
            this.source.body.velocity.lerp(
                Phaser.Math.Vector2.ZERO,
                Phaser.Math.Clamp(1 - distance / 10, 0, 1)
            );
        }
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: {
            debug: true
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

Инициализация сцены и объектов

В методе create() создаются основные игровые объекты: спрайт-источник (source) с физикой Arcade и невидимый спрайт-цель (target). Цель изначально скрыта (setAlpha(0)).

this.target = this.add.image(0, 0, 'flower').setAlpha(0);

this.source = this.physics.add.image(100, 300, 'flower');

this.distanceText = this.add.text(10, 10, 'Click to set target', { fill: 'lime' });

Обработчик клика мыши перемещает цель в указанную точку и запускает движение источника к ней с помощью this.physics.moveToObject.

this.input.on('pointerdown', (pointer) =>
{
    this.target.copyPosition(pointer).setAlpha(0.5);
    this.physics.moveToObject(this.source, this.target, 200);
});

Логика плавного замедления в update()

В каждом кадре метод update() рассчитывает расстояние между центром физического тела источника и целью. Это расстояние — ключевой параметр для интерполяции.

const distance = Phaser.Math.Distance.BetweenPoints(this.source.body.center, this.target);

Затем, если скорость объекта больше нуля, происходит две важные операции. Сначала this.physics.moveToObject постоянно пересчитывает вектор скорости, направляя объект к цели. Это компенсирует возможные отклонения.

if (this.source.body.speed > 0)
{
    this.physics.moveToObject(this.source, this.target, 200);

Далее применяется интерполяция скорости. Метод this.source.body.velocity.lerp() плавно изменяет текущую скорость (velocity) в сторону целевого вектора. В данном случае целевой вектор — Phaser.Math.Vector2.ZERO (нулевая скорость, полная остановка).

this.source.body.velocity.lerp(
    Phaser.Math.Vector2.ZERO,
    Phaser.Math.Clamp(1 - distance / 10, 0, 1)
);

Коэффициент интерполяции (amount) вычисляется как Phaser.Math.Clamp(1 - distance / 10, 0, 1). Это означает: - Когда расстояние больше 10 пикселей, значение отрицательное, и Clamp устанавливает его в 0. Интерполяция не происходит, объект движется с полной скоростью. - При расстоянии менее 10 пикселей коэффициент становится положительным и растёт по мере приближения к цели, достигая 1 в самой точке цели. Это плавно снижает скорость до нуля.

Практическое применение и настройка

Ключевые параметры для настройки поведения: 1. **Максимальная скорость (200)**: Передаётся вторым аргументом в this.physics.moveToObject. Это желаемая скорость подлёта. 2. **Радиус начала замедления (10)**: Число 10 в выражении distance / 10. Это расстояние до цели (в пикселях), на котором начинается плавное торможение. Увеличив его, вы сделаете замедление более длинным и плавным.

// Пример: начало замедления за 50 пикселей до цели
Phaser.Math.Clamp(1 - distance / 50, 0, 1)

3. **Целевой вектор**: Вместо Phaser.Math.Vector2.ZERO можно использовать любой другой вектор. Например, чтобы объект не останавливался, а продолжал медленно дрейфовать.

// Замедление до скорости 10 пикселей/сек вправо
const targetVelocity = new Phaser.Math.Vector2(10, 0);
this.source.body.velocity.lerp(targetVelocity, 0.1);

Этот подход гораздо эффективнее и управляемее, чем простое обнуление скорости при достижении определённого расстояния.

Что попробовать дальше

Использование velocity.lerp() в связке с moveToObject() позволяет легко создавать сложное и реалистичное движение с плавным завершением. Для экспериментов попробуйте: изменить радиус начала замедления; задать конечную скорость не нулевой, а, например, медленного движения по инерции; применить интерполяцию не к скорости, а к ускорению (body.acceleration) для ещё более мягкого изменения движения.