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

Создание сложных и плавных путей для движения объектов — частая задача в разработке игр. Прямые линии выглядят неестественно, а ручная отрисовка кривых требует много усилий. В этом примере мы разберем, как динамически генерировать изящную сплайн-кривую, используя математику Phaser для расчета контрольных точек. Этот подход полезен для создания траекторий полета снарядов, путей патрулирования врагов или просто визуальных эффектов.

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

Живой запуск

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

Исходный код


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

        const line = new Phaser.Geom.Line(50, 400, 700, 200);
        // let line = new Phaser.Geom.Line(700, 500, 100, 300);

        graphics.fillStyle(0xff0000, 1);
        graphics.fillCircle(line.x1, line.y1, 8);
        graphics.fillCircle(line.x2, line.y2, 8);

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

        const points = [];

        points.push(line.getPointA());

        const waves = 4;

        let vx = 100;
        let vy = 100;
        let prevX = line.x1;
        let prevY = line.y1;

        for (let i = 1; i <= waves; i++)
        {
            const currentPoint = line.getPoint(i / waves);

            graphics.fillStyle(0xffff00).fillCircle(currentPoint.x, currentPoint.y, 4);

            const ray = new Phaser.Geom.Line(prevX, prevY, currentPoint.x, currentPoint.y);

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

            const normal = Phaser.Geom.Line.GetNormal(ray);
            const midPoint = Phaser.Geom.Line.GetMidPoint(ray);

            graphics.fillStyle(0x00ff00).fillCircle(midPoint.x + normal.x * vx, midPoint.y + normal.y * vy, 4);

            points.push(new Phaser.Math.Vector2(midPoint.x + normal.x * vx, midPoint.y + normal.y * vy));

            prevX = currentPoint.x;
            prevY = currentPoint.y;

            vy *= -1;
        }

        points.push(line.getPointB());

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

        graphics.lineStyle(1, 0xffffff, 1);
        curve.draw(graphics, 64);
    }

}

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

const game = new Phaser.Game(config);


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

Вся работа происходит в методе create нашей сцены. Первым делом мы создаем объект Graphics для рисования и определяем исходный отрезок, который будет основой для нашей будущей кривой.

const graphics = this.add.graphics();
const line = new Phaser.Geom.Line(50, 400, 700, 200);

Мы сразу визуализируем концы этого отрезка красными кружками, чтобы видеть начальную и конечную точки нашего пути.

graphics.fillStyle(0xff0000, 1);
graphics.fillCircle(line.x1, line.y1, 8);
graphics.fillCircle(line.x2, line.y2, 8);

Разбиваем линию и находим контрольные точки

Задача — превратить прямую линию в волнистую кривую. Для этого мы разобьем исходный отрезок на несколько равных частей (количество задается переменной waves). В каждой точке разбиения мы будем строить перпендикуляр (нормаль) к маленькому отрезку и откладывать по нему контрольную точку.

Создаем массив points, куда сразу помещаем первую точку отрезка (line.getPointA()). Затем в цикле для каждого сегмента: 1. Находим его конечную точку. 2. Строим луч (ray) от предыдущей точки до текущей. 3. Получаем нормаль к этому лучу с помощью Phaser.Geom.Line.GetNormal. 4. Находим середину луча через Phaser.Geom.Line.GetMidPoint. 5. Смещаемся от середины по направлению нормали на расстояние vx и vy — так получается контрольная точка сплайна.

const currentPoint = line.getPoint(i / waves);
const ray = new Phaser.Geom.Line(prevX, prevY, currentPoint.x, currentPoint.y);
const normal = Phaser.Geom.Line.GetNormal(ray);
const midPoint = Phaser.Geom.Line.GetMidPoint(ray);
points.push(new Phaser.Math.Vector2(midPoint.x + normal.x * vx, midPoint.y + normal.y * vy));

Ключевой трюк — инверсия переменной vy на каждом шаге (vy *= -1). Это заставляет каждую следующую контрольную точку отклоняться в противоположную сторону от линии, создавая характерный волнообразный паттерн.

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

После цикла у нас есть массив points, содержащий начальную точку линии, серию рассчитанных контрольных точек и конечную точку линии. Этот массив идеально подходит для инициализации объекта Phaser.Curves.Spline.

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

Класс Spline принимает массив точек и автоматически строит по ним плавную кривую, которая проходит через все эти точки. Для визуализации мы используем метод draw, который отрисует кривую на нашем контексте graphics с заданным разрешением (количество сегментов для аппроксимации).

graphics.lineStyle(1, 0xffffff, 1);
curve.draw(graphics, 64);

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

Практическое применение и настройка

Сгенерированную кривую можно использовать не только для рисования. Основная мощь объекта curve раскрывается в методах для движения объектов.

// Получить точку на кривой в зависимости от прогресса t (от 0 до 1)
const position = curve.getPoint(0.5);
// Заставить спрайт следовать по пути
this.physics.moveToObject(sprite, position, speed);

Экспериментируя с параметрами, вы сможете создавать разнообразные траектории: - waves: Увеличивает количество "волн" на кривой. - vx и vy: Влияют на амплитуду отклонения контрольных точек. Изменение vx создаст волну вдоль линии, а не поперек. - Исходная линия: Попробуйте использовать вторую закомментированную линию new Phaser.Geom.Line(700, 500, 100, 300), чтобы увидеть, как меняется поведение кривой при другом угле наклона основы.

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

Динамическое создание сплайнов через геометрические вычисления Phaser открывает мощный способ генерации органичных путей. Вы можете управлять сложностью и формой кривой, меняя всего несколько параметров. Для экспериментов попробуйте сделать амплитуду (vx, vy) зависимой от прогресса по линии или используйте полученную кривую в качестве пути для анимации частиц через curve.getPoints(). Это станет основой для создания живых, нелинейных перемещений в вашей игре.