О чем этот пример
Создание игр — это не только графика и геймплей, но и атмосфера, которую во многом формирует звук. Однако звуковые эффекты и музыка должны быть тесно связаны с визуальным рядом. Этот пример демонстрирует мощный подход к синхронизации скорости анимации (`timeScale`) с темпом воспроизведения аудио (`rate`), а также к визуальному отображению параметров звука, таких как громкость и заглушение. Вы научитесь создавать динамичные сцены, где анимация буквально "танцует" под музыку, и получите полный контроль над аудиопотоками через удобный интерфейс dat.GUI.
Версия 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 кадров. Ключевой момент — использование массива объектов для хранения данных о кадрах, что удобно для последующей передачи в создание анимации.
for (let i = 0; i < 12; i++)
{
this.horseFrames.push({
key: `horse${(`0${i}`).slice(-2)}`,
frame: '__BASE'
});
}
Загрузка самих изображений и аудиофайлов происходит в цикле и с помощью this.load.audio. Обратите внимание, что для аудио указаны два формата (ogg и mp3) для обеспечения кроссбраузерной совместимости.
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 мы создаем анимацию из подготовленных кадров и два спрайта для левой и правой лошади. Сразу создаются и запускаются на воспроизведение два звуковых объекта (this.soundLeft и this.soundRight).
Важная часть логики — обработка блокировки аудио в браузере. Свойство this.sound.locked проверяет, требуется ли пользовательское взаимодействие для начала воспроизведения. Если да, то выводится текст с подсказкой, и основной метод start() вызывается только после события unlocked.
if (this.sound.locked)
{
this.sound.once('unlocked', (soundManager) =>
{
text.visible = false;
this.start();
});
}
else
{
this.start();
}
Сердце синхронизации: метод update
Магия синхронизации происходит в методе update, который вызывается на каждом кадре. Здесь скорость анимации каждой лошади (anims.timeScale) привязывается к итоговой скорости воспроизведения её звуковой дорожки (totalRate). totalRate — это рассчитанное значение, учитывающее как глобальную скорость (this.sound.rate), так и локальную (this.soundLeft.rate).
this.horseLeft.anims.timeScale = this.soundLeft.totalRate;
Прозрачность (alpha) спрайтов зависит от произведения глобальной и локальной громкости, что позволяет создавать сложные эффекты затухания. Видимость (visible) спрайтов управляется флагами заглушения (mute) как глобального менеджера звука, так и отдельных объектов.
this.horseLeft.visible = !this.sound.mute && !this.soundLeft.mute;
Интерфейс для тонкой настройки: dat.GUI
Метод start() создает панель управления с помощью библиотеки dat.GUI. Это позволяет в реальном времени изменять ключевые параметры звука. Интерфейс разделен на три папки: для глобального менеджера звука (Sound Manager), для левого (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();
Важная конфигурация: `audio.noAudio`
В конфигурации игры есть критически важная для этого примера настройка. Параметр audio: { noAudio: true } указывает Phaser создать полноценную аудиосистему, даже если на устройстве нет аудиоустройств или кодеков. Без этого в некоторых окружениях объекты this.sound и this.soundLeft/Right могли бы быть не созданы, что привело бы к ошибкам при обращении к их свойствам в update.
const config = {
// ... другие настройки ...
audio: {
noAudio: true
}
};
Эта настройка гарантирует, что API звука будет доступно для управления через GUI и для расчетов, даже если физическое воспроизведение невозможно.
Что попробовать дальше
Этот пример раскрывает мощь Phaser в создании глубоко связанных аудиовизуальных композиций. Вы можете экспериментировать: привязать detune к оттенку цвета спрайта, синхронизировать частицы с битами музыки, используя анализ аудиоданных, или создать систему, где громкость звука влияет на масштаб или размытие объектов. Главное — понимание, что свойства totalRate, volume и mute звуковых объектов можно использовать не только по назначению, но и как источник данных для управления визуальным миром.
