О чем этот пример
Кривые в Phaser — это мощный инструмент для создания сложных траекторий движения, плавных анимаций и динамических форм. Этот пример демонстрирует не просто статичный эллипс, а полностью интерактивную кривую, параметры которой можно менять в реальном времени с помощью слайдеров и перетаскивания. Понимание работы кривых открывает двери к созданию орбит планет, патрульных маршрутов для NPC или эффектных меню с плавными переходами.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
graphics;
text;
curve;
path;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.spritesheet('dragcircle', 'assets/sprites/dragcircle.png', { frameWidth: 16 });
}
create ()
{
const sliderGraphics = this.add.graphics();
this.path = { t: 0, vec: new Phaser.Math.Vector2() };
this.curve = new Phaser.Curves.Ellipse(400, 300, 100, 150);
this.createSlider(sliderGraphics, 100, 10, 'width', 500, 0, 400, 100, this.curve.setXRadius);
this.createSlider(sliderGraphics, 100, 30, 'height', 500, 0, 300, 150, this.curve.setYRadius);
this.createSlider(sliderGraphics, 100, 50, 'start', 500, 0, 360, 0, this.curve.setStartAngle);
this.createSlider(sliderGraphics, 100, 70, 'end', 500, 0, 360, 360, this.curve.setEndAngle);
this.createSlider(sliderGraphics, 100, 90, 'angle', 500, 0, 360, 0, this.curve.setRotation);
const centerPoint = this.add.image(this.curve.p0.x, this.curve.p0.y, 'dragcircle', 0).setInteractive();
centerPoint.setData('control', 'center').setData('vector', this.curve.p0);
this.input.setDraggable(centerPoint);
this.input.on('dragstart', (pointer, gameObject) =>
{
gameObject.setFrame(1);
});
this.input.on('drag', (pointer, gameObject, dragX, dragY) =>
{
if (gameObject.data.get('control') === 'center')
{
gameObject.x = dragX;
gameObject.y = dragY;
gameObject.data.get('vector').set(dragX, dragY);
}
});
this.input.on('dragend', (pointer, gameObject) =>
{
gameObject.setFrame(0);
});
this.tweens.add({
targets: this.path,
t: 1,
ease: 'Linear',
duration: 4000,
repeat: -1
});
// Debug graphics
this.graphics = this.add.graphics();
}
update ()
{
this.graphics.clear();
this.graphics.lineStyle(2, 0xffffff, 1);
this.curve.draw(this.graphics, 64);
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);
}
createSlider (graphics, x, y, label, width, min, max, value, callback)
{
// Default value
value = Phaser.Math.Clamp(value, min, max);
graphics.lineStyle(1, 0xffffff, 1);
graphics.lineBetween(x, y + 8, x + width, y + 8);
const text = this.add.text(x - 10, y, `${label}:`, { font: '16px Courier', fill: '#00ff00' }).setOrigin(1, 0);
const textValue = this.add.text(x + width + 10, y, value.toFixed(2), { font: '16px Courier', fill: '#00ff00' });
const image = this.add.image(x, y + 8, 'dragcircle', 0).setInteractive();
image.setData('labelValue', textValue);
image.setData('left', x);
image.setData('right', x + width);
this.input.setDraggable(image);
// Drag limits
image.setData('label', label);
// The range the control is allowed to be within (the actual values, not the percentage or pixels)
image.setData('min', min);
image.setData('max', max);
image.setData('value', value);
// The scale is how many pixels = 1 unit of range
const scale = max / width;
image.setData('scale', scale);
const p = Phaser.Math.Percent(value, min, max);
// console.log('width', width);
// console.log('min', min);
// console.log('max', max);
// console.log('value', value, 'p:', p, '%');
// console.log('scale', scale);
image.x += p * width;
image.setData('callback', callback);
image.on('drag', function (pointer, dragX, dragY)
{
const min = this.getData('min');
const max = this.getData('max');
const scale = this.getData('scale');
const left = this.getData('left');
const right = this.getData('right');
dragX = Phaser.Math.Clamp(dragX, left, right);
this.x = dragX;
// Calculate the value
const value = (dragX - left) * scale;
this.setData('value', value);
this.getData('labelValue').setText(value.toFixed(2));
const callback = this.getData('callback');
callback.call(this.curve, value);
});
// this.input.setOnDragCallback(image, updateSlider, this);
}
updateSlider (handle, pointer, dragX, dragY)
{
const min = handle.getData('min');
const max = handle.getData('max');
const scale = handle.getData('scale');
const left = handle.getData('left');
const right = handle.getData('right');
dragX = Phaser.Math.Clamp(dragX, left, right);
handle.x = dragX;
// Calculate the value
const value = (dragX - left) * scale;
handle.setData('value', value);
handle.getData('labelValue').setText(value.toFixed(2));
const callback = handle.getData('callback');
callback.call(this.curve, value);
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#2d2d2d',
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Создание кривой и базовой структуры
В методе create() сцены инициализируется основная структура примера. Ключевой объект — Phaser.Curves.Ellipse. Он создаёт эллиптическую кривую, заданную центром и радиусами по осям X и Y.
this.curve = new Phaser.Curves.Ellipse(400, 300, 100, 150);
Также создаётся объект this.path, который будет хранить текущую позицию (параметр `t` от 0 до 1) на кривой и вектор для расчётов. Этот объект станет целью твина для анимации движения точки.
this.path = { t: 0, vec: new Phaser.Math.Vector2() };
Интерактивные контролы: слайдеры и draggable-точка
Для управления параметрами эллипса реализованы два типа интерактивных элементов.
**1. Слайдеры:** Функция createSlider() создаёт графическую линию, текстовые метки и draggable-спрайт ("ползунок"). При перетаскивании ползунка вычисляется новое значение в заданном диапазоне и вызывается переданный callback-метод самой кривой (например, setXRadius).
this.createSlider(sliderGraphics, 100, 10, 'width', 500, 0, 400, 100, this.curve.setXRadius);
**2. Центр эллипса:** В центр кривой помещается интерактивный спрайт. При его перетаскивании обновляется вектор this.curve.p0, что мгновенно меняет положение всей кривой.
const centerPoint = this.add.image(this.curve.p0.x, this.curve.p0.y, 'dragcircle', 0).setInteractive();
centerPoint.setData('control', 'center').setData('vector', this.curve.p0);
this.input.setDraggable(centerPoint);
Обработчики событий dragstart, drag и dragend меняют кадр спрайта и обновляют данные кривой.
Анимация движения по кривой
Чтобы визуализировать путь, по кривой непрерывно движется жёлтая точка. Её движение реализовано через твин, который циклически меняет значение `tобъектаthis.path` от 0 до 1.
this.tweens.add({
targets: this.path,
t: 1,
ease: 'Linear',
duration: 4000,
repeat: -1
});
В методе update() на каждом кадре кривая заново отрисовывается с помощью curve.draw(), а затем вычисляется текущая точка на ней для отрисовки движущегося кружка.
this.curve.getPoint(this.path.t, this.path.vec);
this.graphics.fillCircle(this.path.vec.x, this.path.vec.y, 8);
Важно: getPoint() модифицирует переданный в него вектор this.path.vec, что эффективно с точки зрения производительности (избегаем создания новых объектов в цикле).
Логика работы слайдера (функция createSlider)
Разберём детали реализации слайдера, так как это полезный паттерн для создания инструментов в редакторах или отладки.
1. **Инициализация данных:** Ползунку через setData() сохраняются его границы в пикселях (left, right), допустимый диапазон значений (min, max), масштаб (scale) для пересчёта пикселей в единицы значения и callback-функция.
image.setData('left', x);
image.setData('right', x + width);
image.setData('min', min);
image.setData('max', max);
const scale = max / width;
image.setData('scale', scale);
2. **Обработка перетаскивания:** В обработчике события drag позиция ползунка ограничивается, вычисляется новое значение и обновляется текст. Затем callback вызывается с контекстом this.curve.
const value = (dragX - left) * scale;
const callback = this.getData('callback');
callback.call(this.curve, value);
Это позволяет слайдерам напрямую вызывать методы кривой, такие как setRotation или setStartAngle.
Что попробовать дальше
Этот пример — отличная отправная точка для работы с кривыми в Phaser. Вы можете экспериментировать: заменить Ellipse на Spline для создания произвольных путей, привязать движение спрайта к вычисленной точке на кривой вместо отрисовки кружка или использовать несколько кривых для построения сложных составных траекторий. Интерактивные слайдеры можно вынести в отдельный переиспользуемый класс UI-компонента для будущих проектов.
