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

При перезагрузке игры в браузере важно правильно управлять аудиоресурсами, особенно AudioContext. Если создать его заново, можно столкнуться с ошибками или потерей звука. В этой статье мы разберем, как создать и передать единый экземпляр Web Audio API в Phaser, чтобы избежать утечек и обеспечить стабильную работу звука при перезапуске сцены или всей игры.

Версия 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);

Проблема: Браузер блокирует создание нового AudioContext

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

Ключевой шаг — создать AudioContext до инициализации игры и передать его в конфигурацию. Это гарантирует, что при последующих перезапусках игры мы будем использовать один и тот же аудиоконтекст.

Создаем и передаем AudioContext в конфиг

Вне класса сцены, до создания игры, мы пытаемся создать AudioContext. Используется проверка на поддержку префиксных версий для старых браузеров. Контекст сохраняется в переменную, которая затем передается в конфигурацию игры в поле audio.context.

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 // Передаем созданный контекст
    }
};

Логика сцены: анимация и звук взрыва

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

Звук добавляется через this.sound.add, ему задается уменьшенная громкость. Важный момент — отслеживание события complete у звукового объекта. Когда воспроизведение заканчивается, запускается цепочка действий по перезапуску игры.

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

explosion.on('complete', function (sound)
{
    // Действия после завершения звука
}, this);

explosion.play();

Полное уничтожение игры и перезапуск

После завершения звука взрыва, внутри обработчика complete, игра не перезапускается мгновенно. Используется setTimeout без задержки (или с нулевой задержкой) для того, чтобы выйти из текущего стека вызовов Phaser. Затем игра полностью уничтожается методом this.sys.game.destroy(true). Аргумент true означает, что будут также очищены загрузчик, кэш текстуры и другие ресурсы.

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

setTimeout(() =>
{
    this.sys.game.destroy(true);

    document.addEventListener('mousedown', function newGame ()
    {
        game = new Phaser.Game(config);
        document.removeEventListener('mousedown', newGame);
    });
});

Почему это работает и каковы преимущества

Переиспользование одного AudioContext решает несколько проблем: 1. **Избегание ошибок браузера:** Некоторые браузеры ограничивают количество одновременных или последовательных аудиоконтекстов. 2. **Сохранение состояния:** Если в вашей игре есть глобальные настройки звука (громкость, эффекты), они не будут сброшены при перезапуске, так как контекст один. 3. **Контроль над жизненным циклом:** Вы сами решаете, когда создавать и когда уничтожать аудиоконтекст, что важно для сложных приложений.

Phaser, получив внешний контекст, не будет пытаться создать свой и будет использовать предоставленный для всех звуковых операций внутри этой игровой сессии.

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

Использование общего AudioContext — это продвинутая, но важная техника для создания надежных игр на Phaser, особенно если предполагаются частые перезагрузки или горячая перезамена сцен. Для экспериментов попробуйте: 1. Добавить кнопку перезапуска без уничтожения игры, а только перезагрузки сцены, и посмотреть, как поведет себя звук. 2. Создать UI-слайдер для изменения громкости главного контекста (this.audioContext) и убедиться, что настройка сохраняется после перезапуска игры. 3. Реализовать аудио-менеджер, который будет хранить ссылку на контекст и управлять всеми звуками игры централизованно.