О чем этот пример
При создании игр с физикой часто возникает задача плавного перемещения объекта к цели с последующей точной остановкой. Простое использование `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), где объект останавливается на одной точке, а затем автоматически стартует к следующей. Попробуйте применить этот подход к поведению патрулирующих врагов или снарядам с самонаведением.
