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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    audioContext;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.spritesheet('explosion', 'assets/atlas/trimsheet/explosion.png', { frameWidth: 64, frameHeight: 64 });
        this.load.spritesheet('bomb', 'assets/sprites/xenon2_bomb.png', { frameWidth: 8, frameHeight: 16 });
        this.load.audio('explosion', [ 'assets/audio/SoundEffects/explosion.mp3' ]);
    }

    create ()
    {
        this.anims.create({
            key: 'rotate',
            frames: this.anims.generateFrameNumbers('bomb', { start: 0, end: 3, first: 3 }),
            frameRate: 20,
            repeat: -1
        });

        this.anims.create({
            key: 'explode',
            frames: this.anims.generateFrameNumbers('explosion', { start: 0, end: 23, first: 23 }),
            frameRate: 20
        });

        const bomb = this.add.sprite(400, 300, 'bomb');
        bomb.setScale(6, -6);
        bomb.anims.play('rotate');

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

            bomb.visible = false;

            const boom = this.add.sprite(400, 300, 'explosion');
            boom.setScale(6);
            boom.anims.play('explode');

            const explosion = this.sound.add('explosion', {
                volume: 0.5
            });

            explosion.on('complete', function (sound)
            {

                setTimeout(() =>
                {

                    this.sys.game.destroy(true);

                    document.addEventListener('mousedown', function newGame ()
                    {

                        game = new Phaser.Game(config);

                        document.removeEventListener('mousedown', newGame);

                    });

                });

            }, this);

            explosion.play();

        }, this);
    }
}

try
{
    this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
catch (e)
{
    console.error(e);
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    scene: Example,
    pixelArt: true,
    audio: {
        context: this.audioContext
    }
};

let game = new Phaser.Game(config);

Проблема: Браузер блокирует аудио

Современные браузеры (Chrome, Firefox и др.) требуют, чтобы воспроизведение аудио было инициировано действием пользователя (клик, нажатие клавиши). Это защита от навязчивого авто-воспроизведения. Однако, если вы создаёте новый AudioContext после уничтожения игры (например, при перезапуске), браузер может заблокировать его создание, так как это действие уже не связано с прямым жестом пользователя.

Код вне класса Example демонстрирует попытку создать контекст глобально:

Решение: Создание и передача контекста в конфиг

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

try
{
    this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
catch (e)
{
    console.error(e);
}

const config = {
    // ... другие настройки ...
    audio: {
        context: this.audioContext // Передаём существующий контекст
    }
};

Ключевой момент — свойство context внутри объекта audio конфигурации. Phaser принимает его и использует для всей внутренней работы со звуком.

Механика перезапуска игры

В примере игра завершается взрывом бомбы. После завершения звука взрыва игра уничтожается, но глобальный audioContext остаётся жив. Новый клик пользователя создаёт экземпляр игры заново, используя тот же самый аудиоконтекст.

Код в обработчике события complete звука взрыва:

explosion.on('complete', function (sound)
{
    setTimeout(() =>
    {
        // Уничтожаем текущий экземпляр игры
        this.sys.game.destroy(true);

        // Ждём нового клика пользователя для перезапуска
        document.addEventListener('mousedown', function newGame ()
        {
            // Создаём новую игру, конфиг уже содержит наш audioContext
            game = new Phaser.Game(config);
            document.removeEventListener('mousedown', newGame);
        });
    });
}, this);

Обратите внимание: this.sys.game.destroy(true) уничтожает игровой инстанс, но не трогает объект audioContext, созданный ранее. Новый Phaser.Game получает его из конфига.

Загрузка и воспроизведение звука

Звуковой файл загружается стандартным для Phaser способом в preload(). В create() звук создаётся как объект Sound с помощью this.sound.add(), и ему задаётся громкость. Важно, что Phaser использует для этого объекта наш переданный в конфиге аудиоконтекст.

// В preload
this.load.audio('explosion', [ 'assets/audio/SoundEffects/explosion.mp3' ]);

// В create, внутри обработчика клика
const explosion = this.sound.add('explosion', {
    volume: 0.5 // Устанавливаем громкость
});
explosion.play();

Важные детали реализации

1. **Обработка ошибок:** Создание AudioContext обёрнуто в try...catch, так как некоторые старые браузеры или специфичные настройки могут его не поддерживать. 2. **Проверка поддержки:** Используется конструкция window.AudioContext || window.webkitAudioContext для кросс-браузерной совместимости. 3. **Привязка контекста (this):** В обработчике события pointerdown и в коллбеке on('complete') используется , this) в конце, чтобы сохранить правильный контекст выполнения (экземпляр сцены) внутри функций. 4. **setTimeout при перезапуске:** Небольшая задержка перед уничтожением игры и подпиской на новый клик может помочь избежать потенциальных конфликтов событий.

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

Переиспользование одного экземпляра AudioContext — надёжный способ гарантировать работу звука в вашей Phaser-игре, даже при её динамической перезагрузке. Этот подход соответствует требованиям браузеров к взаимодействию с пользователем. Для экспериментов попробуйте: управлять состоянием контекста (приостанавливать suspend() и возобновлять resume() по требованию), создать механизм "пула" звуковых объектов для часто воспроизводимых эффектов или интегрировать этот подход с системой управления звуком в большой игре.