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

Одной из ключевых задач при разработке игр является корректное управление аудиопотоками при переключении между игровыми состояниями или сценами. Неправильная обработка может привести к наложению музыки или её внезапному обрыву. В этой статье мы разберем пример из официальной документации 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,
    audio: {
        disableWebAudio: true
    },
    scene: [ SceneA, SceneB, SceneC ]
};

const game = new Phaser.Game(config);

Архитектура примера и настройка звука

Пример состоит из трех сцен (SceneA, SceneB, SceneC), которые запускаются последовательно. Ключевая особенность — использование HTML5 Audio вместо Web Audio API, что задается в конфигурации игры. Это важно для совместимости.

const config = {
    type: Phaser.AUTO,
    // ... другие настройки ...
    audio: {
        disableWebAudio: true
    },
    scene: [ SceneA, SceneB, SceneC ]
};

В SceneA в методе preload загружаются аудиофайл jungle и другие ресурсы. Обратите внимание, что для обеспечения кроссбраузерной поддержки загружаются два формата файла: OGG и MP3.

this.load.audio('jungle', [
    'assets/audio/jungle.ogg',
    'assets/audio/jungle.mp3'
]);

Обработка блокировки аудио (Autoplay Policy)

Современные браузеры блокируют автоматическое воспроизведение аудио без взаимодействия пользователя. Phaser предоставляет свойство this.sound.locked для проверки этого состояния.

В create методе SceneA создается объект звука jungle и предпринимается попытка его воспроизвести. Если звук заблокирован (locked), на экране появляется инструкция, и воспроизведение откладывается до события unlocked.

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

Координация остановки и запуска между сценами

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

this.input.once('pointerup', function () {
    this.jungle.stop(); // Явная остановка текущего трека
    this.scene.start('sceneB');
}, this);

SceneB выступает в роли промежуточной сцены-загрузчика. В её методе preload загружается новый аудиофайл theme, после чего в create сразу запускается SceneC. Это распространенный паттерн для разделения загрузки ресурсов и основной логики.

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

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

Финальная SceneC создает объект звука из загруженного в предыдущей сцене ключа theme и запускает его воспроизведение. Здесь также проводится проверка на блокировку аудио, хотя пользователь уже взаимодействовал с игрой в SceneA.

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

Важно понимать, что объект звука, созданный через this.sound.add(), существует в контексте текущей сцены и менеджера звука игры. Поскольку предыдущий звук был явно остановлен, конфликта не возникает.

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

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

  1. Убрать вызов this.jungle.stop() и услышать наложение музыки
  2. Использовать фоновую музыку, которая должна играть непрерывно во всех сценах (для этого не останавливайте её и управляйте громкостью)
  3. Реализовать плавное затухание (fade out) перед остановкой с помощью метода fadeOut