О чем этот пример
Эмиттеры частиц оживляют игровые миры, но их поведение часто жестко закодировано. В этой статье мы разберем пример, где частицы следуют по плавной кривой, которую можно изменять в реальном времени с помощью мыши. Этот подход полезен для создания интерактивных спецэффектов, волшебных следов, гибких траекторий пуль или анимации пользовательского интерфейса. Вы научитесь связывать физику, кривые и систему событий ввода для полного контроля над визуальными эффектами.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.55.2.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.spritesheet('dragcircle', 'assets/sprites/dragcircle.png', { frameWidth: 16 });
this.load.atlas('flares', 'assets/particles/flares.png', 'assets/particles/flares.json');
}
create ()
{
const graphics = this.add.graphics();
const particles = this.add.particles('flares');
const path = { t: 0, vec: new Phaser.Math.Vector2() };
const curve = new Phaser.Curves.Spline([
20, 550,
260, 450,
300, 250,
550, 145,
745, 256
]);
const emitter = particles.createEmitter({
frame: 'blue',
quantity: 48,
scale: { start: 0.5, end: 0 },
blendMode: 'ADD',
emitZone: { type: 'edge', source: curve, quantity: 48 }
});
// Create drag-handles for each point
for (let i = 0; i < curve.points.length; i++)
{
const point = curve.points[i];
const handle = this.add.image(point.x, point.y, 'dragcircle', 0)
.setInteractive()
.setDataEnabled();
handle.data.set('vector', point);
this.input.setDraggable(handle);
}
this.input.on('dragstart', (pointer, gameObject) =>
{
gameObject.setFrame(1);
});
this.input.on('drag', (pointer, gameObject, dragX, dragY) =>
{
gameObject.x = dragX;
gameObject.y = dragY;
gameObject.data.get('vector').set(dragX, dragY);
emitter.emitZone.updateSource();
});
this.input.on('dragend', (pointer, gameObject) =>
{
gameObject.setFrame(0);
});
}
}
const config = {
type: Phaser.WEBGL,
width: 800,
height: 600,
backgroundColor: '#000',
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Создание кривой и эмиттера частиц
Основная идея — заставить частицы появляться не в случайной точке, а вдоль заданной кривой. Для этого используется Phaser.Curves.Spline. Эмиттер настраивается с помощью emitZone, которая определяет зону испускания.
const curve = new Phaser.Curves.Spline([
20, 550,
260, 450,
300, 250,
550, 145,
745, 256
]);
Здесь создается сплайн-кривая по пяти опорным точкам, переданным как плоский массив координат [x1, y1, x2, y2, ...].
const emitter = particles.createEmitter({
frame: 'blue',
quantity: 48,
scale: { start: 0.5, end: 0 },
blendMode: 'ADD',
emitZone: { type: 'edge', source: curve, quantity: 48 }
});
Ключевой параметр — emitZone. Установив type: 'edge' и source: curve, мы указываем, что частицы должны рождаться равномерно (quantity: 48) вдоль всей длины кривой (edge), а не внутри замкнутой области. blendMode: 'ADD' дает яркий, светящийся эффект для частиц-вспышек ('flares').
Интерактивные маркеры для управления кривой
Чтобы кривую можно было изменять, на каждую её опорную точку помещается перетаскиваемый спрайт. Это реализуется в цикле по массиву curve.points.
for (let i = 0; i < curve.points.length; i++) {
const point = curve.points[i];
const handle = this.add.image(point.x, point.y, 'dragcircle', 0)
.setInteractive()
.setDataEnabled();
handle.data.set('vector', point);
this.input.setDraggable(handle);
}
Каждый маркер (handle) — это изображение из спрайтшита dragcircle. Важный шаг — setDataEnabled() и последующая привязка векторного объекта точки кривой к данным спрайта с помощью handle.data.set('vector', point). Это создает прямую ссылку, позволяя позже обновлять координаты самой точки кривой при перетаскивании маркера. this.input.setDraggable(handle) делает спрайт доступным для drag-событий.
Обработка перетаскивания и обновление эмиттера
Логика реакции на действия игрока распределена по трем событиям: начало перетаскивания, сам процесс и окончание.
this.input.on('drag', (pointer, gameObject, dragX, dragY) => {
gameObject.x = dragX;
gameObject.y = dragY;
gameObject.data.get('vector').set(dragX, dragY);
emitter.emitZone.updateSource();
});
Во время события drag обновляются визуальные координаты маркера. Затем через gameObject.data.get('vector') получается ссылка на объект точки кривой (Phaser.Math.Vector2), и его координаты перезаписываются методом .set(dragX, dragY). Поскольку emitZone хранит ссылку на исходную кривую (source: curve), а кривая содержит ссылки на эти же векторные объекты, изменение точки происходит «на месте». Однако, чтобы эмиттер узнал об изменениях в геометрии зоны испускания, необходимо вручную вызвать emitter.emitZone.updateSource().
this.input.on('dragstart', (pointer, gameObject) => {
gameObject.setFrame(1); // Меняем кадр спрайта для визуальной обратной связи
});
this.input.on('dragend', (pointer, gameObject) => {
gameObject.setFrame(0);
});
События dragstart и dragend меняют кадр спрайта маркера, обеспечивая простую визуальную индикацию состояния (активно/неактивно).
Что попробовать дальше
Вы создали систему, где траектория частиц динамически связывается с интерактивными элементами. Ключевые моменты: использование emitZone типа edge для испускания вдоль кривой, привязка данных спрайта к объектам геометрии для прямого управления и вызов updateSource() для синхронизации эмиттера.
Идеи для экспериментов:
1. Замените Spline на Ellipse или Path для других форм траекторий.
2. Привяжите физические тела к точкам кривой, чтобы частицы летели за движущимися объектами.
3. Изменяйте свойства эмиттера (цвет, скорость, гравитацию) в зависимости от положения маркера.
4. Используйте полученную кривую не только для частиц, но и для движения спрайта по пути с помощью Phaser.Math.Interpolation.
