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

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

Версия 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');
    }

    create ()
    {
        const target = this.add.image(100, 300, 'flower').setAlpha(0.5);

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

        this.add.text(10, 20, 'Click to set target', { fill: 'yellow' });

        const text = this.add.text(10, 40, '', { fill: 'aqua' });

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

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

        this.physics.world.on('worldstep', (delta) =>
        {
            // Tolerance is half the per-step distance
            const tolerance = 100 * delta;
            const distance = Phaser.Math.Distance.BetweenPoints(source.body.center, target);
            const dx = source.body.deltaX();
            const dy = source.body.deltaY();

            text.setText(`
Delta Time:  ${delta} s
Distance:    ${distance} px
Tolerance:   ${tolerance} px
Body Delta:  ${Math.hypot(dx, dy)} px
Body Speed:  ${source.body.speed} px/s`
            );

            if (source.body.speed > 0 && distance <= tolerance)
            {
                source.body.stop();
                // source.body.reset(target.x, target.y);
            }
        });
    }
}

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

const game = new Phaser.Game(config);

Почему не остановится? Проблема с moveToObject

Метод this.physics.moveToObject(source, target, speed) задает скорости тела так, чтобы оно начало движение к цели. Однако, когда тело приближается к цели, его скорость не обнуляется автоматически. Физический движок продолжает вычислять положение на основе скорости, что заставляет объект "проскакивать" мимо цели, затем разворачиваться и осциллировать вокруг нее.

Для решения этой проблемы нам нужно вручную отслеживать расстояние до цели и останавливать тело в подходящий момент. Именно для этого используется событие worldstep.

Событие worldstep: контроль на каждом шаге физики

Событие worldstep генерируется движком Arcade Physics на каждом этапе обновления мира, до расчета столкновений и позиций. В его обработчик передается ключевой параметр delta — время в секундах, прошедшее с последнего шага. Это идеальное место для проверки условий, зависящих от состояния физики.

В нашем примере мы подписываемся на это событие в методе create:

this.physics.world.on('worldstep', (delta) => {
    // Логика проверки и остановки
});

Внутри обработчика мы имеем доступ ко всем свойствам тела (source.body) и можем безопасно их изменять до того, как движок завершит обновление кадра.

Расчет допуска и проверка дистанции

Ключевая идея — остановить тело не когда дистанция равна нулю (это сложно достичь из-за дискретной природы обновлений), а когда она меньше или равна некоторому "допуску". Этот допуск должен быть связан с расстоянием, которое тело может пройти за один шаг, чтобы избежать резкой остановки.

Рассчитываем допуск и текущее расстояние:

// Допуск = половина расстояния за шаг при заданной скорости.
// 100 — это половина от скорости 200 px/s.
const tolerance = 100 * delta;
const distance = Phaser.Math.Distance.BetweenPoints(source.body.center, target);

Здесь 100 * delta — это расстояние, которое тело прошло бы за половину шага времени, если бы двигалось со скоростью 200 пикселей в секунду. Это эффективный порог для принятия решения об остановке.

Условие остановки и сброс скорости

Остановку следует производить только если тело движется (speed > 0) и уже находится в пределах рассчитанного допуска.

if (source.body.speed > 0 && distance <= tolerance)
{
    source.body.stop();
}

Метод body.stop() обнуляет скорости тела по осям X и Y, мгновенно останавливая его. В закомментированной строке показана альтернатива — body.reset(x, y), которая не только останавливает тело, но и телепортирует его в точные координаты цели, что может быть полезно для абсолютной точности.

В обработчике мы также выводим отладочную информацию, используя body.deltaX() и body.deltaY(), которые возвращают изменение позиции тела за последний шаг физики.

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

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