О чем этот пример
Визуализация параметров звука — мощный приём для создания выразительных игровых сцен. Эта статья на примере анимированных лошадей, скачущих в такт музыке, показывает, как в Phaser можно связать скорость и прозрачность анимации с громкостью, темпом и детюном звука. Вы научитесь работать с глобальными настройками аудио и индивидуальными звуковыми объектами, а также создавать интерактивный интерфейс для их тонкой настройки в реальном времени.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
soundRight;
soundLeft;
horseRight;
horseLeft;
i = 0;
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: {
disableWebAudio: true
}
};
for (; this.i < 12; this.i++)
{
this.horseFrames.push({
key: `horse${(`0${this.i}`).slice(-2)}`,
frame: '__BASE'
});
}
const game = new Phaser.Game(config);
Загрузка ресурсов: анимация и стереозвук
В методе preload происходит подготовка всех необходимых ресурсов. Для анимации лошади загружается 12 отдельных кадров (изображений). Умный приём — создание массива horseFrames с объектами, где key — это имя кадра (например, horse00), а frame установлен в '__BASE', что указывает на использование базового изображения, а не кадра из атласа.
for (let i = 0; i < 12; i++)
{
this.horseFrames.push({
key: `horse${(`0${i}`).slice(-2)}`,
frame: '__BASE'
});
}
Затем в цикле загружаются сами изображения, используя ключи из этого массива. Для звука загружаются два аудиофайла — left и right, каждый в двух форматах (OGG и MP3) для кроссбраузерной совместимости. Это создаёт основу для стереоэффекта.
Создание сцены: анимация, звук и разблокировка аудио
В методе create сначала создаётся анимация horse из загруженных кадров с частотой 20 кадров в секунду и бесконечным повторением. Создаются два спрайта лошадей в разных позициях экрана.
this.soundLeft = this.sound.add('left');
this.soundLeft.play({
loop: true
});
Затем создаются и запускаются на воспроизведение (с циклом) два звуковых объекта: soundLeft и soundRight. Важный момент — обработка блокировки аудио в браузерах. Свойство this.sound.locked проверяет, требуется ли пользовательское взаимодействие для начала воспроизведения. Если да, то выводится текст "Tap to start", и сцена start запускается только после события unlocked. В противном случае start вызывается сразу.
Магия синхронизации в методе Update
Сердце визуализации — метод 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;
1. **Скорость анимации (`timeScale`):** Устанавливается равной `totalRate` каждого звука. `totalRate` — это итоговый темп с учётом как глобального `rate` менеджера звуков (`this.sound`), так и локального `rate` самого звукового объекта. Это заставляет лошадей скакать быстрее или медленнее в такт музыке.
2. **Прозрачность (`alpha`):** Рассчитывается как произведение глобальной громкости (`this.sound.volume`) и громкости конкретного звука. Чем тише звук — тем прозрачнее лошадь.
3. **Видимость (`visible`):** Лошадь становится невидимой, если включён глобальный (`this.sound.mute`) или локальный (`this.soundLeft.mute`) mute. Это наглядно демонстрирует работу отключения звука.
Интерактивная панель управления с dat.GUI
Метод start, вызываемый после разблокировки аудио, запускает анимацию лошадей и создаёт панель управления с помощью библиотеки 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();
- **Sound Manager:** Управляет глобальными настройками — mute, volume, rate (темп), detune (детюн в центах). Эти параметры влияют на все звуки в игре.
- **Left / Right:** Управляют одноимёнными звуковыми объектами, предлагая те же настройки (mute, volume, rate, detune), но на индивидуальном уровне. Флаг .listen() заставляет интерфейс автоматически обновляться при изменении значений в коде.
Изменения, сделанные в GUI, мгновенно отражаются в методе update, создавая живую, отзывчивую визуализацию.
Важная деталь конфигурации: HTML5 Audio
В конфигурации игры есть ключевая настройка, которая определяет поведение аудиосистемы:
const config = {
// ... другие настройки ...
audio: {
disableWebAudio: true
}
};
Параметр disableWebAudio: true заставляет Phaser использовать HTML5 Audio API вместо более мощного Web Audio API. Это может быть необходимо для совместимости или специфического поведения, но накладывает ограничения (например, на точность синхронизации или количество одновременно воспроизводимых звуков). В данном примере это, вероятно, сделано для демонстрации работы с базовым аудио.
Что попробовать дальше
Этот пример — отличная отправная точка для создания сложной аудиовизуальной обратной связи в ваших играх. Вы можете экспериментировать: привязать не timeScale, а выбор конкретного кадра анимации к частоте звука (используя FFT), синхронизировать с параметрами частиц или шейдеров, либо управлять скоростью игрового процесса (timeScale сцены) на основе темпа фоновой музыки. Понимание иерархии настроек звука (глобальные → локальные) и их визуальное представление открывает путь к созданию по-настоящему immersive-игр.
