О чем этот пример
Создание игр часто требует точной синхронизации действий на экране со звуковым сопровождением. В этом примере из официальной коллекции Phaser показано, как управлять позицией воспроизведения аудио (seek) и связывать его с анимацией персонажа. Вы научитесь не только запускать звук с определённой временной точки, но и создавать интерактивный интерфейс для управления звуком в реальном времени, что полезно для создания кат-сцен, музыкальных игр или просто отладки аудиодорожек.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
rainbowMask;
catAstroPhi;
cat;
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');
this.load.image('bg', 'assets/animations/nyan/bg.png');
this.load.image('rainbow', 'assets/animations/nyan/rainbow.png');
this.load.spritesheet('cat', 'assets/animations/nyan/cat.png', { frameWidth: 97, frameHeight: 59 });
this.load.audio('CatAstroPhi', [
'assets/audio/CatAstroPhi_shmup_normal.ogg',
'assets/audio/CatAstroPhi_shmup_normal.mp3'
]);
}
create ()
{
this.catAstroPhi = this.sound.add('CatAstroPhi');
this.catAstroPhi.play({
seek: 2.550
});
// play() method call above has the same effect as the
// two lines below but it is done in only one command
// and it is a bit more efficient
// catAstroPhi.play();
// catAstroPhi.seek = 2.550;
this.add.image(400, 300, 'bg');
if (this.sound.locked)
{
const text = this.add.bitmapText(400, 300, '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.setup.call(this);
}, this);
}
else
{
this.setup.call(this);
}
}
update ()
{
if (this.cat)
{
this.cat.x = this.cat.width / 2 + (this.catAstroPhi.seek / this.catAstroPhi.duration) * (800 - this.cat.width);
this.rainbowMask.clear();
this.rainbowMask.fillStyle(0xffffff, 1);
this.rainbowMask.fillRect(0, 0, this.cat.x - 15, 600);
if (!this.catAstroPhi.isPlaying && this.cat.anims.isPlaying)
{
this.cat.anims.pause();
}
else if (this.catAstroPhi.isPlaying && !this.cat.anims.isPlaying)
{
this.cat.anims.resume();
}
this.cat.anims.timeScale = this.catAstroPhi.totalRate;
}
}
setup ()
{
const gui = new dat.GUI();
const sm = gui.addFolder('CatAstroPhi Sound');
sm.add(this.catAstroPhi, 'seek', 0, this.catAstroPhi.duration).step(0.01).listen();
sm.add(this.catAstroPhi, 'rate', 0.5, 2).listen();
sm.add(this.catAstroPhi, 'detune', -1200, 1200).step(50).listen();
sm.add(this.catAstroPhi, 'loop').listen();
sm.add(this.catAstroPhi, 'play');
sm.add(this.catAstroPhi, 'pause');
sm.add(this.catAstroPhi, 'resume');
sm.add(this.catAstroPhi, 'stop');
sm.open();
this.rainbowMask = this.make.graphics();
const rainbow = this.add.image(400, 300, 'rainbow');
rainbow.enableFilters().filters.external.addMask(this.rainbowMask);
this.anims.create({
key: 'cat',
frames: this.anims.generateFrameNumbers('cat', { start: 0, end: 5, first: 0 }),
frameRate: 15,
repeat: -1
});
this.cat = this.add.sprite(0, 300, 'cat').setInteractive();
this.cat.play('cat');
this.input.setDraggable(this.cat);
this.input.on('drag', (pointer, cat, dragX, dragY) =>
{
cat.x = Math.min(Math.max(cat.width / 2, dragX), 800 - cat.width / 2);
this.catAstroPhi.seek = (cat.x - cat.width / 2) / (800 - cat.width) * this.catAstroPhi.duration;
});
}
}
/**
* @author Pavle Goloskokovic <pgoloskokovic@gmail.com> (http://prunegames.com)
*/
const config = {
type: Phaser.AUTO,
parent: 'phaser-example',
width: 800,
height: 600,
scene: Example,
pixelArt: true,
audio: {
disableWebAudio: true
}
};
const game = new Phaser.Game(config);
Загрузка ресурсов и запуск звука с позиции
В методе preload() загружаются все необходимые ресурсы: шрифт, фоновое изображение, радуга, спрайтшит кота и аудиофайлы в двух форматах для кроссбраузерной совместимости.
Ключевой момент происходит в create(), когда создаётся объект звука и сразу запускается с определённой позиции. Это делается с помощью параметра seek в объекте конфигурации метода play(). Альтернативный способ — сначала вызвать play(), а затем установить свойство .seek объекта звука.
Также здесь обрабатывается ограничение на автоматическое воспроизведение звука в браузерах. Если звуковая система заблокирована, выводится текст с призывом нажать на экран. После разблокировки (unlocked) вызывается основной метод настройки setup().
this.catAstroPhi = this.sound.add('CatAstroPhi');
this.catAstroPhi.play({
seek: 2.550
});
Связывание позиции звука с анимацией кота
В методе update() происходит магия синхронизации. Положение спрайта кота по оси X вычисляется на основе текущей позиции воспроизведения звука (this.catAstroPhi.seek) и его общей длительности (this.catAstroPhi.duration). Это создаёт эффект, будто кот "едет" по шкале времени трека.
Графика rainbowMask используется в качестве маски для изображения радуги. Маска обновляется каждый кадр, отрисовывая белый прямоугольник, ширина которого зависит от позиции кота, создавая эффект "развёртывания" радуги по мере проигрывания.
Также логика связывает состояние воспроизведения звука (isPlaying) с анимацией кота. Если звук остановлен, анимация приостанавливается, и наоборот. Скорость анимации (timeScale) привязывается к общей скорости воспроизведения звука (totalRate), которая учитывает rate и detune.
this.cat.x = this.cat.width / 2 + (this.catAstroPhi.seek / this.catAstroPhi.duration) * (800 - this.cat.width);
this.cat.anims.timeScale = this.catAstroPhi.totalRate;
Интерактивное управление через GUI и перетаскивание
Метод setup() создаёт интерактивные элементы управления. С помощью библиотеки dat.GUI создаётся панель с ползунками и кнопками для управления объектом звука catAstroPhi. Ползунок seek позволяет вручную перемещаться по временной шкале трека. Также можно управлять скоростью (rate), высотой тона (detune), режимом повтора (loop), воспроизведением, паузой и остановкой.
Создаётся анимация (cat) и спрайт кота, который делается перетаскиваемым. Обработчик события drag связывает положение кота на экране с позицией в аудио. Когда игрок перетаскивает кота, его позиция по X пересчитывается во временную позицию в треке и применяется через свойство .seek. Это обеспечивает двустороннюю связь: перемещение ползунка в GUI или перетаскивание кота изменяет позицию воспроизведения.
this.input.on('drag', (pointer, cat, dragX, dragY) => {
cat.x = Math.min(Math.max(cat.width / 2, dragX), 800 - cat.width / 2);
this.catAstroPhi.seek = (cat.x - cat.width / 2) / (800 - cat.width) * this.catAstroPhi.duration;
});
Важная конфигурация: отключение Web Audio
В конфигурации игры (config) есть критически важный параметр для корректной работы данного примера. В объекте audio установлено свойство disableWebAudio: true. Это заставляет Phaser использовать HTML5 Audio API вместо Web Audio API.
Зачем это нужно? Свойство .seek объекта звука в Phaser ведёт себя по-разному в зависимости от используемого API. При работе с Web Audio API изменение seek может быть менее точным или требовать перезагрузки буфера аудио. HTML5 Audio API предоставляет более прямое и предсказуемое управление позицией воспроизведения, что идеально подходит для данного интерактивного примера с частым seek.
const config = {
// ... другие настройки ...
audio: {
disableWebAudio: true
}
};
Что попробовать дальше
Пример демонстрирует мощь и гибкость управления звуком в Phaser. Вы можете не только воспроизводить аудио, но и точно контролировать позицию проигрывания, синхронизировать её с визуальными элементами и предоставлять интерактивные средства управления игроку. Для экспериментов попробуйте: заменить аудиодорожку и анимацию на свои ресурсы; использовать seek для создания системы пропуска диалогов в кат-сценах; привязать seek к прогресс-бару в музыкальном плеере внутри игры; или исследовать, как rate и detune влияют на геймплей в ритм-играх.
