О чем этот пример
Управление воспроизведением звука при переключении между игровыми сценами — частая задача, которая может привести к наложению дорожек или внезапной тишине. В этой статье мы разберем пример, где музыка плавно останавливается в одной сцене и запускается в другой. Вы научитесь правильно обрабатывать аудио в многопоточной структуре 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. **Загрузка в промежуточной сцене**: Использование отдельной сцены для предзагрузки ассетов — эффективный способ организации потока без "заморозки" интерфейса.
Что попробовать дальше
Пример наглядно демонстрирует правильный пайплайн управления аудио при переходах между сценами: явная остановка предыдущего трека, опциональная промежуточная загрузка и запуск нового звука с учетом блокировки браузера. Для экспериментов попробуйте
- Убрать вызов
this.jungle.stop()и услышать наложение музыки - Перенести загрузку 'theme' в
SceneAи убратьSceneB - Включить Web Audio API (убрать
disableWebAudio: true) и проверить, исчезнет ли необходимость в обработкеlocked
