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

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

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('bg', 'assets/skies/darkstone.png');
        this.load.atlas('flares', 'assets/particles/flares.png', 'assets/particles/flares.json');
    }

    create ()
    {
        this.add.image(400, 300, 'bg');

        let loops = 0;

        const text = this.add.text(400, 300, '0', { font: '64px Courier', fill: '#fff' }).setOrigin(0.5);

        const emitter = this.add.particles(400, 300, 'flares', {
            frame: 'red',
            blendMode: 'ADD',
            lifespan: 1000,
            frequency: 16,
            scale: { start: 0.8, end: 0.1 },
            stopAfter: 32
        });

        emitter.on('complete', () => {

            loops++;

            text.setText(loops);

            emitter.start();

        });

        emitter.addEmitZone({
            type: 'edge',
            source: new Phaser.Geom.Circle(0, 0, 160),
            quantity: 32
        });
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#000',
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Настройка сцены и загрузка ресурсов

В методе preload() загружаются необходимые ресурсы. Фон (bg) нужен для визуального контекста, а атлас частиц (flares) содержит сами текстуры для эффекта.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('bg', 'assets/skies/darkstone.png');
    this.load.atlas('flares', 'assets/particles/flares.png', 'assets/particles/flares.json');
}

Создание эмиттера частиц с лимитом

В методе create() сначала добавляется фон и текстовый объект для отображения счетчика циклов. Затем создается эмиттер частиц (this.add.particles). Ключевой параметр здесь — stopAfter: 32. Он указывает, что эмиттер автоматически остановится после выпуска 32 частиц. Это создает дискретный "пакет" частиц, который можно повторять.

const emitter = this.add.particles(400, 300, 'flares', {
    frame: 'red',
    blendMode: 'ADD',
    lifespan: 1000,
    frequency: 16,
    scale: { start: 0.8, end: 0.1 },
    stopAfter: 32
});

Зацикливание с помощью события 'complete'

Чтобы создать бесконечный цикл, мы подписываемся на событие эмиттера complete. Оно срабатывает как раз тогда, когда эмиттер останавливается после достижения лимита stopAfter. В обработчике события мы увеличиваем счетчик циклов, обновляем текст и вручную перезапускаем эмиттер методом start().

emitter.on('complete', () => {
    loops++;
    text.setText(loops);
    emitter.start();
});

Определение области испускания (Emit Zone)

Без зоны испускания все частицы рождались бы в одной точке (400, 300). Мы используем emitter.addEmitZone, чтобы растянуть их по краю геометрической фигуры. Здесь задается зона типа 'edge' (частицы появляются по контуру) источника — круга (Phaser.Geom.Circle) с радиусом 160 пикселей. Параметр quantity: 32 указывает, что все 32 частицы из одного цикла должны быть распределены по этому контуру.

emitter.addEmitZone({
    type: 'edge',
    source: new Phaser.Geom.Circle(0, 0, 160),
    quantity: 32
});

Что попробовать дальше

Комбинация stopAfter и события complete дает полный контроль над ритмом испускания частиц, позволяя создавать не просто непрерывный поток, а повторяющиеся импульсы. Для экспериментов попробуйте изменить форму EmitZone на прямоугольник (Phaser.Geom.Rectangle) или эллипс, поменять параметр type на 'random' для заполнения всей площади фигуры или заставить зону следовать за курсором мыши, обновляя ее source в событии pointermove.