О чем этот пример
Кривые Безье — мощный инструмент для создания плавных траекторий движения объектов, путей камеры или органических форм в играх. Часто недостаточно просто знать положение точки на кривой — нужно понимать, куда она "смотрит", то есть её направление. Эта статья покажет, как получить касательные векторы в любой точке кубической кривой Безье в 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(), или создайте эффект "рельсов", где враги движутся по пути, но могут отклоняться по нормальному вектору при атаке.
