О чем этот пример
Создание иммерсивного игрового опыта часто требует бесшовной синхронизации аудио и визуальных эффектов. Этот пример демонстрирует, как увязать скорость анимации спрайта с темпом воспроизведения звуковой дорожки, а также как реализовать сложную систему управления аудио через глобальные и локальные настройки. Вы научитесь управлять ключевыми параметрами звука: громкостью (`volume`), скоростью воспроизведения (`rate`), детуном (`detune`) и режимом отключения звука (`mute`), и тут же визуализировать эти изменения в реальном времени. Такой подход полезен для создания ритм-игр, динамичных кат-сцен или просто для добавления большей выразительности любым интерактивным элементам, где аудио и видео должны работать как единое целое.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
soundRight;
soundLeft;
horseRight;
horseLeft;
horseFrames = [];
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.bitmapFont('atari-classic', 'assets/fonts/bitmap/atari-classic.png', 'assets/fonts/bitmap/atari-classic.xml');
for (let i = 0; i < 12; i++)
{
this.horseFrames.push({
key: `horse${(`0${i}`).slice(-2)}`,
frame: '__BASE'
});
}
// Loading horse animation
for (let i = 0; i < this.horseFrames.length; i++)
{
this.load.image(this.horseFrames[i].key, `assets/animations/horse/frame_${(`0${i}`).slice(-2)}_delay-0.05s.png`);
}
// Loading music
this.load.audio('left', [
'assets/audio/Rossini - William Tell Overture (8 Bits Version)/left.ogg',
'assets/audio/Rossini - William Tell Overture (8 Bits Version)/left.mp3'
]);
this.load.audio('right', [
'assets/audio/Rossini - William Tell Overture (8 Bits Version)/right.ogg',
'assets/audio/Rossini - William Tell Overture (8 Bits Version)/right.mp3'
]);
}
create ()
{
this.anims.create({
key: 'horse',
frames: this.horseFrames,
frameRate: 20,
repeat: -1
});
this.horseLeft = this.add.sprite(200, 300, 'horse09');
this.horseLeft.setScale(400 / 480);
this.horseRight = this.add.sprite(600, 300, 'horse10');
this.horseRight.setScale(400 / 480);
this.soundLeft = this.sound.add('left');
this.soundLeft.play({
loop: true
});
this.soundRight = this.sound.add('right');
this.soundRight.play({
loop: true
});
if (this.sound.locked)
{
const text = this.add.bitmapText(400, 50, 'atari-classic', 'Tap to start', 40);
text.x -= Math.round(text.width / 2);
text.y -= Math.round(text.height / 2);
this.sound.once('unlocked', (soundManager) =>
{
text.visible = false;
this.start();
});
}
else
{
this.start();
}
}
update ()
{
this.horseLeft.anims.timeScale = this.soundLeft.totalRate;
this.horseRight.anims.timeScale = this.soundRight.totalRate;
this.horseLeft.setAlpha(this.sound.volume * this.soundLeft.volume);
this.horseRight.setAlpha(this.sound.volume * this.soundRight.volume);
this.horseLeft.visible = !this.sound.mute && !this.soundLeft.mute;
this.horseRight.visible = !this.sound.mute && !this.soundRight.mute;
}
start ()
{
this.horseLeft.play('horse');
this.horseRight.play('horse');
const gui = new dat.GUI();
const sm = gui.addFolder('Sound Manager');
sm.add(this.sound, 'mute').listen();
sm.add(this.sound, 'volume', 0, 1).listen();
sm.add(this.sound, 'rate', 0.5, 2).listen();
sm.add(this.sound, 'detune', -1200, 1200).step(50).listen();
sm.open();
const sl = gui.addFolder('Left');
sl.add(this.soundLeft, 'mute').listen();
sl.add(this.soundLeft, 'volume', 0, 1).listen();
sl.add(this.soundLeft, 'rate', 0.5, 2).listen();
sl.add(this.soundLeft, 'detune', -1200, 1200).step(50).listen();
sl.open();
const sr = gui.addFolder('Right');
sr.add(this.soundRight, 'mute').listen();
sr.add(this.soundRight, 'volume', 0, 1).listen();
sr.add(this.soundRight, 'rate', 0.5, 2).listen();
sr.add(this.soundRight, 'detune', -1200, 1200).step(50).listen();
sr.open();
}
}
/**
* @author Pavle Goloskokovic <pgoloskokovic@gmail.com> (http://prunegames.com)
*
* Images by Walter Newton (http://walternewton.tumblr.com/post/58684376490/like-a-train-in-the-night)
* Music by Classical 8 Bit (http://classical8bit.tumblr.com/) / CC BY
*/
const config = {
type: Phaser.AUTO,
parent: 'phaser-example',
width: 800,
height: 600,
scene: Example,
pixelArt: true,
audio: {
noAudio: true
}
};
const game = new Phaser.Game(config);
Подготовка ресурсов и настройка сцены
В методе preload() происходит загрузка всех необходимых ресурсов. Для анимации лошади используется набор из 12 отдельных изображений, которые загружаются в массив horseFrames. Каждый кадр представлен объектом с ключом и меткой кадра __BASE, что указывает на использование базового изображения.
Аудиофайлы загружаются в двух форматах (OGG и MP3) для обеспечения кросс-браузерной совместимости. Обратите внимание на структуру папок и имена файлов в путях.
for (let i = 0; i < 12; i++)
{
this.horseFrames.push({
key: `horse${(`0${i}`).slice(-2)}`,
frame: '__BASE'
});
}
this.load.audio('left', [
'assets/audio/Rossini - William Tell Overture (8 Bits Version)/left.ogg',
'assets/audio/Rossini - William Tell Overture (8 Bits Version)/left.mp3'
]);
Создание анимации и воспроизведение звука
В методе create() создается анимация 'horse' из подготовленных кадров с частотой 20 кадров в секунду и бесконечным повторением. Спрайты лошадей создаются из конкретных кадров (09 и 10) и масштабируются.
Звуковые объекты создаются через this.sound.add() и сразу запускаются в режиме зацикливания (loop: true). Важный момент — обработка блокировки аудио в браузере. Если аудиосистема заблокирована (свойство this.sound.locked), на экране появляется текст с подсказкой, и воспроизведение начнется только после события unlocked.
this.anims.create({
key: 'horse',
frames: this.horseFrames,
frameRate: 20,
repeat: -1
});
if (this.sound.locked)
{
this.sound.once('unlocked', (soundManager) =>
{
text.visible = false;
this.start();
});
}
else
{
this.start();
}
Связывание звука и анимации в реальном времени
Сердце примера — метод update(), который выполняется каждый кадр. Здесь происходит синхронизация состояния спрайтов с параметрами звука.
Свойство totalRate звукового объекта (учитывающее и глобальный rate, и локальный) напрямую задает timeScale анимации. Это заставляет лошадь бежать быстрее или медлее в такт музыке.
Прозрачность (alpha) спрайта вычисляется как произведение глобальной громкости (this.sound.volume) и громкости конкретного звука. Видимость (visible) спрайта отключается, если звук заглушен либо на глобальном уровне (this.sound.mute), либо на уровне самого звукового объекта.
this.horseLeft.anims.timeScale = this.soundLeft.totalRate;
this.horseRight.anims.timeScale = this.soundRight.totalRate;
this.horseLeft.setAlpha(this.sound.volume * this.soundLeft.volume);
this.horseRight.setAlpha(this.sound.volume * this.soundRight.volume);
this.horseLeft.visible = !this.sound.mute && !this.soundLeft.mute;
this.horseRight.visible = !this.sound.mute && !this.soundRight.mute;
Интерактивная панель управления аудио
Метод start() вызывается после разблокировки аудио. В нем запускаются анимации лошадей и создается графический интерфейс с помощью библиотеки dat.GUI.
Интерфейс разделен на три папки: 'Sound Manager' для глобальных настроек и по папке для каждого из стереоканалов ('Left', 'Right'). Для каждого параметра (mute, volume, rate, detune) создается ползунок. Флаг .listen() заставляет интерфейс автоматически обновляться при изменении значения из кода.
Особенность detune — это изменение высоты тона в центах (100 центов = 1 полутон). С помощью .step(50) ползунок настраивается с шагом в 50 центов.
const sm = gui.addFolder('Sound Manager');
sm.add(this.sound, 'mute').listen();
sm.add(this.sound, 'volume', 0, 1).listen();
sm.add(this.sound, 'rate', 0.5, 2).listen();
sm.add(this.sound, 'detune', -1200, 1200).step(50).listen();
Важная конфигурация: режим `noAudio`
Ключевой момент конфигурации игры — параметр audio: { noAudio: true } в объекте config. Этот параметр не отключает звук, а включает эмуляцию Web Audio API.
Зачем это нужно? Некоторые браузеры (особенно на мобильных устройствах) блокируют автовоспроизведение аудио до первого взаимодействия пользователя. Режим noAudio создает "тихий" звуковой контекст, который не блокируется. Это позволяет вашему коду, управляющему звуком (например, расчет totalRate), работать корректно, даже если реальное аудио еще не играет. После разблокировки (unlocked) система переключается на реальное воспроизведение.
const config = {
// ... другие настройки
audio: {
noAudio: true
}
};
Что попробовать дальше
Этот пример наглядно демонстрирует мощь и гибкость аудиосистемы Phaser. Вы научились не просто проигрывать музыку, а создавать глубокую связь между звуковым миром и графикой, управлять параметрами на разных уровнях и корректно обрабатывать ограничения браузеров.
Для экспериментов попробуйте: заменить анимацию лошади на частицы, скорость и размер которых зависят от rate и volume; использовать detune для создания эффекта "пьяного" или поврежденного персонажа; или привязать параметры звука не к ползункам, а к игровой логике, например, чтобы скорость музыки увеличивалась с ростом скорости игрока.
