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

Управление воспроизведением звука при переключении между игровыми сценами — частая задача, которая может привести к наложению дорожек или внезапной тишине. В этой статье мы разберем пример, где музыка плавно останавливается в одной сцене и запускается в другой. Вы научитесь правильно обрабатывать аудио в многопоточной структуре 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), которые запускаются последовательно. Ключевая идея — показать, как остановить фоновую музыку из первой сцены перед запуском новой музыки в третьей. Сцена SceneB служит промежуточным звеном для загрузки нового аудио-актива, демонстрируя разделение ответственности.

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

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

SceneA: Загрузка, воспроизведение и остановка первого трека

В SceneA загружается и запускается на повторе аудиофайл 'jungle'. Важный момент — обработка блокировки аудио браузером. Свойство this.sound.locked проверяет, требуется ли пользовательский ввод для разблокировки. Если да, то через событие unlocked откладывается настройка управления. В противном случае управление настраивается сразу.

Метод setupSceneInput создает одноразовый обработчик клика. По клику музыка явно останавливается вызовом this.jungle.stop(), что предотвращает её фоновое проигрывание после перехода. Затем запускается SceneB.

// Создание и воспроизведение звукового объекта
this.jungle = this.sound.add('jungle');
this.jungle.play({
    loop: true
});

// Обработка блокировки аудио
if (this.sound.locked) {
    this.sound.once('unlocked', function (soundManager) {
        this.setupSceneInput(text);
    }, this);
}

// В обработчике клика:
this.jungle.stop();
this.scene.start('sceneB');

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

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

preload () {
    this.load.audio('theme', [
        'assets/audio/oedipus_wizball_highscore.ogg',
        'assets/audio/oedipus_wizball_highscore.mp3'
    ]);
}

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

SceneC: Воспроизведение новой музыки с проверкой блокировки

В финальной сцене создается и запускается новый звуковой объект music. Здесь также присутствует проверка на this.sound.locked. Если аудио заблокировано, выводится текст с инструкцией, который скрывается после события unlocked. Обратите внимание: музыкальный объект создается и команда play выполняется сразу, но фактическое воспроизведение начнется только после разблокировки аудиосистемы браузером.

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

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

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

1.  **Менеджер звука (`this.sound`)**: Центральный объект для управления аудио. Через него добавляются звуки (`this.sound.add()`), проверяется блокировка (`this.sound.locked`) и обрабатываются глобальные события (`this.sound.once('unlocked', ...)`).
2.  **Звуковые объекты**: Экземпляры, возвращаемые `this.sound.add()`. Позволяют контролировать конкретную дорожку: `play()`, `stop()`, настраивать `loop`.
3.  **Явная остановка звука**: Критически важно вызывать `stop()` у звука из предыдущей сцены перед `this.scene.start()`, чтобы избежать наложения.
4.  **Обработка блокировки**: Паттерн с проверкой `this.sound.locked` и подпиской на `unlocked` обязателен для корректной работы в мобильных браузерах и некоторых десктопных.
5.  **Загрузка в промежуточной сцене**: Использование отдельной сцены для предзагрузки ассетов — эффективный способ организации потока без "заморозки" интерфейса.

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

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

  1. Убрать вызов this.jungle.stop() и услышать наложение музыки
  2. Перенести загрузку 'theme' в SceneA и убрать SceneB
  3. Включить Web Audio API (убрать disableWebAudio: true) и проверить, исчезнет ли необходимость в обработке locked