О чем этот пример
Создание интерактивных кривых и их визуализация — ключевая задача для разработки траекторий движения, путей камеры или плавных анимаций. В этом примере мы покажем, как создать сплайн, который можно редактировать прямо в реальном времени, перетаскивая его контрольные точки. Вы научитесь работать с классом `Phaser.Curves.Spline`, вычислять его ограничивающий прямоугольник (`bounds`) и анимировать движение точки по кривой. Это практический фундамент для создания сложных и динамических игровых систем.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
path;
curve;
bounds;
points;
graphics;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.spritesheet('dragcircle', 'assets/sprites/dragcircle.png', { frameWidth: 16 });
}
create ()
{
this.path = { t: 0, vec: new Phaser.Math.Vector2() };
this.bounds = new Phaser.Geom.Rectangle();
this.curve = new Phaser.Curves.Spline([
20, 550,
260, 450,
300, 250,
550, 145,
745, 256
]);
this.points = this.curve.points;
this.curve.getBounds(this.bounds);
// Create drag-handles for each point
for (var i = 0; i < this.points.length; i++)
{
var point = this.points[i];
var handle = this.add.image(point.x, point.y, 'dragcircle', 0)
.setName(i)
.setInteractive();
handle.setData('vector', point);
this.input.setDraggable(handle, true);
}
this.input.on(Phaser.Input.Events.DRAG_START, (pointer, gameObject) => {
gameObject.setFrame(1);
});
this.input.on(Phaser.Input.Events.DRAG, (pointer, gameObject) => {
gameObject.x = pointer.x;
gameObject.y = pointer.y;
gameObject.setData('vector', { x: pointer.x, y: pointer.y });
// Update the this.curve and this.bounds
const index = gameObject.name;
this.curve.points[index].x = gameObject.getData('vector').x;
this.curve.points[index].y = gameObject.getData('vector').y;
this.curve.getBounds(this.bounds);
});
this.input.on(Phaser.Input.Events.DRAG_END, (pointer, gameObject) => {
gameObject.setFrame(0);
});
this.tweens.add({
targets: this.path,
t: 1,
ease: 'Sine.easeInOut',
duration: 2000,
yoyo: true,
repeat: -1
});
this.graphics = this.add.graphics();
}
update ()
{
this.graphics.clear();
// Draw the this.bounds
this.graphics.lineStyle(1, 0x00ff00, 1).strokeRectShape(this.bounds);
// Draw the this.curve through the this.points
this.graphics.lineStyle(2, 0xffffff, 1);
this.curve.draw(this.graphics, 64);
// Draw t
this.curve.getPoint(this.path.t, this.path.vec);
this.graphics.fillStyle(0xffff00, 1);
this.graphics.fillCircle(this.path.vec.x, this.path.vec.y, 8);
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#2d2d2d',
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Создание сплайна и его bounding box
В методе create() мы инициализируем кривую и её геометрические границы.
Первым шагом создаётся объект this.path, который будет хранить текущую позицию на кривой (параметр `t` от 0 до 1) и вектор для её вычисления.
Сплайн строится по набору точек. Мы передаём массив координат [x1, y1, x2, y2, ...] в конструктор Phaser.Curves.Spline. В примере используется пять точек.
Очень важный метод — getBounds. Он вычисляет минимальный прямоугольник, который полностью содержит кривую, и записывает результат в заранее созданный объект Phaser.Geom.Rectangle. Этот прямоугольник (bounds) будет визуализироваться.
this.path = { t: 0, vec: new Phaser.Math.Vector2() };
this.bounds = new Phaser.Geom.Rectangle();
this.curve = new Phaser.Curves.Spline([
20, 550,
260, 450,
300, 250,
550, 145,
745, 256
]);
this.curve.getBounds(this.bounds);
Интерактивные контрольные точки
Чтобы кривая стала динамической, мы создаём drag-handles для каждой её точки. Используется спрайт из spritesheet 'dragcircle'.
Ключевой момент: мы связываем графический объект (handle) с математической точкой кривой (point). Для этого используется метод setData, который присваивает объекту произвольные данные. В данном случае мы сохраняем вектор точки.
Также каждому handle присваивается имя (setName), соответствующее его индексу в массиве точек кривой. Это позволит нам знать, какую именно точку кривой мы перемещаем при drag событии.
for (var i = 0; i < this.points.length; i++) {
var point = this.points[i];
var handle = this.add.image(point.x, point.y, 'dragcircle', 0)
.setName(i)
.setInteractive();
handle.setData('vector', point);
this.input.setDraggable(handle, true);
}
Обработка drag событий и обновление кривой
Когда пользователь начинает, продолжает или завершает перетаскивание handle, система событий Phaser (DRAG_START, DRAG, DRAG_END) реагирует на это.
Самое важное происходит в событии `DRAG`. Здесь мы:
1. Обновляем позицию графического handle согласно позиции указателя (`pointer`).
2. Обновляем данные handle (`setData`) новым вектором.
3. По имени (`gameObject.name`, которое равно индексу) находим соответствующую точку в массиве `this.curve.points` и меняем её координаты.
4. После изменения точек кривой необходимо пересчитать её bounding box. Мы вызываем `this.curve.getBounds(this.bounds)` снова.
Таким образом, кривая и её границы мгновенно реагируют на действия пользователя.
this.input.on(Phaser.Input.Events.DRAG, (pointer, gameObject) => {
gameObject.x = pointer.x;
gameObject.y = pointer.y;
gameObject.setData('vector', { x: pointer.x, y: pointer.y });
const index = gameObject.name;
this.curve.points[index].x = gameObject.getData('vector').x;
this.curve.points[index].y = gameObject.getData('vector').y;
this.curve.getBounds(this.bounds);
});
Анимированное движение точки по кривой
Чтобы визуализировать, как объект может двигаться по созданной кривой, мы используем tween. Он циклически меняет значение this.path.t от 0 до 1 и обратно (с параметрами yoyo и repeat).
В методе `update()` мы каждый кадр:
1. Очищаем графический контейнер (`this.graphics.clear()`).
2. Рисуем зеленый bounding box с помощью `strokeRectShape`.
3. Рисуем белый контур самой кривой. Метод `draw` отрисовывает сплайн с заданным количеством сегментов (64).
4. Используя текущее значение `t`, получаем точку на кривой методом `getPoint`. Результат записывается в вектор `this.path.vec`.
5. Рисуем желтый кружок в этой позиции.
this.tweens.add({
targets: this.path,
t: 1,
ease: 'Sine.easeInOut',
duration: 2000,
yoyo: true,
repeat: -1
});
// В update():
this.curve.getPoint(this.path.t, this.path.vec);
this.graphics.fillStyle(0xffff00, 1);
this.graphics.fillCircle(this.path.vec.x, this.path.vec.y, 8);
Что попробовать дальше
Этот пример демонстрирует мощный инструмент для создания интерактивных траекторий. Вы можете использовать эту систему для определения пути камеры, следования врагов по сложному маршруту или создания плавных анимаций UI. Для экспериментов попробуйте: зафиксировать некоторые точки кривой, чтобы их нельзя было перемещать; добавить несколько объектов, движущихся по кривой с разной скоростью; или использовать полученный bounding box для проверки коллизий с другими объектами игры.
