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

Переключение сцен — частое действие в играх, но резкий «скачок» может нарушить погружение. В этом примере показана техника плавного визуального перехода с использованием встроенных фильтров камеры и твинов Phaser 3. Вы научитесь создавать стильный эффект «пикселизации» для скрытия загрузки и смены контента, что сделает вашу игру более профессиональной и кинематографичной. Мы разберем, как применить фильтр `Pixelate` к основной камере и анимировать его параметры для входа в сцену и выхода из нее.

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

Живой запуск

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

Исходный код


class SceneB extends Phaser.Scene
{
    constructor ()
    {
        super({
            key: 'SceneB'
        });
    }

    init ()
    {
        this.cameras.main.fadeIn(100);
        const fxCamera = this.cameras.main.filters.external.addPixelate(40);
        this.add.tween({
            targets: fxCamera,
            duration: 700,
            amount: -1,
        });
    }

    create ()
    {
        const bg = this.add.image(0, 0, "bg1")
            .setScale(.5)
            .setOrigin(0);

        const planet = this.add.image(this.sys.scale.width / 2, this.sys.scale.height / 2, "planet")
            .setScale(.3);

        // Planet rotation
        this.add.tween({
            targets: planet,
            duration: 10000,
            angle: 360,
            repeat: -1

        });

        // FX
        const pixelated = this.cameras.main.filters.external.addPixelate(-1);

        // Create button
        const buttonBox = this.add.rectangle(this.sys.scale.width / 2, this.sys.scale.height - 100, 290, 50, 0x000000, 1);
        buttonBox.setInteractive();
        const buttonText = this.add.text(this.sys.scale.width / 2, this.sys.scale.height - 100, "Click to Change Scene").setOrigin(0.5);

        // Click to change scene
        buttonBox.on('pointerdown', () => {
            // Transition to next scene
            this.add.tween({
                targets: pixelated,
                duration: 700,
                amount: 40,
                onComplete: () => {
                    this.cameras.main.fadeOut(100);
                    this.scene.start('SceneA');
                }
            })
        });

        // Hover button properties
        buttonBox.on('pointerover', () => {
            buttonBox.setFillStyle(0x222222, 1);
            this.input.setDefaultCursor('pointer');
        });

        buttonBox.on('pointerout', () => {
            buttonBox.setFillStyle(0x000000, 1);
            this.input.setDefaultCursor('default');
        });
    }
}

class SceneA extends Phaser.Scene
{
    ship;
    flame;

    constructor ()
    {
        super({ key: 'SceneA' });
    }

    init ()
    {
        this.cameras.main.fadeIn(100);
        const fxCamera = this.cameras.main.filters.external.addPixelate(40);
        this.add.tween({
            targets: fxCamera,
            duration: 700,
            amount: -1,
        });
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.setPath('assets/');

        this.load.image("bg1", "skies/pixelsky.png");
        this.load.image("bg2", "skies/space3.png");

        this.load.image("ship", "sprites/x2kship.png");

        this.load.atlas('flares', '/particles/flares.png', '/particles/flares.json');
        this.load.image("planet", "tests/space/blue-planet.png");
    }

    create ()
    {
        const bg = this.add.image(0, 0, "bg2")
            .setOrigin(0);

        this.ship = this.add.image(200, 100, "ship")
            .setScale(1.5);

        // FX
        const pixelated = this.cameras.main.filters.external.addPixelate(-1);

        // Create button
        const buttonBox = this.add.rectangle(this.sys.scale.width / 2, this.sys.scale.height - 100, 290, 50, 0x000000, 1);
        buttonBox.setInteractive();
        const buttonText = this.add.text(this.sys.scale.width / 2, this.sys.scale.height - 100, "Click to Change Scene").setOrigin(0.5);

        // Click to change scene
        buttonBox.on('pointerdown', () => {
            // Transition to next scene
            this.add.tween({
                targets: pixelated,
                duration: 700,
                amount: 40,
                onComplete: () => {
                    this.cameras.main.fadeOut(100);
                    this.scene.start('SceneB');
                }
            })
        });

        // Hover button properties
        buttonBox.on('pointerover', () => {
            buttonBox.setFillStyle(0x222222, 1);
            this.input.setDefaultCursor('pointer');
        });

        buttonBox.on('pointerout', () => {
            buttonBox.setFillStyle(0x000000, 1);
            this.input.setDefaultCursor('default');
        });

        this.flame = this.add.particles(this.ship.x -65, this.ship.y, 'flares',
            {
                frame: 'white',
                color: [ 0xfacc22, 0xf89800, 0xf83600, 0x9f0404 ],
                colorEase: 'quad.out',
                lifespan: 1000,
                angle: { min: 175, max: 185 },
                scale: { start: 0.40, end: 0, ease: 'sine.out' },
                speed: 200,
                advance: 2000,
                blendMode: 'ADD'
            });
    }

    update ()
    {
        // Wrap ship
        this.ship.x = Phaser.Math.Wrap(this.ship.x + 1, 1, this.sys.scale.width + 50);
        this.flame.setPosition(this.ship.x -65, this.ship.y);
    }
}

const config = {
    type: Phaser.AUTO,
    width: 700,
    height: 500,
    pixelArt: true,
    backgroundColor: '#000000',
    parent: 'phaser-example',
    scene: [SceneA, SceneB]
};

const game = new Phaser.Game(config);

Идея перехода: скрыть смену контента

Вместо мгновенного переключения между сценами (scene.start) мы можем визуально подготовить игрока. Идея в том, чтобы в начале сцены быстро "собрать" изображение из пикселей, а в конце — наоборот, "разобрать" его. Это скрывает потенциальные задержки и выглядит как осмысленный эффект.

Фильтр Pixelate из пространства имен this.cameras.main.filters.external идеально подходит для этой задачи. Он имеет параметр amount, который контролирует размер пикселей. Ключевая магия заключается в двух значениях: - amount: 40 — сильная, заметная пикселизация. - amount: -1 — фильтр отключен (исходное изображение). Анимируя этот параметр с помощью tween, мы создаем плавный переход.

Структура сцены: init, create и фильтр

Каждая сцена (SceneA, SceneB) построена по единому шаблону, что делает код предсказуемым.

Метод init() выполняется до create() и идеально подходит для инициализации перехода. Здесь мы сразу применяем фильтр с сильной пикселизацией и запускаем твин, который за 700 миллисекунд убирает эффект, «собирая» картинку на глазах у игрока.

init ()
{
    this.cameras.main.fadeIn(100);
    const fxCamera = this.cameras.main.filters.external.addPixelate(40);
    this.add.tween({
        targets: fxCamera,
        duration: 700,
        amount: -1,
    });
}

В методе create() мы настраиваем основной игровой контент (фон, объекты) и, что важно, создаем экземпляр фильтра с параметром -1. Этот экземпляр мы сохраняем в переменную (например, pixelated), чтобы позднее анимировать его для обратного перехода. Фильтр уже добавлен в камеру, но не активен.

const pixelated = this.cameras.main.filters.external.addPixelate(-1);

Механика перехода по клику

В каждой сцене создается интерактивная кнопка. Её логика — сердце перехода. При клике (pointerdown) мы анимируем ранее созданный фильтр pixelated, увеличивая его amount с -1 до 40. Это создает эффект «разложения» изображения на пиксели.

buttonBox.on('pointerdown', () => {
    this.add.tween({
        targets: pixelated,
        duration: 700,
        amount: 40,
        onComplete: () => {
            this.cameras.main.fadeOut(100);
            this.scene.start('SceneA'); // или 'SceneB'
        }
    })
});

Обратите внимание на колбэк onComplete. Как только анимация пикселизации завершена, мы запускаем короткое затемнение камеры (fadeOut) и только затем переключаем сцену командой this.scene.start. Это добавляет ещё один слой плавности: сцена меняется уже затемнённой, что полностью скрывает любые артефакты переключения.

Важные детали реализации

Для корректной работы фильтров и общего визуального стиля в конфигурации игры выставлена критически важная настройка:

pixelArt: true,

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

Фильтры применяются к объекту this.cameras.main.filters.external. Это специальный контейнер для пост-обработки, который рендерится после отрисовки всей сцены. Добавление фильтра через .addPixelate() автоматически включает этот конвейер.

Также в примере используется простое, но эффективное создание кнопки из прямоугольника (add.rectangle) и текста (add.text). Прямоугольнику назначаются обработчики pointerover и pointerout для изменения цвета и курсора, что обеспечивает базовую, но достаточную интерактивность.

buttonBox.on('pointerover', () => {
    buttonBox.setFillStyle(0x222222, 1);
    this.input.setDefaultCursor('pointer');
});

Дополнительные эффекты: частицы и движение

Пример не ограничивается переходами. В SceneA демонстрируются другие возможности Phaser для оживления сцены.

Система частиц создаёт пламя позади корабля. Частицы привязаны к позиции корабля в методе update(), создавая иллюзию работающего двигателя.

this.flame = this.add.particles(this.ship.x -65, this.ship.y, 'flares',
    {
        frame: 'white',
        color: [ 0xfacc22, 0xf89800, 0xf83600, 0x9f0404 ],
        // ... другие параметры
        blendMode: 'ADD'
    });

Движение корабля реализовано через Phaser.Math.Wrap в update(). Это заставляет корабль плавно перемещаться по горизонтали и, достигнув правой границы, появляться слева.

this.ship.x = Phaser.Math.Wrap(this.ship.x + 1, 1, this.sys.scale.width + 50);

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

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

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