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

В играх часто нужно, чтобы объекты плавно двигались по заданному пути — например, вражеский корабль по траектории или камера в сцене. Ручной расчёт таких путей сложен и негибок. Phaser предоставляет мощный инструмент — класс `Phaser.Curves`. В этой статье на конкретном примере разберём, как создать гладкую кривую по точкам и как в реальном времени перемещать по ней объект, используя координаты указателя мыши. Этот подход универсален и может быть использован для патрулирования, анимации движения или создания динамических траекторий.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    create ()
    {
        const graphics = this.add.graphics().lineStyle(1, 0xffffff, 1);

        const points = [
            50, 300,
            164, 246,
            274, 302,
            412, 400,
            522, 241,
            664, 364,
            750, 400
        ];

        const startX = points[0];
        const endX = points[points.length - 2];

        const curve = new Phaser.Curves.Spline(points);

        curve.draw(graphics, 64);

        const point = curve.getPointAt(0);

        const location = this.add.rectangle(point.x, point.y, 16, 16, 0xff00ff, 0.8);

        this.input.on('pointermove', pointer => {

            //  getPointAt requires a value between 0 and 1 (start and end of curve)
            //  We know the start and end x coordinate of the curve, so we can calculate it from that

            let px = pointer.worldX;
            const distance = endX - startX;

            if (px >= startX && px <= endX)
            {
                px -= startX;

                curve.getPointAt(px / distance, point);

                location.setPosition(point.x, point.y);
            }

        });
    }
}

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

const game = new Phaser.Game(config);

Создание кривой сплайна

Класс Phaser.Curves.Spline позволяет построить гладкую кривую, проходящую через заданные точки. Это удобно для создания органичных, нелинейных путей.

Мы создаём массив points, где координаты идут последовательно: x1, y1, x2, y2 и так далее. Затем этот массив передаётся в конструктор сплайна.

const points = [
    50, 300,
    164, 246,
    274, 302,
    412, 400,
    522, 241,
    664, 364,
    750, 400
];

const curve = new Phaser.Curves.Spline(points);

Чтобы визуализировать кривую на сцене, мы используем объект Graphics. Метод curve.draw отрисует кривую с заданным разрешением (количеством отрезков для аппроксимации).

const graphics = this.add.graphics().lineStyle(1, 0xffffff, 1);
curve.draw(graphics, 64);

Получение точки на кривой

Ключевой метод для работы с позицией на кривой — curve.getPointAt(t). Параметр `t— это нормализованное значение от 0 до 1, где 0 соответствует началу кривой, а 1 — её концу. Метод возвращает или обновляет переданный ему объектPhaser.Math.Vector2` с координатами (x, y) в этой позиции.

В примере мы сразу получаем стартовую точку кривой (t = 0) и создаём в этом месте прямоугольник (location), который будем двигать.

const point = curve.getPointAt(0);
const location = this.add.rectangle(point.x, point.y, 16, 16, 0xff00ff, 0.8);

Важно: метод getPointAt ожидает именно нормализованное значение. Если передать, например, 0.5, мы получим точку ровно посередине кривой.

Привязка движения объекта к курсору

Задача: перемещать прямоугольник по кривой в зависимости от горизонтального положения курсора мыши.

Сложность в том, что координата указателя pointer.worldX — это абсолютное значение в пикселях, а getPointAt требует значение от 0 до 1. Необходимо выполнить преобразование.

1. Мы заранее вычисляем начальную (startX) и конечную (endX) координату X нашей кривой, взяв их из массива точек. 2. Вычисляем общую длину пути по оси X: distance = endX - startX. 3. В обработчике события pointermove проверяем, находится ли курсор в пределах кривой по оси X. 4. Если да, то смещаем координату курсора относительно начала кривой: px -= startX. 5. Теперь px — это расстояние от начала кривой до курсора. Чтобы получить нормализованное значение `t, делим это расстояние на общую длину:px / distance`.

const startX = points[0];
const endX = points[points.length - 2]; // Предпоследний элемент — это X последней точки

this.input.on('pointermove', pointer => {
    let px = pointer.worldX;
    const distance = endX - startX;

    if (px >= startX && px <= endX) {
        px -= startX;
        // Преобразуем координату курсора в значение от 0 до 1
        curve.getPointAt(px / distance, point);
        // Обновляем позицию прямоугольника
        location.setPosition(point.x, point.y);
    }
});

Обратите внимание: второй аргумент point в getPointAt — это ссылка на ранее созданный объект вектор. Метод обновит его координаты, что эффективнее, чем создание нового объекта каждый кадр.

Сборка сцены и конфигурация игры

Код примера является полноценной сценой Phaser 3. В методе create происходит вся инициализация, описанная выше.

Конфигурационный объект игры задаёт базовые параметры: тип рендерера, размер холста, цвет фона и корневую сцену.

class Example extends Phaser.Scene {
    create () {
        // ... весь код из примеров выше
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#2d2d2d',
    parent: 'phaser-example',
    scene: Example // Указываем наш класс сцены
};

const game = new Phaser.Game(config);

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

Использование Phaser.Curves.Spline и метода getPointAt — это мощный и элегантный способ управления движением по сложным траекториям. Вы можете экспериментировать: привязать движение не к курсору, а к времени для создания циклической анимации, использовать curve.getPoints(quantity) для получения массива точек и перемещать объект между ними, или комбинировать несколько кривых для создания сложного маршрута. Этот механизм отлично подходит для создания траекторий снарядов, путей камеры или маршрутов патрулирования NPC.