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

При создании игр часто возникает необходимость плавно управлять фоновой музыкой при переходе между игровыми сценами. Например, остановить трек меню и запустить музыку уровня. Пример демонстрирует, как организовать такое управление, используя встроенный аудиоменеджер 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 является стартовой сценой. В его методе preload загружаются аудиофайлы и другие ресурсы. Важно использовать метод setBaseURL, чтобы задать базовый путь для загрузчиков всех сцен.

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

В методе create создается и запускается на воспроизведение объект звука jungle. Ключевой момент — проверка свойства this.sound.locked. Это свойство указывает, заблокировал ли браузер аудиоконтекст (обычно до первого взаимодействия пользователя). Если аудио заблокировано, мы выводим текст с инструкцией и подписываемся на событие unlocked менеджера звуков.

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, который настраивает переход к следующей сцене.

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

Метод setupSceneInput в SceneA отвечает за переход. По клику он выполняет две ключевые операции: останавливает текущую музыку и запускает следующую сцену.

this.input.once('pointerup', function ()
{
    this.jungle.stop();
    this.scene.start('sceneB');
}, this);

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

Цепочка сцен и загрузка в промежуточной сцене

Класс SceneB служит промежуточным звеном. В его методе preload загружается новый аудиофайл для следующей сцены. Обратите внимание, что setBaseURL здесь закомментирован, так как базовый URL уже был установлен в SceneA и наследуется.

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 эта сцена мгновенно запускает SceneC. Это распространенный паттерн для организации загрузочных экранов или сцен-переходов, где нужно загрузить ресурсы для следующей игровой фазы.

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

Финальная сцена SceneC создает свой звуковой объект на основе аудиоключа 'theme', который был загружен в предыдущей сцене (SceneB). Это возможно, потому что загруженные ресурсы хранятся в глобальном кэше игры.

const music = this.sound.add('theme');
music.play({
    loop: true
});

Здесь также присутствует обработка блокировки аудио. Если контекст все еще заблокирован (например, пользователь тапнул в SceneA, но система не разблокировала аудио глобально), выводится соответствующий текст. После события unlocked текст скрывается.

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);
}

Структура конфигурации игры

Все три сцены передаются в массив scene конфигурации игры. Phaser автоматически инициализирует их, но запускает только первую сцену в массиве (SceneA). Остальные сцены находятся в "спящем" режиме до момента их запуска методом start.

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

Настройка pixelArt: true автоматически включает линейную фильтрацию текстур, что помогает сохранить четкость пиксельной графики при масштабировании.

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

Пример наглядно показывает, как организовать последовательное управление аудио между сценами в Phaser 3. Основные принципы: останавливать звуки текущей сцены перед переходом, загружать ресурсы для следующей сцены заранее (можно в промежуточной) и всегда учитывать возможность блокировки аудио браузером. Для экспериментов попробуйте

  1. Добавить плавное затухание звука (fadeOut) перед остановкой
  2. Создать глобальный менеджер аудио, доступный из всех сцен, для централизованного управления плейлистом
  3. Загружать аудио для всех сцен сразу в начальной загрузке, чтобы избежать промежуточных сцен