О чем этот пример

Эмиттеры частиц оживляют игровые миры, но их поведение часто жестко закодировано. В этой статье мы разберем пример, где частицы следуют по плавной кривой, которую можно изменять в реальном времени с помощью мыши. Этот подход полезен для создания интерактивных спецэффектов, волшебных следов, гибких траекторий пуль или анимации пользовательского интерфейса. Вы научитесь связывать физику, кривые и систему событий ввода для полного контроля над визуальными эффектами.

Версия 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.