О чем этот пример
Визуальная обратная связь для изменений звука делает игровой мир более живым и отзывчивым. Этот пример демонстрирует, как синхронизировать скорость анимации спрайта с темпом воспроизведения аудио, а прозрачность — с громкостью. Вы научитесь тонко настраивать звуковую среду игры, управляя глобальными и индивидуальными параметрами аудио через наглядный интерфейс, и получите готовый паттерн для создания синхронизированных аудиовизуальных эффектов.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
soundRight;
soundLeft;
horseRight;
horseLeft;
i = 0;
horseFrames = [];
init ()
{
for (let i = 0; i < 12; i++)
{
this.horseFrames.push({
key: `horse${(`0${i}`).slice(-2)}`,
frame: '__BASE'
});
}
}
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');
// 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', function (soundManager)
{
text.visible = false;
this.start.call(this);
}, this);
}
else
{
this.start.call(this);
}
}
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
};
const game = new Phaser.Game(config);
Инициализация и загрузка ресурсов
Класс Example расширяет Phaser.Scene. В методе init() подготавливается массив horseFrames с данными для 12 кадров анимации лошади. Каждый элемент содержит ключ (например, horse00) и ссылку на базовый фрейм.
В preload() сначала задается базовый URL для загрузки и загружается bitmap-шрифт. Затем в цикле загружаются изображения для каждого кадра анимации. Ключевой момент — загрузка стерео-аудио: два трека (left и right) загружаются с указанием форматов .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 FPS и бесконечным повторением. Создаются два спрайта лошадей с разными стартовыми кадрами и масштабируются.
Затем создаются и запускаются на воспроизведение (с циклом) два звуковых объекта: this.soundLeft и this.soundRight.
Важная часть — обработка ограничения автовоспроизведения в браузерах. Свойство this.sound.locked проверяет, заблокирован ли звук. Если да, то выводится текст с подсказкой, и основная логика (this.start) запускается только после события unlocked. Если звук не заблокирован, this.start вызывается сразу.
this.soundLeft = this.sound.add('left');
this.soundLeft.play({
loop: true
});
if (this.sound.locked)
{
this.sound.once('unlocked', function (soundManager)
{
text.visible = false;
this.start.call(this);
}, this);
}
Связь аудиопараметров с визуальными свойствами
Вся магия синхронизации происходит в update(), который вызывается каждый кадр.
Скорость анимации (anims.timeScale) каждого спрайта привязывается к итоговой скорости воспроизведения его звуковой дорожки. Свойство totalRate объекта звука учитывает и его собственный rate, и глобальный this.sound.rate.
Прозрачность (setAlpha) спрайта вычисляется как произведение глобальной громкости (this.sound.volume) на громкость конкретного звука. Это позволяет визуализировать как общую, так и индивидуальную настройку громкости.
Видимость спрайтов (visible) управляется флагами 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;
Интерактивная панель управления с dat.GUI
Метод start() запускает анимацию лошадей и создает интерактивную панель управления с помощью библиотеки dat.GUI. Это позволяет в реальном времени изменять параметры и сразу видеть (и слышать) результат.
Создаются три папки (Folders):
1. **Sound Manager**: управление глобальными настройками (mute, volume, rate, detune).
2. **Left** и **Right**: управление индивидуальными настройками для каждого из звуковых объектов.
Метод .listen() заставляет элементы GUI автоматически обновляться при изменении значений из кода. Параметры rate и detune влияют на высоту тона и скорость воспроизведения, что наглядно отражается на скорости анимации.
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();
const sl = gui.addFolder('Left');
sl.add(this.soundLeft, 'mute').listen();
sl.add(this.soundLeft, 'volume', 0, 1).listen();
Что попробовать дальше
Пример предоставляет готовую систему для создания динамической связи между звуком и графикой. Вы можете экспериментировать: привязать к параметрам звука не только скорость и прозрачность, но и размер, цвет или позицию спрайтов. Попробуйте использовать detune для создания эффекта «расстроенного» или ускоряющегося объекта. Этот подход отлично подойдет для визуализации аудио в ритмичных играх, музыкальных интерактивных сценах или для наглядной отладки сложной звуковой логики.
