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

В игровом движке 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) для движения с постоянной скоростью, а не за фиксированное время.