О чем этот пример
В игровом движке Phaser 3 часто возникает задача заставить объект плавно перемещаться по заданному пути. Официальный пример демонстрирует два подхода: использование касательного вектора кривой для расчета скорости и прямое перемещение к точкам с физикой Matter. Эта статья разберет, как работает каждый метод, почему один был закомментирован, и как адаптировать код под ваши нужды. Понимание этих техник полезно для создания патрулирующих врагов, движущихся платформ или кат-сцен с плавной камерой.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
tempVecP;
tempVec;
ship;
points;
curve;
t = 0;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('ship', 'assets/sprites/lemming.png');
}
create ()
{
// var p0 = new Phaser.Math.Vector2(100, 500);
// var p1 = new Phaser.Math.Vector2(50, 100);
// var p2 = new Phaser.Math.Vector2(600, 100);
// var p3 = new Phaser.Math.Vector2(750, 300);
// curve = new Phaser.Curves.CubicBezier(p0, p1, p2, p3);
this.curve = new Phaser.Curves.Ellipse(400, 300, 200);
this.points = this.curve.getSpacedPoints(32);
this.tempVec = new Phaser.Math.Vector2();
this.tempVecP = new Phaser.Math.Vector2();
this.ship = this.matter.add.image(this.points[0].x, this.points[0].y, 'ship');
this.ship.setFrictionAir(0.0005);
this.nextPoint(this);
}
update ()
{
// var t = curve.getUtoTmapping(map.u);
// curve.getPoint(t + 0.1, tempVecP);
// curve.getTangent(t, tempVec);
// tempVec.scale(180);
// ship.setVelocity(tempVec.x, tempVec.y);
// ship.rotation = Phaser.Math.Angle.Between(ship.x, ship.y, tempVecP.x, tempVecP.y);
}
nextPoint (scene)
{
const next = this.points[this.t % this.points.length];
this.moveToXY(this.ship, next.x, next.y, 0, 500);
this.t++;
scene.time.addEvent({ delay: 500, callback: this.nextPoint, callbackScope: scene, args: [ scene ] });
}
moveToXY (gameObject, x, y, speed, maxTime)
{
if (speed === undefined) { speed = 60; }
if (maxTime === undefined) { maxTime = 0; }
const angle = Math.atan2(y - gameObject.y, x - gameObject.x);
if (maxTime > 0)
{
// We know how many pixels we need to move, but how fast?
const dx = gameObject.x - x;
const dy = gameObject.y - y;
speed = Math.sqrt(dx * dx + dy * dy) / (maxTime / 1000);
}
gameObject.setVelocityX((Math.cos(angle) * speed) / 100);
gameObject.setVelocityY((Math.sin(angle) * speed) / 100);
// gameObject.rotation = angle;
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#2d2d2d',
parent: 'phaser-example',
physics: {
default: 'matter',
matter: {
gravity: {
scale: 0
}
}
},
scene: Example
};
const game = new Phaser.Game(config);
Создание траектории и объекта
В основе примера лежит создание геометрической кривой, по которой будет двигаться спрайт. Изначально в коде закомментирована кривая Безье (Phaser.Curves.CubicBezier), но используется эллипс (Phaser.Curves.Ellipse).
this.curve = new Phaser.Curves.Ellipse(400, 300, 200);
Это создает эллиптическую траекторию с центром в точке (400, 300) и радиусом 200 пикселей. Далее из кривой получают набор из 32 равноудаленных точек, которые будут служить целевыми позициями для объекта.
this.points = this.curve.getSpacedPoints(32);
Спрайт корабля создается как физическое тело Matter в первой точке массива points. Важно отметить настройку setFrictionAir(0.0005), которая делает движение в среде очень плавным, почти без сопротивления, что идеально для демонстрации.
this.ship = this.matter.add.image(this.points[0].x, this.points[0].y, 'ship');
this.ship.setFrictionAir(0.0005);
Альтернативный метод: скорость из касательного вектора (закомментирован)
В методе update закомментирован более математически точный и плавный способ движения. Он использует касательный вектор к кривой в текущей точке для расчета мгновенной скорости объекта.
curve.getTangent(t, tempVec);
tempVec.scale(180);
ship.setVelocity(tempVec.x, tempVec.y);
Функция curve.getTangent(t, tempVec) вычисляет вектор, указывающий направление движения вдоль кривой в параметре `t`. Этот вектор затем масштабируется (увеличивается) для задания скорости. Таким образом, объект движется именно **вдоль** кривой, а не к следующей точке.
ship.rotation = Phaser.Math.Angle.Between(ship.x, ship.y, tempVecP.x, tempVecP.y);
Эта строка поворачивает спрайт лицом к точке, находящейся чуть впереди на кривой (tempVecP), что создает естественное направление взгляда. Этот метод требует непрерывного обновления параметра `tвupdate` и обеспечивает идеально гладкое движение по всей кривой.
Реализованный метод: пошаговое движение к точкам
Вместо непрерывного движения по кривой, рабочий код использует дискретный подход: объект последовательно перемещается от одной точки пути к следующей. Это реализовано в функции nextPoint.
const next = this.points[this.t % this.points.length];
this.moveToXY(this.ship, next.x, next.y, 0, 500);
Индекс следующей точки определяется через остаток от деления счетчика `tна длину массива, создавая бесконечный цикл по траектории. Каждые 500 миллисекунд таймерscene.time.addEventвызываетnextPoint` снова, создавая ритмичное движение.
Ключевая логика расчета скорости заключена в функцию moveToXY. Она определяет угол к цели и вычисляет необходимую скорость, чтобы достичь ее за строго заданное время (maxTime).
const angle = Math.atan2(y - gameObject.y, x - gameObject.x);
const dx = gameObject.x - x;
const dy = gameObject.y - y;
speed = Math.sqrt(dx * dx + dy * dy) / (maxTime / 1000);
Расстояние до цели делится на время в секундах, получая требуемую скорость в пикселях в секунду. Эта скорость затем разлагается на компоненты X и Y через Math.cos(angle) и Math.sin(angle).
gameObject.setVelocityX((Math.cos(angle) * speed) / 100);
gameObject.setVelocityY((Math.sin(angle) * speed) / 100);
**Важное замечание:** Скорость делится на 100. Это, скорее всего, артефакт настройки или необходимость калибровки под конкретные параметры физики Matter в примере. В вашем проекте этот делитель, вероятно, нужно будет убрать или изменить.
Настройка физики Matter
Для корректной работы примера используется физический движок Matter, а не Arcade. Это видно в конфигурации игры и способе создания спрайта (this.matter.add.image).
physics: {
default: 'matter',
matter: {
gravity: {
scale: 0
}
}
}
Гравитация отключена (scale: 0), чтобы объект не падал вниз. Matter предоставляет более сложную и реалистичную физическую модель. Использование setVelocityX и setVelocityY для Matter-тела напрямую задает его скорость, игнорируя силы (при нулевом трении).
Что попробовать дальше
Пример наглядно показывает два принципа движения по пути: непрерывный (через касательный вектор) и дискретный (через последовательность точек). Рабочий код с moveToXY проще для понимания и контроля времени, но может создавать ломаную траекторию между точками. Закомментированный метод в update дает идеально гладкое движение по кривой.
**Идеи для экспериментов:**
1. Раскомментируйте блок в update и закомментируйте вызов nextPoint в create, чтобы увидеть плавное движение по эллипсу.
2. Измените тип кривой с Ellipse на CubicBezier, раскомментировав соответствующие строки, чтобы создать произвольный S-образный путь.
3. Поэкспериментируйте со значением делителя скорости ( / 100 ) в moveToXY и параметром setFrictionAir, чтобы добиться желаемой инерции и резкости движения.
4. Используйте curve.getLength() и curve.getPointAt(distance) для движения с постоянной скоростью, а не за фиксированное время.
