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

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

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

Живой запуск

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

Исходный код


// Bloom effect by combining filters
function AddBloomTo (gameObject)
{
    // This has no effect if filters are already enabled.
    gameObject.enableFilters();

    const parallelFilters = gameObject.filters.external.addParallelFilters();
    parallelFilters.top.addThreshold(0.5, 1);
    const blur = parallelFilters.top.addBlur();
    parallelFilters.blend.blendMode = Phaser.BlendModes.ADD;
    parallelFilters.blend.amount = 0;

    return {
        get amount () {
            return parallelFilters.blend.amount;
        },
        set amount (value) {
            parallelFilters.blend.amount = value;
        },
        get blurStrength () {
            return blur.strength;
        },
        set blurStrength (value) {
            blur.strength = value;
        }
    };
}

// Bullet class - fires from ship and "destroys" planet
class Bullet extends Phaser.GameObjects.Image
{
    speed;
    flame;
    constructor(scene, x, y) {
        super(scene, x, y, "bullet");
        this.speed = Phaser.Math.GetSpeed(450, 1);
        this.name = "bullet";

        Phaser.Actions.AddEffectBloom(this,
            {
                blendAmount: 1.2,
                blurStrength: 2
            }
        );
    }

    fire (x, y)
    {
        this.setPosition(x, y);
        this.setActive(true);
        this.setVisible(true);
    }

    destroyBullet ()
    {
        if (this.flame === undefined) {
            // Create particles for flame
            this.flame = this.scene.add.particles(this.x, this.y, 'flares',
                {
                    frame: 'white',
                    color: [0xfacc22, 0xf89800, 0xf83600, 0x9f0404],
                    colorEase: 'quad.out',
                    lifespan: 500,
                    scale: { start: 0.70, end: 0, ease: 'sine.out' },
                    speed: 200,
                    advance: 500,
                    frequency: 50,
                    blendMode: 'ADD',
                    duration: 1000,
                });
                this.flame.setDepth(1);
            // When particles are complete, destroy them
            this.flame.once("complete", () => {
                this.flame.destroy();
            })
        }

        // Destroy bullet after 50ms (helps to enter inside of planet)
        this.scene.time.addEvent({
            delay: 50,
            callback: () => {
                this.setActive(false);
                this.setVisible(false);
                this.destroy();
            }
        });

    }

    // Update bullet position and destroy if it goes off screen
    update (time, delta)
    {
        this.x += this.speed * delta;

        if (this.x > this.scene.sys.canvas.width) {
            this.setActive(false);
            this.setVisible(false);
            this.destroy();
        }
    }
}

// Logic game
class Example extends Phaser.Scene
{
    ship;
    bullets;
    // Control for firing bullets
    spacebar;
    constructor ()
    {
        super({
            key: 'MainScene'
        });
    }

    init ()
    {
        // Description text for fire bullet
        this.add.text(10, 10, 'Press "space" to fire bullet', { font: '16px Courier', fill: '#ffffff' }).setDepth(100);

        // Fade in camera
        this.cameras.main.fadeIn(800);
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.setPath("assets/");
        this.load.image("bullet", "sprites/bullets/bullet6.png");
        this.load.image("ship", "sprites/x2kship.png");
        this.load.image("bg", "tests/space/nebula.jpg");
        this.load.image("planet", "tests/space/blue-planet.png");

        this.load.atlas('flares', '/particles/flares.png', '/particles/flares.json');
    }

    create ()
    {
        // Just stars background
        const bg = this.add.image(0, 0, "bg")
            .setOrigin(0, 0)
            .setTint(0x333333);

        const planet = this.physics.add.image(this.sys.scale.width - 100, this.sys.scale.height / 2, "planet")
            .setScale(.2);
        planet.flipX = true;
        // Tween to rotate slow planet
        this.tweens.add({
            targets: planet,
            duration: 5000000,
            rotation: 360,
            repeat: -1
        });

        // Bloom effect for the planet
        const { blur } = Phaser.Actions.AddEffectBloom(planet,
            {
                blendAmount: 1.2,
                blurStrength: 0
            }
        );

        this.ship = this.add.image(100, this.sys.scale.height / 2, 'ship')
            .setDepth(2);

        this.bullets = this.physics.add.group({
            classType: Bullet,
            maxSize: 30,
            runChildUpdate: true,
        });

        this.spacebar = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);

        // Effect for planet bloom
        const planetFXTween = this.tweens.add({
            targets: blur,
            strength: 2,
            yoyo: true,
            duration: 100,
            paused: true,
            onComplete: () => {
                planetFXTween.restart();
                planetFXTween.pause();
            }
        });

        this.physics.add.overlap(this.bullets, planet, (planet, bullet) => {
            // If bullet hits planet, destroy the bullet and play the effect
            bullet.destroyBullet();
            if (!planetFXTween.isPlaying()) {
                planetFXTween.restart();
                planetFXTween.play();
            }
        });
    }

    // Bullet fire
    update() {
        if (this.spacebar)
        {
            if (Phaser.Input.Keyboard.JustDown(this.spacebar))
            {
                const bullet = this.bullets.get();

                if (bullet) {
                    bullet.fire(this.ship.x, this.ship.y);
                }
            }
        }
    }
}

const config = {
    type: Phaser.WEBGL,
    width: 700,
    height: 500,
    physics: {
        default: 'arcade'
    },
    backgroundColor: '#2f3640',
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Суть эффекта Bloom

Bloom — это постобработка, имитирующая пересвет камеры, когда яркие области "растекаются" на соседние пиксели. В Phaser 3 этот эффект можно собрать из нескольких базовых фильтров.

Основная функция AddBloomTo создаёт свечение для любого игрового объекта. Она использует систему параллельных фильтров (external.addParallelFilters()), которая позволяет применять несколько эффектов одновременно и гибко их смешивать.

const parallelFilters = gameObject.filters.external.addParallelFilters();
parallelFilters.top.addThreshold(0.5, 1);
const blur = parallelFilters.top.addBlur();
parallelFilters.blend.blendMode = Phaser.BlendModes.ADD;
parallelFilters.blend.amount = 0;

Сначала фильтр Threshold (пороговый) выделяет только самые яркие части спрайта (те, чья яркость выше 0.5). Затем к этому выделению применяется размытие по Гауссу (Blur). Результат смешивается с исходным изображением в режиме ADD (сложение), что и даёт эффект свечения. Параметр parallelFilters.blend.amount контролирует интенсивность этого наложения.

Готовое решение Phaser.Actions.AddEffectBloom

В примере используется удобная обёртка Phaser.Actions.AddEffectBloom, которая внутри вызывает рассмотренную логику. Она сразу применяет bloom к объекту и возвращает геттеры/сеттеры для управления параметрами.

const { blur } = Phaser.Actions.AddEffectBloom(planet,
    {
        blendAmount: 1.2,
        blurStrength: 0
    }
);

Функция принимает объект и конфигурацию. blendAmount задаёт начальную интенсивность свечения, а blurStrength — силу размытия. Возвращаемый объект blur позволяет анимировать силу размытия, что используется для создания вспышки при попадании.

Динамическая реакция на событие

Свечение планеты становится интерактивным: при попадании пули сила размытия (blurStrength) анимируется, создавая вспышку. Для этого используется Tween.

const planetFXTween = this.tweens.add({
    targets: blur,
    strength: 2,
    yoyo: true,
    duration: 100,
    paused: true,
    onComplete: () => {
        planetFXTween.restart();
        planetFXTween.pause();
    }
});

Твин анимирует свойство strength объекта blur от текущего значения до 2 и обратно (за счёт yoyo: true). Изначально он поставлен на паузу (paused: true). При коллизии пули с планетой твин перезапускается и проигрывается.

this.physics.add.overlap(this.bullets, planet, (planet, bullet) => {
    bullet.destroyBullet();
    if (!planetFXTween.isPlaying()) {
        planetFXTween.restart();
        planetFXTween.play();
    }
});

Визуализация выстрела и попадания

Для пули также применяется bloom-эффект, чтобы она сама светилась. При её уничтожении создаётся система частиц, имитирующая взрыв.

this.flame = this.scene.add.particles(this.x, this.y, 'flares',
    {
        frame: 'white',
        color: [0xfacc22, 0xf89800, 0xf83600, 0x9f0404],
        colorEase: 'quad.out',
        lifespan: 500,
        scale: { start: 0.70, end: 0, ease: 'sine.out' },
        speed: 200,
        advance: 500,
        frequency: 50,
        blendMode: 'ADD',
        duration: 1000,
    });

Ключевые параметры: color задаёт градиент от жёлтого к тёмно-красному, blendMode: 'ADD' обеспечивает яркое, аддитивное наложение частиц, а duration: 1000 ограничивает время жизни эмиттера. Частицы уничтожаются после завершения анимации (once('complete', ...)).

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

Комбинация фильтров bloom и частиц позволяет создавать сочные, отзывчивые визуальные эффекты с минимальным кодом. Для экспериментов попробуйте: изменить параметры threshold и blurStrength для получения разного характера свечения; анимировать не только силу размытия, но и blendAmount; применить bloom-эффект к фону или UI-элементам; комбинировать bloom с другими фильтрами, например, цветовой коррекцией.