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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    t = 0;
    move = false;
    spark1 = null;
    spark0 = null;
    whiteSmoke = null;
    fire = null;
    darkSmoke = null;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('dark-smoke', 'assets/particles/smoke-puff.png');
        this.load.image('white-smoke', 'assets/particles/smoke0.png');
        this.load.image('fire', 'assets/particles/muzzleflash3.png');
        this.load.image('spark0', 'assets/particles/blue.png');
        this.load.image('spark1', 'assets/particles/red.png');
    }

    create ()
    {

        this.spark0 = this.add.particles('spark0').createEmitter({
            x: 400,
            y: 300,
            speed: { min: -500, max: 500 },
            angle: { min: -120, max: -60 },
            scale: { min: 0.05, max: 0 },
            alpha: { min: 1, max: 0 },
            gravityY: 500,
            lifespan: 1
        });
        this.spark0.reserve(1000);

        this.spark1 = this.add.particles('spark1').createEmitter({
            x: 400,
            y: 300,
            speed: { min: -100, max: 100 },
            angle: { min: -120, max: -60 },
            scale: { start: 0, end: 0.4 },
            alpha: { start: 1, end: 0, ease: 'Expo.easeIn' },
            blendMode: 'SCREEN',
            gravityY: 500,
            lifespan: 1000
        });
        this.spark1.reserve(1000);

        this.fire = this.add.particles('fire').createEmitter({
            x: 400,
            y: 300,
            speed: { min: 100, max: 200 },
            angle: { min: -85, max: -95 },
            scale: { start: 0, end: 1, ease: 'Back.easeOut' },
            alpha: { start: 1, end: 0, ease: 'Quart.easeOut' },
            blendMode: 'SCREEN',
            lifespan: 1000
        });
        this.fire.reserve(1000);

        this.whiteSmoke = this.add.particles('white-smoke').createEmitter({
            x: 400,
            y: 300,
            speed: { min: 20, max: 100 },
            angle: { min: 0, max: 360},
            scale: { start: 1, end: 0},
            alpha: { start: 0, end: 0.5},
            lifespan: 2000

            // active: false
        });
        this.whiteSmoke.reserve(1000);

        this.darkSmoke = this.add.particles('dark-smoke').createEmitter({
            x: 400,
            y: 300,
            speed: { min: 20, max: 100 },
            angle: { min: 0, max: 360},
            scale: { start: 1, end: 0},
            alpha: { start: 0, end: 0.1},
            blendMode: 'ADD',
            lifespan: 2000

            // active: false
        });
        this.darkSmoke.reserve(1000);

        this.fire.onParticleDeath(particle =>
        {
            this.darkSmoke.setPosition(particle.x, particle.y);
            this.whiteSmoke.setPosition(particle.x, particle.y);
            this.darkSmoke.emitParticle();
            this.whiteSmoke.emitParticle();
        });

        this.input.on('pointermove', pointer =>
        {
            this.darkSmoke.setPosition(pointer.x, pointer.y);
            this.fire.setPosition(pointer.x, pointer.y);
        });

    }

    update ()
    {

        this.spark0.x = this.fire.x;
        this.spark0.y = this.fire.y;
        this.spark1.x = this.fire.x;
        this.spark1.y = this.fire.y;
    }
}

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

const game = new Phaser.Game(config);

Базовый принцип: создание менеджера частиц

Phaser предоставляет систему частиц через фабрику this.add.particles(). Этот метод создает менеджер (ParticleEmitterManager), который управляет текстурами и эмиттерами. Каждый эмиттер (ParticleEmitter) настраивается отдельно, определяя, как именно будут появляться, двигаться и исчезать частицы.

В примере в методе preload загружаются пять отдельных спрайтов для частиц: два вида искр (spark0, spark1), огонь (fire), белый и темный дым (white-smoke, dark-smoke). Именно эти изображения будут использоваться эмиттерами.

this.load.image('dark-smoke', 'assets/particles/smoke-puff.png');
this.load.image('white-smoke', 'assets/particles/smoke0.png');
this.load.image('fire', 'assets/particles/muzzleflash3.png');
this.load.image('spark0', 'assets/particles/blue.png');
this.load.image('spark1', 'assets/particles/red.png');

Настройка отдельных эмиттеров

В методе create создается пять эмиттеров. Каждый эмиттер конфигурируется объектом с параметрами. Давайте разберем ключевые параметры на примере первого эмиттера spark0.

* speed и angle: Задают вектор движения. Здесь speed от -500 до 500 пикселей в секунду, а angle от -120 до -60 градусов (вверх и влево-вправо). * scale и alpha: Контролируют размер и прозрачность частицы на протяжении ее жизни (lifespan). Можно задавать min/max или start/end. * gravityY: Симулирует гравитацию, заставляя частицы падать вниз. * blendMode: Режим наложения, например 'SCREEN' или 'ADD', для создания эффекта свечения.

Важный этап — вызов reserve(1000). Он предварительно выделяет память под 1000 частиц для этого эмиттера, предотвращая лаги в момент их создания во время работы эффекта.

this.spark0 = this.add.particles('spark0').createEmitter({
    x: 400,
    y: 300,
    speed: { min: -500, max: 500 },
    angle: { min: -120, max: -60 },
    scale: { min: 0.05, max: 0 },
    alpha: { min: 1, max: 0 },
    gravityY: 500,
    lifespan: 1
});
this.spark0.reserve(1000);

Взаимодействие эмиттеров: цепные реакции

Самый интересный аспект примера — связь между эмиттерами. Эмиттер fire (огонь) привязан к слушателю события pointermove, поэтому он следует за курсором мыши.

Более того, на эмиттере fire регистрируется обработчик события onParticleDeath. Каждый раз, когда частица огня завершает свой жизненный цикл и исчезает, этот обработчик срабатывает. Внутри него мы берем координаты умершей частицы (particle.x, particle.y) и устанавливаем на это же положение эмиттеры дыма (darkSmoke и whiteSmoke), после чего принудительно испускаем по одной новой частице дыма с помощью emitParticle(). Это создает эффект, будто дым появляется там, где только что погас огонек.

this.fire.onParticleDeath(particle =>
{
    this.darkSmoke.setPosition(particle.x, particle.y);
    this.whiteSmoke.setPosition(particle.x, particle.y);
    this.darkSmoke.emitParticle();
    this.whiteSmoke.emitParticle();
});

this.input.on('pointermove', pointer =>
{
    this.darkSmoke.setPosition(pointer.x, pointer.y);
    this.fire.setPosition(pointer.x, pointer.y);
});

Динамическое обновление в игровом цикле

Чтобы эффект выглядел целостно, эмиттеры искр (spark0 и spark1) должны постоянно следовать за основным эмиттером огня (fire). Поскольку их положение не обновляется в обработчике мыши напрямую, эта задача выполняется в методе update, который вызывается каждый кадр.

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

update ()
{
    this.spark0.x = this.fire.x;
    this.spark0.y = this.fire.y;
    this.spark1.x = this.fire.x;
    this.spark1.y = this.fire.y;
}

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

Разобранный пример демонстрирует мощный паттерн: использование нескольких связанных эмиттеров для создания сложного составного эффекта. Вы можете экспериментировать: измените параметры speed, angle или gravityY, чтобы искры летели в другую сторону, а дым был тяжелее. Попробуйте заменить текстуры на свои, чтобы создать след из магических runes или технологических искр. Отключите следование за мышью и привяжите систему частиц к игровому персонажу для создания постоянного ауры или шлейфа.