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

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

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

Живой запуск

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

Исходный код


class SceneA extends Phaser.Scene
{
    jungle;

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

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.audio('jungle', [
            'assets/audio/jungle.ogg',
            'assets/audio/jungle.mp3'
        ]);

        this.load.image('wizball', 'assets/sprites/wizball.png');

        this.load.bitmapFont('atari-classic', 'assets/fonts/bitmap/atari-classic.png', 'assets/fonts/bitmap/atari-classic.xml');
    }

    create ()
    {
        console.log('SceneA');

        const text = this.add.bitmapText(400, 100, 'atari-classic', '', 30)
            .setOrigin(0.5);

        this.add.image(400, 300, 'wizball');

        this.jungle = this.sound.add('jungle');

        this.jungle.play({
            loop: true
        });

        if (this.sound.locked)
        {
            text.setText('Tap to unlock\nand play music');

            this.sound.once('unlocked', function (soundManager)
            {
                this.setupSceneInput(text);

            }, this);
        }
        else
        {
            this.setupSceneInput(text);
        }
    }

    setupSceneInput (text)
    {
        text.setText(' Tap to load and play\nmusic from child scene');

        this.input.once('pointerup', function ()
        {

            this.jungle.stop();

            this.scene.start('sceneB');

        }, this);
    }
}


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

    preload ()
    {
        // this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.audio('theme', [
            'assets/audio/oedipus_wizball_highscore.ogg',
            'assets/audio/oedipus_wizball_highscore.mp3'
        ]);
    }

    create ()
    {
        console.log('SceneB');

        this.scene.start('sceneC');
    }
}

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

    create ()
    {
        console.log('SceneC');

        this.add.image(400, 300, 'wizball').setScale(4);

        const music = this.sound.add('theme');

        music.play({
            loop: true
        });

        if (this.sound.locked)
        {
            const text = this.add.bitmapText(400, 100, 'atari-classic',
                'Tap to unlock and play\nmusic from child scene', 30)
                .setOrigin(0.5);

            this.sound.once('unlocked', soundManager =>
            {
                text.visible = false;

            }, this);
        }

    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    pixelArt: true,
    scene: [ SceneA, SceneB, SceneC ]
};

const game = new Phaser.Game(config);

Загрузка аудио в родительской сцене

В SceneA мы загружаем начальный аудиофайл и спрайт. Обратите внимание на использование метода this.load.audio с массивом путей для поддержки разных форматов (OGG и MP3). Это обеспечивает кроссбраузерность.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.audio('jungle', [
        'assets/audio/jungle.ogg',
        'assets/audio/jungle.mp3'
    ]);
    this.load.image('wizball', 'assets/sprites/wizball.png');
}

В методе create создается объект звука this.jungle с помощью this.sound.add. Он воспроизводится с параметром loop: true. Важный момент — проверка свойства this.sound.locked. Оно указывает, требуется ли пользовательское взаимодействие (например, клик) для разблокировки аудио в браузере. Если звук заблокирован, мы показываем подсказку и ждем события unlocked.

Остановка звука и запуск дочерней сцены

После разблокировки (или если блокировки не было) вызывается метод setupSceneInput. Он меняет текст подсказки и назначает обработчик на клик. По клику происходит ключевое действие: остановка текущей музыки и запуск следующей сцены.

setupSceneInput (text)
{
    text.setText(' Tap to load and play\nmusic from child scene');
    this.input.once('pointerup', function ()
    {
        this.jungle.stop();
        this.scene.start('sceneB');
    }, this);
}

Метод this.jungle.stop() гарантирует, что музыка из SceneA не будет накладываться на звук из следующей сцены. this.scene.start('sceneB') полностью останавливает SceneA и запускает SceneB. Это важно, так как SceneB выступает промежуточным звеном для загрузки нового аудиоресурса.

Промежуточная сцена для загрузки

SceneB — это техническая сцена, основная задача которой — загрузить новый аудиофайл. Она не содержит логики отображения. В preload загружается аудио с ключом 'theme', а в create сразу запускается SceneC.

create ()
{
    console.log('SceneB');
    this.scene.start('sceneC');
}

Такой подход разделяет ответственность: SceneA управляет первым звуком и интерфейсом, SceneB загружает данные, а SceneC их использует. Это чистая архитектура, упрощающая отладку.

Воспроизведение звука в целевой сцене

SceneC — конечная точка. Она использует спрайт, загруженный еще в SceneA (ресурсы в Phaser глобальны для всего экземпляра Game). Здесь создается и воспроизводится звук 'theme', который был загружен в SceneB.

create ()
{
    const music = this.sound.add('theme');
    music.play({
        loop: true
    });
    if (this.sound.locked)
    {
        // ... показ текста и подписка на 'unlocked'
    }
}

Обратите внимание, что здесь снова проверяется this.sound.locked. Хотя аудио уже было разблокировано в SceneA, эта проверка является хорошей практикой на случай, если пользователь перезагрузит страницу или перейдет прямо в SceneC в другом сценарии.

Конфигурация игры и глобальный доступ к аудио

Все три сцены передаются в массив scene конфигурации игры. Phaser инициализирует их и создает глобальный менеджер звуков this.sound в каждой сцене.

const config = {
    type: Phaser.AUTO,
    scene: [ SceneA, SceneB, SceneC ]
};

Ключевой вывод: аудио, загруженное в одной сцене с помощью this.load.audio, становится доступно во всех других сценах через this.sound.add('key'). Это позволяет эффективно разделять загрузку ресурсов и их использование.

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

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

  1. использовать this.scene.switch вместо this.scene.start, чтобы сохранять состояние предыдущей сцены
  2. создать глобальный менеджер аудио в отдельном плагине или сцене
  3. добавить fade-in/fade-out эффекты при переходе между треками с помощью this.tweens.add и свойства volume объекта Sound