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

Создание плавных и предсказуемых траекторий — основа для анимации врагов, полёта снарядов или камеры в 2D-играх. Встроенная кривая Безье в Phaser 3 позволяет легко описать сложный изгиб, но для равномерного перемещения объекта недостаточно просто получить точки с фиксированным шагом по кривой. В этой статье разберём метод `getSpacedPoints`, который решает эту проблему, и покажем, как его применять для создания точных и контролируемых путей.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    create ()
    {
        const graphics = this.add.graphics();

        const p0 = new Phaser.Math.Vector2(100, 500);
        const p1 = new Phaser.Math.Vector2(50, 100);
        const p2 = new Phaser.Math.Vector2(600, 100);
        const p3 = new Phaser.Math.Vector2(700, 500);

        const curve = new Phaser.Curves.CubicBezier(p0, p1, p2, p3);

        graphics.lineStyle(1, 0xffffff, 1);

        curve.draw(graphics, 64);

        //  Get 20 points equally spaced out along the curve
        const points = curve.getSpacedPoints(20);

        //  Draw the points
        graphics.fillStyle(0xff0000, 1);

        for (let i = 0; i < points.length; i++)
        {
            graphics.fillCircle(points[i].x, points[i].y, 4);
        }
    }
}

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

const game = new Phaser.Game(config);

Проблема неравномерного шага на кривой

Кривая Безье задаётся параметрически (параметр `tот 0 до 1). Если взять точки с фиксированным шагом поt(например,t = 0, 0.1, 0.2...`), они будут распределены неравномерно по длине кривой. На участках с малым искривлением точки окажутся далеко друг от друга, а на резких поворотах — слишком близко. Это приводит к рваному, ускоряющемуся и замедляющемуся движению объекта, что выглядит неестественно.

Для решения Phaser предоставляет метод getSpacedPoints, который рассчитывает точки, равномерно распределённые именно по длине кривой, а не по её параметру.

Создание кубической кривой Безье

Вначале нужно определить кривую. Кубическая кривая Безье задаётся четырьмя точками: начальной, двумя контрольными и конечной. В Phaser для этого используется класс Phaser.Curves.CubicBezier.

const p0 = new Phaser.Math.Vector2(100, 500);
const p1 = new Phaser.Math.Vector2(50, 100);
const p2 = new Phaser.Math.Vector2(600, 100);
const p3 = new Phaser.Math.Vector2(700, 500);

const curve = new Phaser.Curves.CubicBezier(p0, p1, p2, p3);

Здесь p0 и p3 — это начало и конец пути. Точки p1 и p2 — контрольные, они «притягивают» к себе кривую, формируя её изгиб. Для визуализации кривую можно нарисовать с помощью graphics.

Получение равномерно распределённых точек

Ключевой метод — getSpacedPoints. Он принимает количество точек (или массив для заполнения) и возвращает массив объектов Vector2. Эти точки будут находиться на равном расстоянии друг от друга вдоль всей длины кривой.

//  Get 20 points equally spaced out along the curve
const points = curve.getSpacedPoints(20);

Внутри метод выполняет сложную работу: сначала вычисляет примерную длину кривой, затем итеративно находит такие значения параметра `t`, которые соответствуют равным отрезкам длины. Это гарантирует, что объект, перемещающийся от точки к точке, будет двигаться с постоянной скоростью.

Визуализация результата

Чтобы убедиться в равномерности, нарисуем полученные точки. Используем тот же объект graphics, что и для кривой.

graphics.fillStyle(0xff0000, 1);

for (let i = 0; i < points.length; i++)
{
    graphics.fillCircle(points[i].x, points[i].y, 4);
}

Если запустить этот код, вы увидите красные точки, равномерно «рассыпанные» вдоль всей белой кривой. Расстояние между соседними точками по прямой будет примерно одинаковым, что подтверждает корректность работы метода.

Практическое применение в играх

Массив точек, полученный от getSpacedPoints, — это готовый маршрут для движения. Его можно использовать в игровом цикле update для перемещения спрайта.

// В create()
this.sprite = this.add.sprite(points[0].x, points[0].y, 'texture');
this.currentPointIndex = 0;
this.pathPoints = points; // массив от curve.getSpacedPoints(50)

// В update()
if (this.currentPointIndex < this.pathPoints.length)
{
    const target = this.pathPoints[this.currentPointIndex];
    // Плавное движение к целевой точке (lerp или moveToObject)
    Phaser.Math.MoveTo(this.sprite, target, speed);
    // Если достигли точки, переходим к следующей
    if (Phaser.Math.Distance.Between(this.sprite.x, this.sprite.y, target.x, target.y) < 1)
    {
        this.currentPointIndex++;
    }
}

Этот подход идеален для патрулирования врагов, траекторий снарядов «бумеранг» или плавного пролёта камеры по сцене.

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

Метод getSpacedPoints — это мощный и простой инструмент для создания плавного и физически корректного движения по сложным траекториям в Phaser 3. Он избавляет разработчика от ручных расчётов и гарантирует постоянную скорость объекта. Для экспериментов попробуйте: анимировать несколько объектов с разной скоростью по одной кривой, изменять контрольные точки кривой в реальном времени (например, в ответ на действия игрока) или комбинировать несколько кривых в один непрерывный путь для создания сложных маршрутов.