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

Кривые Безье — мощный инструмент для создания плавных траекторий движения объектов, путей камеры или органических форм в играх. Часто недостаточно просто знать положение точки на кривой — нужно понимать, куда она "смотрит", то есть её направление. Эта статья покажет, как получить касательные векторы в любой точке кубической кривой Безье в Phaser. Это знание открывает двери для выравнивания спрайтов вдоль пути, расчета отскоков, создания рельсовых систем или плавного поворота камеры, следующей за игроком по сложной траектории.

Версия 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);

        const max = 16;
        const points = [];
        const tangents = [];

        for (let c = 0; c <= max; c++)
        {
            const t = curve.getUtoTmapping(c / max);

            points.push(curve.getPoint(t));
            tangents.push(curve.getTangent(t));
        }

        const tempVec = new Phaser.Math.Vector2();

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

        for (let i = 0; i < points.length; i++)
        {
            const p = points[i];

            graphics.fillCircle(p.x, p.y, 6);

            //  Draw the tangent vector
            tempVec.copy(tangents[i]).scale(32).add(p);

            graphics.lineStyle(1, 0x00ff00, 1);
            graphics.lineBetween(p.x, p.y, tempVec.x, tempVec.y);

            //  Draw the right-hand tangent vector
            tempVec.copy(tangents[i]).normalizeRightHand().scale(-32).add(p);

            graphics.lineStyle(1, 0xff00ff, 1);
            graphics.lineBetween(p.x, p.y, tempVec.x, tempVec.y);
        }
    }
}

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

const game = new Phaser.Game(config);

Создание кривой Безье и подготовка данных

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

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);

const max = 16;
const points = [];
const tangents = [];

Получение точек и касательных векторов

Ключевой шаг — пройтись по кривой с равномерным шагом и в каждой позиции запросить два значения: саму точку и вектор касательной. Параметр `t(от 0 до 1) определяет положение на кривой. Однако для равномерного распределения точек по длине кривой используется методgetUtoTmapping`.

for (let c = 0; c <= max; c++)
{
    const t = curve.getUtoTmapping(c / max);

    points.push(curve.getPoint(t));
    tangents.push(curve.getTangent(t));
}

* curve.getUtoTmapping(c / max): Преобразует равномерное значение c/max в параметр `t`, учитывающий нелинейную скорость движения по кривой. Это дает более равномерное распределение точек. * curve.getPoint(t): Возвращает объект Vector2 с координатами (x, y) точки на кривой для параметра `t`. * curve.getTangent(t): Возвращает объект Vector2, представляющий касательный вектор в точке `t`. Это вектор, указывающий направление кривой в данной точке. Важно: это именно **направление**, а не позиция.

Визуализация: точки и векторы

Для наглядности мы рисуем каждую рассчитанную точку и её касательный вектор. Касательный вектор масштабируется, чтобы его было лучше видно, и отрисовывается как линия из точки в направлении вектора.

const tempVec = new Phaser.Math.Vector2();

graphics.fillStyle(0xff0000, 1);

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

    //  Рисуем касательный вектор
    tempVec.copy(tangents[i]).scale(32).add(p);
    graphics.lineStyle(1, 0x00ff00, 1);
    graphics.lineBetween(p.x, p.y, tempVec.x, tempVec.y);
}

1. Создается временный вектор tempVec для вычислений. 2. Для каждой точки `p` рисуется красный круг. 3. Касательный вектор tangents[i] копируется в tempVec. 4. Метод .scale(32) увеличивает длину вектора в 32 раза. 5. Метод .add(p) сдвигает начало вектора из мирового центра (0,0) в точку `p` на кривой. 6. Зеленая линия рисуется от точки `pдо конца модифицированного вектораtempVec`.

Правое нормальное направление

Часто требуется не просто направление вперед по кривой, а направление, перпендикулярное ему (например, для вычисления нормали при столкновении). Метод normalizeRightHand() вектора возвращает перпендикуляр, повернутый на 90 градусов по часовой стрелке.

//  Рисуем правосторонний перпендикулярный вектор
tempVec.copy(tangents[i]).normalizeRightHand().scale(-32).add(p);
graphics.lineStyle(1, 0xff00ff, 1);
graphics.lineBetween(p.x, p.y, tempVec.x, tempVec.y);

1. Цепочка методов начинается с копирования исходного касательного вектора. 2. normalizeRightHand() превращает его в единичный вектор, направленный вправо от направления движения. 3. .scale(-32) разворачивает его в противоположную сторону (влево) и задает длину. 4. Фиолетовая линия визуализирует этот "левый" нормальный вектор относительно направления кривой.

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

Работа с касательными векторами кривых Безье в Phaser — это фундамент для создания сложной и плавной динамики в вашей игре. Вы можете использовать эти векторы для автоматического поворота спрайта движущегося по пути (sprite.rotation = tangent.angle()), расчета угла отскока мяча от изогнутой стены или смещения объектов (например, частиц или тропинки) вбок от основной траектории. Попробуйте заставить спрайт двигаться по кривой, плавно поворачиваясь в направлении getTangent(), или создайте эффект "рельсов", где враги движутся по пути, но могут отклоняться по нормальному вектору при атаке.