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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    shakeCamera;
    flashCamera;
    fadeCamera;
    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.cameras.main.setViewport(5, 5, 390, 290);
        this.fadeCamera = this.cameras.add(405, 5, 390, 290);
        this.flashCamera = this.cameras.add(5, 305, 390, 290);
        this.shakeCamera = this.cameras.add(405, 305, 390, 290);

        this.fadeCamera.fade(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);

        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},
            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},
            alpha: { start: 1, end: 0},
            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: 'SCREEN',
            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;

        this.flashCamera.flash(1000);
        this.shakeCamera.shake(1000);

        if (this.fadeCamera._fadeAlpha >= 1.0)
        {
            this.fadeCamera._fadeAlpha = 0.0;
            this.fadeCamera.fade(1000);
        }
    }
}

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

const game = new Phaser.Game(config);

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

Класс Example расширяет Phaser.Scene и содержит свойства для хранения ссылок на камеры и эмиттеры частиц. В методе preload() загружаются текстуры для частиц с помощью this.load.setBaseURL() и this.load.image(). Это стандартный подход для загрузки ассетов в Phaser.

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.cameras.main.setViewport(). Затем создаются три дополнительные камеры с помощью this.cameras.add(). Каждая камера занимает свой квадрант на экране, что позволяет одновременно наблюдать за разными эффектами.

На камеру fadeCamera сразу применяется эффект затухания (fade()). Это создаёт плавное появление сцены в этом квадранте.

create ()
{
    this.cameras.main.setViewport(5, 5, 390, 290);
    this.fadeCamera = this.cameras.add(405, 5, 390, 290);
    this.flashCamera = this.cameras.add(5, 305, 390, 290);
    this.shakeCamera = this.cameras.add(405, 305, 390, 290);

    this.fadeCamera.fade(1000);
}

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

Далее создаются пять эмиттеров частиц, представляющих разные элементы эффекта: искры (spark0, spark1), огонь (fire), белый дым (whiteSmoke) и тёмный дым (darkSmoke). Каждый эмиттер создаётся из менеджера частиц, который возвращается this.add.particles(), с последующим вызовом .createEmitter().

Конфигурация каждого эмиттера детально настраивает поведение частиц: скорость, угол разлёта, масштаб, прозрачность (альфа), гравитацию и время жизни (lifespan). Важный момент — предварительное резервирование пула частиц с помощью .reserve(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);

Взаимодействие частиц и обработка ввода

Ключевой момент связки эффектов — обработчик события смерти частицы огня. С помощью метода onParticleDeath() эмиттера fire мы подписываемся на это событие. Когда частица огня исчезает, в её последней позиции размещаются и испускаются эмиттеры дыма (darkSmoke и whiteSmoke). Это создаёт эффект, будто огонь оставляет за собой шлейф дыма.

Одновременно обработчик события pointermove перемещает эмиттеры огня и тёмного дыма за курсором мыши. Это делает сцену интерактивной.

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() выполняется каждый кадр и отвечает за непрерывную анимацию. Здесь происходит несколько важных вещей: 1. Позиции эмиттеров искр (spark0 и spark1) синхронизируются с позицией эмиттера огня (fire), создавая эффект летящих вместе искр. 2. На камеру flashCamera постоянно применяется эффект вспышки (flash()), а на shakeCamera — эффект тряски (shake()). 3. Реализуется циклический эффект затухания на камере fadeCamera. Когда затухание завершается (значение внутреннего свойства _fadeAlpha достигает 1.0), оно сбрасывается и запускается заново.

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;

    this.flashCamera.flash(1000);
    this.shakeCamera.shake(1000);

    if (this.fadeCamera._fadeAlpha >= 1.0)
    {
        this.fadeCamera._fadeAlpha = 0.0;
        this.fadeCamera.fade(1000);
    }
}

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

Этот пример демонстрирует, как комбинировать мощные, но относительно простые в использовании инструменты Phaser — эффекты камеры и систему частиц — для создания сложного и динамичного визуального ряда. Вы можете экспериментировать с параметрами эмиттеров (скорость, гравитация, время жизни), пробовать другие эффекты камер (например, zoom или rotation), или привязывать запуск эффектов к конкретным игровым событиям, например, к столкновению или получению урона.