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

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

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

Живой запуск

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

Исходный код


class MainGame extends Phaser.Scene
{
    constructor ()
    {
        super('MainGame');
    }

    create ()
    {
        this.add.image(400, 300, 'bg').setScale(1.0).setScrollFactor(0, 0);

        const grass = this.add.layer();

        const trees = [ 'Spruce-1', 'Spruce-2', 'Spruce-3', 'Spruce-5', 'Spruce-6', 'Flower_2' ];

        for (let i = 0; i < 128; i++)
        {
            let x = Phaser.Math.Between(0, 800);
            let y = Phaser.Math.Between(100, 600 * 4);

            let frame = Phaser.Utils.Array.GetRandom(trees);

            let tree = this.add.image(x, y, 'glade', frame);

            tree.setDepth(y);
            tree.setOrigin(0.5, 1);

            grass.add(tree);
        }

        const camera = this.cameras.main;

        this.tweens.add({
            targets: camera,
            scrollY: 1800,
            duration: 20000,
            yoyo: true,
            loop: -1
        });

        const quit = this.add.image(0, 0, 'quitButton').setOrigin(0, 0).setScrollFactor(0, 0);

        quit.setInteractive();

        quit.once('pointerdown', () => {

            const fx = this.cameras.main.filters.external.addWipe(0.3, 1, 1);

            this.scene.transition({
                target: 'Menu',
                duration: 2000,
                moveBelow: true,
                onUpdate: (progress) => {

                    fx.progress = progress;

                }
            });
        });
    }
}

class Example extends Phaser.Scene
{
    constructor ()
    {
        super('Menu');
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.atlas('flares', 'assets/particles/flares.png', 'assets/particles/flares.json');
        this.load.image('quitButton', 'assets/tests/quitbutton.png');
        this.load.image('playButton', 'assets/tests/playbutton.png');
        this.load.image('logo', 'assets/tests/gametitle.png');
        this.load.image('village', 'assets/pics/village.jpg');
        this.load.image('bg', 'assets/textures/grass.jpg');
        this.load.atlas('glade', 'assets/atlas/glade.png', 'assets/atlas/glade.json');
    }

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

        this.add.particles(400, 490, 'flares',
        {
            frame: 'white',
            color: [ 0x96e0da, 0x937ef3 ],
            colorEase: 'quart.out',
            lifespan: 2500,
            angle: { min: -140, max: -40 },
            scale: { start: 1, end: 0, ease: 'sine.in' },
            speed: { min: 150, max: 200 },
            advance: 2000,
            frequency: 100,
            blendMode: 'ADD'
        });

        this.add.image(400, 160, 'logo');

        const play = this.add.image(400, 520, 'playButton');

        play.setInteractive();

        play.once('pointerdown', () => {

            const fx = this.cameras.main.filters.external.addWipe();

            this.scene.transition({
                target: 'MainGame',
                duration: 2000,
                moveBelow: true,
                onUpdate: (progress) => {

                    fx.progress = progress;

                }
            });
        });
    }
}

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

let game = new Phaser.Game(config);

Структура примера и загрузка ассетов

Пример состоит из двух сцен: MainGame (основная игровая сцена с лесом) и Example (сцена меню, переименованная в 'Menu'). Все ресурсы загружаются в методе preload сцены Example (меню).

Обратите внимание на использование this.load.setBaseURL. Это позволяет указывать относительные пути к файлам, что удобно для примеров.

this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.atlas('glade', 'assets/atlas/glade.png', 'assets/atlas/glade.json');
this.load.image('quitButton', 'assets/tests/quitbutton.png');

Создание игрового мира и управление камерой

В сцене MainGame создается параллакс-эффект с помощью слоев и анимации камеры. Статичный фон bg фиксируется с помощью .setScrollFactor(0, 0). Деревья, добавленные в слой grass, имеют глубину (setDepth), зависящую от их координаты Y, что создает корректное перекрытие.

Камере добавляется твин, который автоматически пролистывает мир по вертикали, создавая иллюзию путешествия.

// Создание дерева с корректной глубиной
let tree = this.add.image(x, y, 'glade', frame);
tree.setDepth(y);
tree.setOrigin(0.5, 1); // Точка привязки внизу изображения

// Автопрокрутка камеры
this.tweens.add({
    targets: camera,
    scrollY: 1800,
    duration: 20000,
    yoyo: true,
    loop: -1
});

Механика перехода: фильтр и смена сцены

Ключевая логика находится в обработчиках клика по кнопкам 'Quit' и 'Play'. При нажатии происходит не мгновенная смена сцены, а плавный переход.

Сначала на основную камеру (this.cameras.main) добавляется Wipe-фильтр через менеджер внешних фильтров: this.cameras.main.filters.external.addWipe(). Этот метод возвращает объект фильтра (fx).

Затем запускается переход между сценами с помощью this.scene.transition. В его конфигурации критически важен колбэк onUpdate, который синхронизирует прогресс анимации перехода сцен (progress) с прогрессом фильтра (fx.progress). Именно это связывает логику Phaser с визуальным эффектом.

// Добавление фильтра на камеру
const fx = this.cameras.main.filters.external.addWipe(0.3, 1, 1);

// Запуск перехода на целевую сцену
this.scene.transition({
    target: 'Menu', // Имя целевой сцены
    duration: 2000, // Длительность перехода в мс
    moveBelow: true, // Текущая сцена уходит под целевую
    onUpdate: (progress) => {
        fx.progress = progress; // Синхронизация!
    }
});

Настройка Wipe-фильтра

Метод addWipe может принимать параметры для тонкой настройки эффекта. В примере для кнопки 'Quit' используются аргументы (0.3, 1, 1), а для 'Play' — фильтр создается со значениями по умолчанию.

Согласно документации Phaser, эти параметры управляют направлением и характером 'стирания'. Экспериментируя с ними, можно получить переходы слева-направо, сверху-вниз, по диагонали или даже с мягкими краями.

// Фильтр с кастомизацией (в коде для кнопки Quit)
const fx = this.cameras.main.filters.external.addWipe(0.3, 1, 1);

// Фильтр с параметрами по умолчанию (в коде для кнопки Play)
const fx = this.cameras.main.filters.external.addWipe();

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

Wipe-фильтр в Phaser — это мощный и простой инструмент для создания плавных переходов между сценами, который работает на уровне камеры и идеально синхронизируется с системой сцен. Для экспериментов попробуйте изменить параметры addWipe, чтобы получить разные направления стирания, или привяжите эффект не к клику по кнопке, а к другому игровому событию (завершение уровня, смерть игрока). Также можно изменить длительность перехода или использовать другие фильтры из коллекции filters.external, например, для затемнения или размытия.