О чем этот пример
В интерактивных играх и приложениях важна синхронизация визуальных эффектов со звуковым сопровождением. Этот пример демонстрирует, как в Phaser можно динамически связывать скорость анимации спрайтов с параметрами воспроизведения аудио. Вы научитесь управлять общими настройками звука (`SoundManager`) и отдельными звуковыми экземплярами, а также визуализировать эти изменения в реальном времени. Это полезно для создания ритм-игр, интерактивных музыкальных плееров или просто для улучшения обратной связи в проекте.
Версия 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() загружаются необходимые ресурсы: растровый шрифт, кадры анимации лошади и стерео-дорожки аудиофайлов. Обратите внимание, что для корректного отображения нумерации кадров используется форматирование строк с ведущими нулями.
for (let i = 0; i < 12; i++)
{
this.horseFrames.push({
key: `horse${(`0${i}`).slice(-2)}`,
frame: '__BASE'
});
}
Затем, в create(), создаётся анимация horse из подготовленных кадров. Важный момент: в конфигурации игры (config) указан параметр pixelArt: true, что обеспечивает чёткое отображение пиксельной графики, и audio: { disableWebAudio: true }, который заставляет Phaser использовать HTML5 Audio вместо Web Audio API. Это может быть необходимо для совместимости или специфического поведения.
Создание звуков и обработка блокировки
После загрузки создаются два звуковых экземпляра (soundLeft и soundRight) с помощью this.sound.add() и сразу запускаются в режиме зацикливания. В современных браузерах воспроизведение аудио может быть заблокировано до первого взаимодействия пользователя. Код проверяет свойство this.sound.locked.
if (this.sound.locked)
{
const text = this.add.bitmapText(400, 50, 'atari-classic', 'Tap to start', 40);
// ... позиционирование текста
this.sound.once('unlocked', (soundManager) =>
{
text.visible = false;
this.start();
});
}
else
{
this.start();
}
Если звук заблокирован, выводится текст с подсказкой, и метод start() вызывается только после события unlocked. Это обеспечивает корректную работу на мобильных устройствах и в строгих политиках браузеров.
Динамическая связь звука и анимации в update()
Вся магия синхронизации происходит в методе update(), который вызывается каждый кадр. Скорость анимации (anims.timeScale) каждого спрайта привязывается к итоговой скорости воспроизведения соответствующего звука. Итоговая скорость (totalRate) — это произведение глобальной rate и локальной rate звукового экземпляра.
this.horseLeft.anims.timeScale = this.soundLeft.totalRate;
this.horseRight.anims.timeScale = this.soundRight.totalRate;
Прозрачность (alpha) спрайтов вычисляется на основе произведения глобальной громкости и громкости каждого экземпляра. Видимость (visible) спрайтов отключается, если включён глобальный или локальный mute.
this.horseLeft.setAlpha(this.sound.volume * this.soundLeft.volume);
this.horseRight.visible = !this.sound.mute && !this.soundRight.mute;
Таким образом, любое изменение параметров звука через UI мгновенно отражается на анимации.
Интерактивное управление параметрами через dat.GUI
Метод start() создаёт интерактивную панель управления с помощью библиотеки dat.GUI. Она разделена на три папки: для глобального менеджера звуков (SoundManager) и для каждого из звуковых экземпляров.
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();
Метод .listen() заставляет элементы GUI автоматически обновляться при программном изменении значений. Параметры:
- mute: включение/выключение звука.
- volume: громкость от 0 до 1.
- rate: скорость воспроизведения (0.5 — в два раза медленнее, 2 — в два раза быстрее).
- detune: изменение тона в центах (полутон = 100 центов).
Это позволяет в реальном времени экспериментировать с настройками и сразу видеть и слышать результат.
Что попробовать дальше
Пример наглядно показывает, как система звука Phaser (особенно в режиме HTML5 Audio) предоставляет детальный контроль над воспроизведением и как эти параметры можно связать с визуальными элементами игры. Для экспериментов попробуйте: заменить анимацию на частицы, скорость которых зависит от rate; привязать detune к изменению цвета спрайта; или создать интерфейс, где игрок должен повторить мелодию, регулируя параметры, чтобы синхронизировать анимацию.
