О чем этот пример
Управление звуком — ключевой элемент игровой атмосферы. В Phaser 3 звуковая система предлагает богатые возможности: от базового воспроизведения до сложных аудиоспрайтов и глобального управления. Этот пример демонстрирует практически все аспекты работы с аудио через цепочку автоматических тестов, что делает его отличной отправной точкой для изучения. Разобрав этот код, вы научитесь загружать и проигрывать музыку, управлять параметрами вроде громкости и скорости, работать с событиями звука, а также использовать аудиоспрайты для эффективного воспроизведения коротких звуковых эффектов. Эти навыки необходимы для создания профессионального звукового сопровождения в вашей игре.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
/**
* @author Pavle Goloskokovic <pgoloskokovic@gmail.com> (http://prunegames.com)
*
* Prometheus Brings Fire To Mankind - Painting by Heinrich Füger, 1817, Public Domain
* The Creatures of Prometheus, Op. 43, Overture - Music by Ludwig van Beethoven, 1801, Public Domain
*/
var text;
var first;
var second;
var audioSprite;
class Example extends Phaser.Scene
{
constructor ()
{
super();
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
const head = document.getElementsByTagName('head')[0];
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://fonts.googleapis.com/css?family=Sorts+Mill+Goudy';
head.appendChild(link);
this.load.image('prometheus', 'assets/pics/Prometheus Brings Fire To Mankind.jpg');
this.load.audio('overture', [
'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/Overture.ogg',
'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/Overture.mp3'
],{
instances: 2
});
this.load.audioSprite('creatures', 'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/sprites.json', [
'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/sprites.ogg',
'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/sprites.mp3'
]);
}
create ()
{
this.sound.pauseOnBlur = false;
var prometheus = this.add.image(400, 300, 'prometheus');
prometheus.setScale(600/prometheus.height);
text = this.add.text(400, 300, 'Loading...', {
fontFamily: '\'Sorts Mill Goudy\', serif',
fontSize: 80,
color: '#fff',
align: 'center'
});
text.setOrigin(0.5);
text.setShadow(0, 1, "#888", 2);
first = this.sound.add('overture', { loop: true });
second = this.sound.add('overture', { loop: true });
audioSprite = this.sound.addAudioSprite('creatures');
this.enableInput();
}
chain(i)
{
return function()
{
if (tests[i])
{
tests[i].call(this, this.chain(++i));
}
else
{
text.setText('Complete!');
this.time.addEvent({
delay: 5000,
callback: this.enableInput,
callbackScope: this
});
}
}.bind(this);
}
enableInput()
{
text.setText('Click to start');
this.input.once('pointerdown', function (pointer)
{
tests[0].call(this, this.chain(1));
}, this);
}
}
const config = {
type: Phaser.AUTO,
parent: 'phaser-example',
width: 800,
height: 600,
scene: Example,
audio: {
disableWebAudio: true
}
};
const game = new Phaser.Game(config);
var tests = [
function (fn)
{
first.once('play', function (sound)
{
text.setText('Playing');
this.time.addEvent({
delay: 2000,
callback: fn,
callbackScope: this
});
}, this);
first.play();
},
function (fn)
{
first.once('pause', function (sound)
{
text.setText('Paused');
this.time.addEvent({
delay: 1500,
callback: fn,
callbackScope: this
});
}, this);
first.pause();
},
function(fn)
{
first.once('resume', function (sound)
{
text.setText('Resuming');
this.time.addEvent({
delay: 2000,
callback: fn,
callbackScope: this
});
}, this);
first.resume();
},
function(fn)
{
first.once('stop', function (sound)
{
text.setText('Stopped');
this.time.addEvent({
delay: 1500,
callback: fn,
callbackScope: this
});
}, this);
first.stop();
},
function(fn)
{
first.once('play', function (sound)
{
text.setText('Play from start');
this.time.addEvent({
delay: 2000,
callback: fn,
callbackScope: this
});
}, this);
first.play();
},
function(fn)
{
first.once('rate', function (sound, value)
{
text.setText('Speed up rate');
this.time.addEvent({
delay: 2000,
callback: fn,
callbackScope: this
});
}, this);
first.rate = 1.5;
},
function(fn)
{
first.once('detune', function (sound, value)
{
text.setText('Speed up detune');
this.time.addEvent({
delay: 2000,
callback: fn,
callbackScope: this
});
}, this);
first.detune = 600;
},
function(fn)
{
first.once('rate', function (sound, value)
{
text.setText('Slow down rate');
this.time.addEvent({
delay: 2000,
callback: fn,
callbackScope: this
});
}, this);
first.rate = 1;
},
function(fn)
{
first.once('detune', function (sound, value)
{
text.setText('Slow down detune');
this.time.addEvent({
delay: 2000,
callback: fn,
callbackScope: this
});
}, this);
first.detune = 0;
},
function(fn)
{
this.tweens.add({
onStart: function ()
{
text.setText('Fade out');
},
targets: first,
volume: 0,
ease: 'Linear',
duration: 2000,
onComplete: fn
});
},
function(fn)
{
this.tweens.add({
onStart: function ()
{
text.setText('Fade in');
},
targets: first,
volume: 1,
ease: 'Linear',
duration: 2000,
onComplete: fn
});
},
function(fn)
{
first.once('mute', function()
{
text.setText('Mute');
this.time.addEvent({
delay: 1500,
callback: fn,
callbackScope: this
});
}, this);
first.mute = true;
},
function(fn)
{
first.once('mute', function()
{
text.setText('Unmute');
this.time.addEvent({
delay: 2000,
callback: fn,
callbackScope: this
});
}, this);
first.mute = false;
},
function(fn)
{
first.once('volume', function()
{
text.setText('Half volume');
this.time.addEvent({
delay: 2000,
callback: fn,
callbackScope: this
});
}, this);
first.volume = 0.5;
},
function(fn)
{
first.once('volume', function()
{
text.setText('Full volume');
this.time.addEvent({
delay: 2000,
callback: fn,
callbackScope: this
});
}, this);
first.volume = 1;
},
function(fn)
{
first.once('seek', function()
{
text.setText('Seek to start');
this.time.addEvent({
delay: 2000,
callback: fn,
callbackScope: this
});
}, this);
first.seek = 0;
},
function(fn)
{
second.once('play', function()
{
text.setText('Play 2nd');
this.time.addEvent({
delay: 2000,
callback: fn,
callbackScope: this
});
}, this);
second.play();
},
function(fn)
{
this.sound.once('mute', function (soundManager, value)
{
text.setText('Mute global');
this.time.addEvent({
delay: 1500,
callback: fn,
callbackScope: this
});
}, this);
this.sound.mute = true;
},
function(fn)
{
this.sound.once('mute', function (soundManager, value)
{
text.setText('Unmute global');
this.time.addEvent({
delay: 2000,
callback: fn,
callbackScope: this
});
}, this);
this.sound.mute = false;
},
function(fn)
{
this.sound.once('volume', function (soundManager, value)
{
text.setText('Half volume global');
this.time.addEvent({
delay: 2000,
callback: fn,
callbackScope: this
});
}, this);
this.sound.volume = 0.5;
},
function(fn)
{
this.tweens.add({
onStart: function ()
{
text.setText('Fade out global');
},
targets: this.sound,
volume: 0,
ease: 'Linear',
duration: 2000,
onComplete: fn
});
},
function(fn)
{
this.tweens.add({
onStart: function ()
{
text.setText('Fade in global');
},
targets: this.sound,
volume: 1,
ease: 'Linear',
duration: 2000,
onComplete: fn
});
},
function(fn)
{
this.sound.once('pauseall', function (soundManager)
{
text.setText('Pause all');
this.time.addEvent({
delay: 1500,
callback: fn,
callbackScope: this
});
}, this);
this.sound.pauseAll();
},
function(fn)
{
this.sound.once('resumeall', function (soundManager)
{
text.setText('Resume all');
this.time.addEvent({
delay: 2000,
callback: fn,
callbackScope: this
});
}, this);
this.sound.resumeAll();
},
function(fn)
{
this.sound.once('stopall', function (soundManager)
{
text.setText('Stop all');
this.time.addEvent({
delay: 1500,
callback: fn,
callbackScope: this
});
}, this);
this.sound.stopAll();
},
function(fn)
{
audioSprite.once('play', function (sound)
{
text.setText('Play sprite');
this.time.addEvent({
delay: 1500,
callback: fn,
callbackScope: this
});
}, this);
audioSprite.play('07');
},
function(fn)
{
audioSprite.once('pause', function (sound)
{
text.setText('Pause sprite');
this.time.addEvent({
delay: 1000,
callback: fn,
callbackScope: this
});
}, this);
audioSprite.pause();
},
function(fn)
{
audioSprite.once('resume', function (sound)
{
text.setText('Resume sprite');
this.time.addEvent({
delay: 1500,
callback: fn,
callbackScope: this
});
}, this);
audioSprite.resume();
},
function(fn)
{
audioSprite.once('play', function (sound)
{
text.setText('Multiple sprites');
this.time.addEvent({
delay: 10000,
callback: fn,
callbackScope: this
});
}, this);
var sounds = ['01', '02', '03', '03', '05'];
for (var i=0; i<sounds.length; i++)
{
this.time.addEvent({
delay: i * 2000,
callback: audioSprite.play.bind(audioSprite, sounds[i]),
callbackScope: audioSprite
});
}
},
function(fn)
{
audioSprite.once('play', function (sound)
{
text.setText('Loop sprite');
this.time.addEvent({
delay: 4000,
callback: fn,
callbackScope: this
});
}, this);
audioSprite.play('06', {
loop: true
});
},
function(fn)
{
this.tweens.add({
onStart: function ()
{
text.setText('Fade out sprite');
},
targets: audioSprite,
volume: 0,
ease: 'Linear',
duration: 4000,
onComplete: function()
{
audioSprite.volume = 1;
audioSprite.stop();
fn();
}
});
}
];
Настройка и загрузка аудио
Перед использованием звуков их необходимо загрузить. В методе preload() сцены мы видим три разных способа загрузки аудиоресурсов.
Одиночный аудиофайл загружается с помощью load.audio(). Важно указать несколько форматов (например, .ogg и .mp3) для кроссбраузерной совместимости. Параметр instances определяет, сколько независимых экземпляров этого звука можно создать одновременно — это полезно для наложения одного и того же звука.
Аудиоспрайт — это один аудиофайл, содержащий множество коротких звуков (спрайтов), и JSON-файл с метаданными (временными отметками для каждого спрайта). Он загружается методом load.audioSprite(). Это эффективно для управления множеством звуковых эффектов.
this.load.audio('overture', [
'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/Overture.ogg',
'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/Overture.mp3'
],{
instances: 2
});
this.load.audioSprite('creatures', 'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/sprites.json', [
'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/sprites.ogg',
'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/sprites.mp3'
]);
Также в конфигурации игры отключается WebAudio API, что заставляет Phaser использовать HTML5 Audio. Это может быть полезно для специфических случаев совместимости.
const config = {
// ... другие настройки
audio: {
disableWebAudio: true
}
};
Создание звуковых объектов и управление ими
В методе create() загруженные аудиоресурсы превращаются в игровые объекты, с которыми можно взаимодействовать.
Из одного аудиоключа 'overture' создаются два независимых экземпляра звука (first и second). Это позволяет, например, проигрывать одну и ту же музыку с наложением или с разными параметрами. Аудиоспрайт создаётся одним вызовом addAudioSprite() — он даёт доступ ко всем звуковым фрагментам, описанным в JSON.
first = this.sound.add('overture', { loop: true });
second = this.sound.add('overture', { loop: true });
audioSprite = this.sound.addAudioSprite('creatures');
У каждого экземпляра Sound есть набор свойств и методов для контроля воспроизведения. В примере последовательно демонстрируются:
- Базовые операции: play(), pause(), resume(), stop().
- Контроль параметров: изменение rate (скорости), detune (стройки), volume (громкости), mute (отключения звука).
- Перемотка с помощью свойства seek.
Важный нюанс: многие свойства, такие как rate или volume, являются сеттерами, которые генерируют одноимённые события при изменении. Именно на эти события ('rate', 'volume') и подписываются обработчики в примере, чтобы синхронизировать цепочку тестов.
Работа со звуковыми событиями
Phaser звуковая система построена на событиях. Это позволяет легко реагировать на изменения состояния: начало воспроизведения, паузу, изменение громкости и т.д.
В примере для синхронизации последовательности тестов используется паттерн с колбэками и метод once(). Этот метод подписывает обработчик на одно срабатывание события, после чего автоматически отписывает его.
first.once('pause', function (sound) {
text.setText('Paused');
this.time.addEvent({
delay: 1500,
callback: fn, // fn — это следующий тест в цепочке
callbackScope: this
});
}, this);
first.pause(); // Вызов метода генерирует событие 'pause'
Таким образом, каждый тест инициирует действие (например, pause()), ждёт соответствующего события и по его получению, через задержку delay, запускает следующий тест. Это создаёт чёткую, управляемую последовательность демонстрации.
Глобальное управление звуком
Помимо управления отдельными звуками, Phaser предоставляет доступ к глобальному менеджеру звуков this.sound. Через него можно управлять всеми звуками в игре одновременно.
В примере показаны ключевые методы:
- `this.sound.pauseAll()` и `this.sound.resumeAll()` для приостановки и возобновления всего звука.
- `this.sound.stopAll()` для полной остановки.
- Свойства `this.sound.mute` и `this.sound.volume` для глобального отключения звука и управления общей громкостью. Изменения этих свойств также генерируют события (`'mute'`, `'volume'`).
// Глобальное отключение звука
this.sound.mute = true;
// Глобальное уменьшение громкости
this.sound.volume = 0.5;
// Остановка всех звуков
this.sound.stopAll();
Это особенно полезно для реализации опций в меню настроек игры, где игрок может одним ползунком регулировать общую громкость или кнопкой отключать звук.
Использование аудиоспрайтов и твинов
Аудиоспрайты идеально подходят для коротких звуковых эффектов (выстрелов, шагов, кликов). В примере audioSprite проигрывает конкретные спрайты по их ключам ('01', '07' и т.д.).
audioSprite.play('07'); // Воспроизвести спрайт с ключом '07'
Можно задавать параметры воспроизведения, например, зацикливание:
audioSprite.play('06', {
loop: true
});
Для плавного изменения свойств (например, для fade-in/fade-out эффектов) пример использует систему твинов Phaser. Твин анимирует значение свойства volume у звукового объекта от текущего до целевого за заданное время.
this.tweens.add({
targets: first, // Цель — звуковой объект
volume: 0, // Конечное значение громкости
duration: 2000, // Длительность анимации в миллисекундах
ease: 'Linear',
onComplete: fn // Колбэк по завершении твина
});
Этот подход гораздо эффективнее и плавнее, чем ручное изменение volume в цикле обновления игры.
Архитектура цепочки тестов
Весь пример построен вокруг массивов tests и chain(). Это умное архитектурное решение для последовательного выполнения демонстраций.
Массив tests содержит функции. Каждая функция принимает колбэк fn (следующий тест) и выполняет три действия:
1. Подписывается на событие звукового объекта с помощью once().
2. В обработчике события устанавливает задержку и планирует вызов колбэка fn.
3. Вызывает метод звукового объекта, который инициирует событие.
Функция chain() создает замыкание, которое по очереди вызывает функции из массива tests. Когда тесты заканчиваются, она выводит "Complete!" и через 5 секунд перезапускает демонстрацию.
function(fn) {
first.once('play', function (sound) {
text.setText('Playing');
this.time.addEvent({
delay: 2000,
callback: fn, // Запуск следующего теста
callbackScope: this
});
}, this);
first.play(); // Запуск события
}
Такая архитектура делает код модульным и легко расширяемым — новый тест это просто новая функция в массиве.
Что попробовать дальше
Пример охватывает весь спектр работы со звуком в Phaser 3: от загрузки и базового воспроизведения до тонкого контроля параметров, глобального управления и использования аудиоспрайтов. Ключевые моменты для запоминания: используйте события для синхронизации, твины для плавных переходов, а аудиоспрайты — для эффективной работы со звуковыми эффектами.
Для экспериментов попробуйте: создать свой аудиоспрайт из набора эффектов; реализовать систему микширования, где громкость музыки плавно снижается при появлении диалогов; привязать изменение rate или detune звука к игровой механике (например, замедление времени).
