О чем этот пример
Кубические кривые Безье — мощный инструмент для создания плавных и естественных траекторий движения в играх: от полёта снарядов до анимации интерфейсов. Однако их математическая природа может казаться сложной. Эта статья на практическом примере показывает, как визуализировать и понять логику построения кривой Безье в реальном времени. Вы научитесь не только создавать кривые, но и отлаживать их, что критически важно для тонкой настройки движения игровых объектов.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
drawGraphics;
graphics;
line5;
line4;
line3;
line2;
line1;
p3;
p2;
p1;
p0;
curve;
path;
create ()
{
// Create a Canvas based texture to draw on
this.textures.createCanvas('curve', 800, 600);
this.add.image(0, 0, 'curve').setOrigin(0);
this.drawGraphics = this.add.graphics();
this.graphics = this.add.graphics();
this.path = { t: 0, vec: new Phaser.Math.Vector2() };
this.p0 = new Phaser.Math.Vector2(100, 500);
this.p1 = new Phaser.Math.Vector2(50, 100);
this.p2 = new Phaser.Math.Vector2(600, 100);
this.p3 = new Phaser.Math.Vector2(700, 550);
this.curve = new Phaser.Curves.CubicBezier(this.p0, this.p1, this.p2, this.p3);
this.line1 = new Phaser.Curves.Line(this.p0, this.p1);
this.line2 = new Phaser.Curves.Line(this.p1, this.p2);
this.line3 = new Phaser.Curves.Line(this.p2, this.p3);
this.line4 = new Phaser.Curves.Line(new Phaser.Math.Vector2(), new Phaser.Math.Vector2());
this.line5 = new Phaser.Curves.Line(new Phaser.Math.Vector2(), new Phaser.Math.Vector2());
this.tweens.add({
targets: this.path,
t: 1,
ease: 'Sine.easeInOut',
duration: 8000,
yoyo: true,
repeat: -1
});
}
update ()
{
this.graphics.clear();
// Debug lines
this.graphics.lineStyle(6, 0xababab, 1);
this.graphics.lineBetween(this.p0.x, this.p0.y, this.p1.x, this.p1.y);
this.graphics.lineBetween(this.p1.x, this.p1.y, this.p2.x, this.p2.y);
this.graphics.lineBetween(this.p2.x, this.p2.y, this.p3.x, this.p3.y);
// Between p0 and p1
this.graphics.lineStyle(2, 0x00ff00, 1);
this.graphics.fillStyle(0x00ff00, 1);
const t1 = this.line1.getPoint(this.path.t);
const t2 = this.line2.getPoint(this.path.t);
const t3 = this.line3.getPoint(this.path.t);
this.graphics.fillCircle(t1.x, t1.y, 6);
this.graphics.fillCircle(t2.x, t2.y, 6);
this.graphics.fillCircle(t3.x, t3.y, 6);
this.graphics.lineBetween(t1.x, t1.y, t2.x, t2.y);
this.graphics.lineBetween(t2.x, t2.y, t3.x, t3.y);
this.graphics.lineStyle(2, 0x0000ff, 1);
this.graphics.fillStyle(0x0000ff, 1);
this.line4.p0.copy(t1);
this.line4.p1.copy(t2);
this.line5.p0.copy(t2);
this.line5.p1.copy(t3);
const t4 = this.line4.getPoint(this.path.t);
const t5 = this.line5.getPoint(this.path.t);
this.graphics.lineBetween(t4.x, t4.y, t5.x, t5.y);
this.graphics.fillCircle(t4.x, t4.y, 6);
this.graphics.fillCircle(t5.x, t5.y, 6);
// Bezier
this.curve.getPoint(this.path.t, this.path.vec);
this.graphics.fillStyle(0xff0000, 1);
this.graphics.fillCircle(this.path.vec.x, this.path.vec.y, 8);
this.drawGraphics.clear();
this.drawGraphics.fillStyle(0xff0000, 0.1);
this.drawGraphics.fillCircle(this.path.vec.x, this.path.vec.y, 4);
this.drawGraphics.generateTexture('curve', 800, 600);
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#efefef',
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Подготовка сцены и объектов кривых
В методе create() происходит базовая настройка. Сначала создаётся Canvas-текстура 'curve', которая будет использоваться для отрисовки следа движения. Затем инициализируются два объекта Graphics: drawGraphics для следа и graphics для отладочной визуализации.
Определяются четыре опорные точки (p0, p1, p2, p3), которые задают форму кривой Безье. На их основе создаются сама кривая и три линейные кривые (Line), соединяющие эти точки — это будут контрольные линии.
Два дополнительных объекта Line (line4 и line5) создаются с нулевыми векторами. Они будут динамически пересчитываться в update() для визуализации промежуточных этапов построения.
this.path = { t: 0, vec: new Phaser.Math.Vector2() };
this.p0 = new Phaser.Math.Vector2(100, 500);
this.p1 = new Phaser.Math.Vector2(50, 100);
this.p2 = new Phaser.Math.Vector2(600, 100);
this.p3 = new Phaser.Math.Vector2(700, 550);
this.curve = new Phaser.Curves.CubicBezier(this.p0, this.p1, this.p2, this.p3);
Для анимации параметра `t(от 0 до 1) используетсяthis.tweens.add`. Этот параметр определяет положение точки вдоль всей кривой и промежуточных линий.
this.tweens.add({
targets: this.path,
t: 1,
ease: 'Sine.easeInOut',
duration: 8000,
yoyo: true,
repeat: -1
});
Отрисовка контрольного многоугольника и первого шага де Кастельжо
Каждый кадр в update() начинается с очистки графики отладочных линий. Первым делом рисуется серый контрольный многоугольник — ломаная, соединяющая опорные точки p0-p1-p2-p3. Это основа, определяющая форму будущей кривой.
this.graphics.lineStyle(6, 0xababab, 1);
this.graphics.lineBetween(this.p0.x, this.p0.y, this.p1.x, this.p1.y);
this.graphics.lineBetween(this.p1.x, this.p1.y, this.p2.x, this.p2.y);
this.graphics.lineBetween(this.p2.x, this.p2.y, this.p3.x, this.p3.y);
Затем, используя текущее значение this.path.t, находятся точки на каждом из трёх отрезков контрольного многоугольника. Эти точки (t1, t2, t3) визуализируются зелёными кружками. Алгоритм де Кастельжо на первом шаге соединяет эти точки, образуя два новых зелёных отрезка (t1-t2 и t2-t3).
const t1 = this.line1.getPoint(this.path.t);
const t2 = this.line2.getPoint(this.path.t);
const t3 = this.line3.getPoint(this.path.t);
this.graphics.lineBetween(t1.x, t1.y, t2.x, t2.y);
this.graphics.lineBetween(t2.x, t2.y, t3.x, t3.y);
Второй шаг алгоритма и финальная точка кривой
На втором шаге алгоритма зелёные отрезки (t1-t2) и (t2-t3) рассматриваются как новые «контрольные линии». Их конечные точки копируются в заранее заготовленные объекты line4 и line5.
this.line4.p0.copy(t1);
this.line4.p1.copy(t2);
this.line5.p0.copy(t2);
this.line5.p1.copy(t3);
Для этих новых линий снова вычисляются точки (t4 и t5) по тому же параметру `t`. Они отрисовываются синими кружками и соединяются синей линией. Это предпоследний этап построения.
const t4 = this.line4.getPoint(this.path.t);
const t5 = this.line5.getPoint(this.path.t);
this.graphics.lineBetween(t4.x, t4.y, t5.x, t5.y);
Наконец, на синем отрезке находится итоговая точка кривой Безье для данного `t. Она вычисляется методомthis.curve.getPoint()` и отображается большим красным кружком.
this.curve.getPoint(this.path.t, this.path.vec);
this.graphics.fillCircle(this.path.vec.x, this.path.vec.y, 8);
Созвание следа траектории с помощью Canvas-текстуры
Чтобы визуализировать всю траекторию движения красной точки, используется отдельный объект Graphics (drawGraphics) и техника Canvas-текстуры. Каждый кадр на месте текущей точки рисуется полупрозрачный маленький красный кружок с низкой альфа-каналом (0.1).
this.drawGraphics.fillStyle(0xff0000, 0.1);
this.drawGraphics.fillCircle(this.path.vec.x, this.path.vec.y, 4);
Затем эта графика экспортируется обратно в ту же самую Canvas-текстуру 'curve', которая отображается как изображение в сцене. Так как текстура не очищается между кадрами, на ней накапливается след, образуя видимый путь кривой.
this.drawGraphics.generateTexture('curve', 800, 600);
Этот приём полезен для отладки путей движения частиц или камеры, когда нужно увидеть историю перемещения.
Что попробовать дальше
Визуализация алгоритма де Кастельжо делает абстрактную кривую Безье осязаемой. Вы можете менять координаты опорных точек p0-p3 прямо в коде, чтобы сразу видеть, как преобразуется форма кривой и её контрольная структура. Для экспериментов попробуйте привязать опорные точки к спрайтам с физикой или сделать параметр `t` зависимым от скорости игрока. Понимание этого механизма открывает путь к созданию сложных нелинейных траекторий для врагов, кат-сцен или плавных UI-анимаций.
