О чем этот пример
В играх часто требуется запустить фоновую музыку или звуковой эффект не с начала, а с определенного момента. Например, при перезапуске уровня или для синхронизации звука с анимацией. 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: {
noAudio: true
}
};
const game = new Phaser.Game(config);
Загрузка и старт с указанной позиции
В методе preload загружаются все необходимые ресурсы: шрифт, фоновое изображение, радуга, спрайтшит для анимации кота и аудиофайл. Обратите внимание, что для аудио указаны два формата (OGG и MP3) для лучшей кросс-браузерной поддержки.
Ключевой момент происходит в create. Сначала создается объект звука с помощью this.sound.add('CatAstroPhi'). Затем он воспроизводится методом play(), но с конфигурационным объектом, где указано свойство seek.
this.catAstroPhi = this.sound.add('CatAstroPhi');
this.catAstroPhi.play({
seek: 2.550
});
Этот код запускает трек не с нулевой отметки, а с позиции 2.55 секунды. Как отмечено в комментарии, аналогичного результата можно добиться, вызвав play() без параметров, а затем установив свойство this.catAstroPhi.seek = 2.550. Однако первый способ — короче и эффективнее.
Обработка аудиоблокировки браузера
Современные браузеры блокируют воспроизведение аудио до первого взаимодействия пользователя со страницей. Phaser предоставляет удобный способ обработки этого сценария через свойство this.sound.locked.
if (this.sound.locked)
{
const text = this.add.bitmapText(400, 300, 'atari-classic', 'Tap to start', 40);
text.setOrigin(0.5);
this.sound.once('unlocked', this.setup, this);
}
else
{
this.setup.call(this);
}
Если звук заблокирован, на экране появляется текст с просьбой нажать на экран. Как только пользователь совершит действие, сработает событие unlocked, текст скроется и вызовется основной метод инициализации setup. Если блокировки нет, setup вызывается сразу.
Создание UI для управления звуком
В методе setup создается панель управления с помощью библиотеки 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');
Особенно важно здесь контролируемое свойство seek. Слайдер позволяет перематывать аудио в любой момент его длительности (duration). Метод .listen() заставляет UI автоматически обновляться при изменении значения из кода.
Визуализация и синхронизация
Пример не просто играет звук, а создает визуальную метафору. Положение анимированного кота на экране напрямую привязано к текущей позиции (seek) в аудиотреке.
if (this.cat)
{
this.cat.x = this.cat.width / 2 + (this.catAstroPhi.seek / this.catAstroPhi.duration) * (800 - this.cat.width);
}
В каждом кадре (update) вычисляется позиция кота на основе прогресса воспроизведения. Кроме того, создается маска для радуги, которая растет вслед за котом, создавая эффект "разворачивания".
this.rainbowMask.clear();
this.rainbowMask.fillStyle(0xffffff, 1);
this.rainbowMask.fillRect(0, 0, this.cat.x - 15, 600);
Анимация кота также синхронизирована со звуком: она приостанавливается, когда звук останавливается, и ее скорость (timeScale) привязывается к общей скорости воспроизведения (totalRate), которая учитывает и rate, и detune.
Интерактивное перетаскивание для перемотки
Помимо UI-панели, реализован интерактивный способ перемотки — перетаскивание кота мышью.
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;
});
При событии drag вычисляется новая позиция `xдля спрайта кота (с учетом границ экрана). Затем на основе этой позиции пересчитывается и устанавливается новое значение дляthis.catAstroPhi.seek. Это демонстрирует двустороннюю связь: UI (положение кота) меняет состояние звука, и наоборот — вupdate` звук управляет положением кота.
Что попробовать дальше
Свойство seek в Phaser — мощный инструмент для точного контроля над воспроизведением аудио. Оно открывает возможности для создания продвинутых аудиоплееров внутри игр, синхронизации кат-сцен и реализации нелинейного аудио. Для экспериментов попробуйте
- Связать
seekс прогресс-баром уровня - Создать систему "звуковых закладок" для быстрого перехода к ключевым моментам трека
- Реализовать запись и воспроизведение игровых реплеев с синхронным звуком, сохраняя массив временных меток
seek
