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

Одна из частых задач в аркадных играх — заставить объект двигаться к заданной точке на экране, будь то клик мыши или координата противника. Phaser Arcade Physics предоставляет для этого простой метод `moveToObject`, но он не останавливает объект автоматически по прибытии. В этой статье мы разберем, как реализовать движение с плавной остановкой в нужной позиции, что критически важно для механик следования за курсором, преследования врагов или перемещения по пути.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    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.source = this.physics.add.image(100, 300, 'flower');

        this.target = new Phaser.Math.Vector2();

        const cursor = this.add.image(0, 0, 'cursor').setVisible(false);

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

        this.input.on('pointerdown', (pointer) =>
        {
            this.target.x = pointer.x;
            this.target.y = pointer.y;

            // Move at 200 px/s:
            this.physics.moveToObject(this.source, this.target, 200);

            cursor.copyPosition(this.target).setVisible(true);
        });
    }

    update ()
    {
        //  4 is our distance tolerance, i.e. how close the source can get to the target
        //  before it is considered as being there. The faster it moves, the more tolerance is required.
        const tolerance = 4;

        // const tolerance = 200 * 1.5 / this.game.loop.targetFps;

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

        if (this.source.body.speed > 0)
        {
            this.distanceText.setText(`Distance: ${distance}`);

            if (distance < tolerance)
            {
                this.source.body.reset(this.target.x, this.target.y);
            }
        }
    }
}

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

const game = new Phaser.Game(config);

Подготовка сцены и объектов

В методе preload загружаем два спрайта: основной объект (flower) и визуальный маркер цели (cursor). В create инициализируем физический спрайт 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.source = this.physics.add.image(100, 300, 'flower');
    this.target = new Phaser.Math.Vector2();
    const cursor = this.add.image(0, 0, 'cursor').setVisible(false);
}

Задание цели и запуск движения

Обрабатываем клик мыши, чтобы установить новую цель. При событии pointerdown координаты указателя сохраняются в вектор target. Затем вызывается ключевой метод this.physics.moveToObject, который задает скорость тела source так, чтобы оно двигалось прямо к цели с указанной скоростью (200 пикселей в секунду). Маркер cursor становится видимым в точке цели для наглядности.

this.input.on('pointerdown', (pointer) =>
{
    this.target.x = pointer.x;
    this.target.y = pointer.y;
    // Move at 200 px/s:
    this.physics.moveToObject(this.source, this.target, 200);
    cursor.copyPosition(this.target).setVisible(true);
});

Контроль прибытия и остановка

Метод moveToObject только задает скорость, но не следит за достижением цели. Поэтому логику остановки мы реализуем в update. Каждый кадр вычисляется текущее расстояние между source и target с помощью Phaser.Math.Distance.BetweenPoints.

Ключевая идея — использовать **допуск (tolerance)**. Из-за дискретной природы обновления кадров объект может «проскочить» точку цели. Допуск определяет, насколько близко объект должен подойти, чтобы считаться прибывшим. В примере он равен 4 пикселям. Если расстояние меньше допуска и скорость тела больше нуля, мы сбрасываем позицию и скорость тела, используя this.source.body.reset.

update ()
{
    const tolerance = 4;
    const distance = Phaser.Math.Distance.BetweenPoints(this.source, this.target);
    if (this.source.body.speed > 0)
    {
        if (distance < tolerance)
        {
            this.source.body.reset(this.target.x, this.target.y);
        }
    }
}

Вызов reset устанавливает тело в указанные координаты и обнуляет его скорость, что приводит к мгновенной и точной остановке.

Настройка физики и запуск игры

Для работы примера необходимо активировать Arcade Physics в конфигурации игры. Убедитесь, что в настройках physics указан default: 'arcade'.

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    physics: {
        default: 'arcade'
    },
    scene: Example
};
const game = new Phaser.Game(config);

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

Мы реализовали базовый паттерн движения к точке с контролируемой остановкой. Для экспериментов попробуйте: изменить скорость движения и адаптивный допуск (закомментированная строка tolerance = 200 * 1.5 / this.game.loop.targetFps), добавить плавное замедление перед остановкой, или заставить объект обходить препятствия, комбинируя этот подход с collider. Этот механизм — основа для AI врагов, управления персонажем по клику или любых задач точечного наведения.