О чем этот пример
При создании игр часто возникает необходимость плавно управлять фоновой музыкой при переходе между игровыми сценами. Например, остановить трек меню и запустить музыку уровня. Пример демонстрирует, как организовать такое управление, используя встроенный аудиоменеджер 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. Основные принципы: останавливать звуки текущей сцены перед переходом, загружать ресурсы для следующей сцены заранее (можно в промежуточной) и всегда учитывать возможность блокировки аудио браузером. Для экспериментов попробуйте
- Добавить плавное затухание звука (
fadeOut) перед остановкой - Создать глобальный менеджер аудио, доступный из всех сцен, для централизованного управления плейлистом
- Загружать аудио для всех сцен сразу в начальной загрузке, чтобы избежать промежуточных сцен
