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

Работа с кривыми и путями — основа для создания плавных траекторий движения объектов в играх. Этот пример демонстрирует, как создать простую линейную кривую, визуализировать её, анимировать точку вдоль неё и, что самое важное, сделать концы линии перетаскиваемыми в реальном времени. Такой подход полезен для редакторов уровней, траекторий полёта снарядов или создания гибких путевых точек для NPC.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    graphics;
    curve;
    path;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.spritesheet('dragcircle', 'assets/sprites/dragcircle.png', { frameWidth: 16 });
    }

    create ()
    {
        this.graphics = this.add.graphics();

        this.path = { t: 0, vec: new Phaser.Math.Vector2() };

        this.curve = new Phaser.Curves.Line([ 100, 100, 600, 400 ]);

        // curve = new Phaser.Curves.Line(new Phaser.Math.Vector2(100, 100), new Phaser.Math.Vector2(600, 400));

        const point0 = this.add.image(this.curve.p0.x, this.curve.p0.y, 'dragcircle', 0).setInteractive();
        const point1 = this.add.image(this.curve.p1.x, this.curve.p1.y, 'dragcircle', 0).setInteractive();

        point0.setData('vector', this.curve.p0);
        point1.setData('vector', this.curve.p1);

        this.input.setDraggable([ point0, point1 ]);

        this.input.on('dragstart', (pointer, gameObject) =>
        {

            gameObject.setFrame(1);

        });

        this.input.on('drag', (pointer, gameObject, dragX, dragY) =>
        {

            gameObject.x = dragX;
            gameObject.y = dragY;

            gameObject.data.get('vector').set(dragX, dragY);

        });

        this.input.on('dragend', (pointer, gameObject) =>
        {

            gameObject.setFrame(0);

        });

        this.tweens.add({
            targets: this.path,
            t: 1,
            ease: 'Sine.easeInOut',
            duration: 2000,
            yoyo: true,
            repeat: -1
        });
    }

    update ()
    {
        this.graphics.clear();
        this.graphics.lineStyle(2, 0xffffff, 1);

        this.curve.draw(this.graphics);

        this.curve.getPoint(this.path.t, this.path.vec);

        this.graphics.fillStyle(0xff0000, 1);
        this.graphics.fillRect(this.path.vec.x - 8, this.path.vec.y - 8, 16, 16);
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#2d2d2d',
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка сцены и создание кривой

В методе preload загружается спрайт для визуальных маркеров. Основная работа начинается в create.

Сначала создаётся объект Graphics для отрисовки. Затем определяется объект path, который будет хранить текущую позицию (параметр `t` от 0 до 1) на кривой и вектор для вычислений.

Кривая создаётся с помощью класса Phaser.Curves.Line. Её можно инициализировать массивом координат или двумя объектами Vector2.

this.curve = new Phaser.Curves.Line([100, 100, 600, 400]);
// Альтернатива:
// curve = new Phaser.Curves.Line(new Phaser.Math.Vector2(100, 100), new Phaser.Math.Vector2(600, 400));

Точки p0 и p1 кривой — это векторы, определяющие начало и конец линии.

Интерактивные точки и перетаскивание

Чтобы сделать кривую интерактивной, в позициях её концов размещаются спрайты. Каждому спрайту через setData привязывается ссылка на соответствующий вектор кривой (p0 или p1). Это ключевой момент для связи графического объекта с математическими данными.

point0.setData('vector', this.curve.p0);
point1.setData('vector', this.curve.p1);

Затем спрайты делаются перетаскиваемыми с помощью this.input.setDraggable. Обработчики событий dragstart, drag и dragend меняют кадр спрайта (визуальный feedback) и, что самое важное, обновляют координаты привязанного вектора кривой в реальном времени во время перетаскивания.

this.input.on('drag', (pointer, gameObject, dragX, dragY) => {
    gameObject.x = dragX;
    gameObject.y = dragY;
    gameObject.data.get('vector').set(dragX, dragY);
});

Таким образом, при перемещении маркера сама геометрическая кривая немедленно изменяется.

Анимация движения по кривой

Для создания циклического движения точки вдоль линии используется твин. Он анимирует свойство `tобъектаpathот 0 до 1 и обратно (yoyo: true`).

this.tweens.add({
    targets: this.path,
    t: 1,
    ease: 'Sine.easeInOut',
    duration: 2000,
    yoyo: true,
    repeat: -1
});

Параметр `t(от 0 в начале линии до 1 в её конце) определяет позицию на кривой. В методеupdate` это значение используется для вычисления текущих координат точки.

Визуализация в реальном времени

Каждый кадр в update происходит перерисовка. Сначала очищается холст Graphics, задаётся стиль линии и рисуется сама кривая методом this.curve.draw(this.graphics).

Затем, используя текущее значение this.path.t, вычисляется позиция точки на кривой. Метод getPoint записывает координаты в переданный вектор this.path.vec.

this.curve.getPoint(this.path.t, this.path.vec);

На этих координатах рисуется красный квадрат, который и представляет движущуюся точку. Поскольку перерисовка происходит каждые 16ms (при ~60fps), а кривая обновляется сразу при драге, мы получаем полностью интерактивную и анимированную сцену.

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

Этот пример — отличная основа для множества игровых механик. Вы можете экспериментировать: заменить Line на Spline или Quadratic Bezier для более сложных траекторий, привязать к движущейся точке спрайт игрока или врага, использовать массив таких кривых для построения сложного пути или сохранять конечные позиции точек в JSON для последующей загрузки уровня. Интерактивность кривых открывает двери к созданию внутриигровых редакторов.