О чем этот пример
Одной из ключевых задач при разработке игр является корректное управление аудиопотоками при переключении между игровыми состояниями или сценами. Неправильная обработка может привести к наложению музыки или её внезапному обрыву. В этой статье мы разберем пример из официальной документации 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), которые запускаются последовательно. Ключевая особенность — использование HTML5 Audio вместо Web Audio API, что задается в конфигурации игры. Это важно для совместимости.
const config = {
type: Phaser.AUTO,
// ... другие настройки ...
audio: {
disableWebAudio: true
},
scene: [ SceneA, SceneB, SceneC ]
};
В SceneA в методе preload загружаются аудиофайл jungle и другие ресурсы. Обратите внимание, что для обеспечения кроссбраузерной поддержки загружаются два формата файла: OGG и MP3.
this.load.audio('jungle', [
'assets/audio/jungle.ogg',
'assets/audio/jungle.mp3'
]);
Обработка блокировки аудио (Autoplay Policy)
Современные браузеры блокируют автоматическое воспроизведение аудио без взаимодействия пользователя. Phaser предоставляет свойство this.sound.locked для проверки этого состояния.
В create методе SceneA создается объект звука jungle и предпринимается попытка его воспроизвести. Если звук заблокирован (locked), на экране появляется инструкция, и воспроизведение откладывается до события unlocked.
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 настраивает обработчик клика, который останавливает текущую музыку и запускает следующую сцену. Это центральный момент для бесшовного перехода.
Координация остановки и запуска между сценами
Перед переходом из SceneA в SceneB необходимо явно остановить играющий звук. Это предотвращает его наложение на музыку из следующей сцены.
this.input.once('pointerup', function () {
this.jungle.stop(); // Явная остановка текущего трека
this.scene.start('sceneB');
}, this);
SceneB выступает в роли промежуточной сцены-загрузчика. В её методе preload загружается новый аудиофайл theme, после чего в create сразу запускается SceneC. Это распространенный паттерн для разделения загрузки ресурсов и основной логики.
create ()
{
console.log('SceneB');
this.scene.start('sceneC');
}
Воспроизведение нового трека в конечной сцене
Финальная SceneC создает объект звука из загруженного в предыдущей сцене ключа theme и запускает его воспроизведение. Здесь также проводится проверка на блокировку аудио, хотя пользователь уже взаимодействовал с игрой в SceneA.
const music = this.sound.add('theme');
music.play({
loop: true
});
Важно понимать, что объект звука, созданный через this.sound.add(), существует в контексте текущей сцены и менеджера звука игры. Поскольку предыдущий звук был явно остановлен, конфликта не возникает.
Что попробовать дальше
Пример наглядно показывает паттерн управления жизненным циклом аудио при переходах между сценами: явная остановка старого звука -> (опциональная промежуточная сцена для предзагрузки) -> создание и запуск нового звука в целевой сцене. Для экспериментов попробуйте
- Убрать вызов
this.jungle.stop()и услышать наложение музыки - Использовать фоновую музыку, которая должна играть непрерывно во всех сценах (для этого не останавливайте её и управляйте громкостью)
- Реализовать плавное затухание (fade out) перед остановкой с помощью метода
fadeOut
