О чем этот пример
В игровых проектах звук часто требует тонкого контроля. Например, нужно проигрывать короткие эффекты по клику или ставить на паузу длинную фоновую композицию. Phaser предоставляет мощный API для работы с аудио, включая поддержку аудиоспрайтов и маркеров. В этой статье мы разберем пример, который демонстрирует, как загружать аудиофайл с несколькими звуковыми эффектами, размечать его на отдельные маркеры и управлять воспроизведением с возможностью паузы и возобновления. Этот подход экономит ресурсы, сокращая количество HTTP-запросов, и упрощает организацию звуков в игре.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
pauseResumeButtonText;
pauseResumeButton;
fx;
markers = [
{ name: 'charm', start: 0, duration: 2.7, config: {} },
{ name: 'curse', start: 4, duration: 2.9, config: {} },
{ name: 'fireball', start: 8, duration: 5.2, config: {} },
{ name: 'spell', start: 14, duration: 4.7, config: {} },
{ name: 'soundscape', start: 20, duration: 18.8, config: {} }
];
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('bg', 'assets/pics/cougar-dragonsun.png');
this.load.spritesheet('button', 'assets/ui/flixel-button.png', { frameWidth: 80, frameHeight: 20 });
this.load.bitmapFont('nokia', 'assets/fonts/bitmap/nokia16black.png', 'assets/fonts/bitmap/nokia16black.xml');
this.load.audio('sfx', [
'assets/audio/SoundEffects/magical_horror_audiosprite.ogg',
'assets/audio/SoundEffects/magical_horror_audiosprite.mp3'
]);
}
create ()
{
const bg = this.add.image(400, 300, 'bg');
bg.setScale(800 / bg.width, 600 / bg.height);
this.fx = this.sound.add('sfx');
for (let i = 0; i < this.markers.length; i++)
{
const marker = this.markers[i];
this.fx.addMarker(marker);
this.makeButton.call(this, marker.name, 680, 115 + i * 40);
}
this.makePauseResumeButton.call(this);
this.input.on('gameobjectover', (pointer, button) =>
{
this.setButtonFrame(button, 0);
});
this.input.on('gameobjectout', (pointer, button) =>
{
this.setButtonFrame(button, 1);
});
this.input.on('gameobjectdown', (pointer, button) =>
{
if (button.name === 'pause')
{
if (this.fx.isPaused)
{
this.fx.resume();
}
else if (this.fx.isPlaying)
{
this.fx.pause();
}
else
{
this.setButtonFrame(button, 0);
return;
}
this.setButtonFrame(button, 2);
}
else
{
this.fx.play(button.name);
this.setButtonFrame(button, 2);
}
});
this.input.on('gameobjectup', (pointer, button) =>
{
this.setButtonFrame(button, 0);
});
}
updatePauseResumeButton ()
{
if (this.fx.isPaused)
{
this.pauseResumeButtonText.text = 'resume';
}
else if (this.fx.isPlaying)
{
this.pauseResumeButtonText.text = 'pause';
}
else
{
this.pauseResumeButtonText.text = 'stopped';
}
this.pauseResumeButtonText.x = 640 + (this.pauseResumeButton.width - this.pauseResumeButtonText.width) / 2;
}
makeButton (name, x, y)
{
const button = this.add.image(x, y, 'button', 0).setInteractive();
button.name = name;
button.setScale(2, 1.5);
const text = this.add.bitmapText(x - 40, y - 8, 'nokia', name, 16);
text.x += (button.width - text.width) / 2;
}
makePauseResumeButton ()
{
this.pauseResumeButton = this.add.image(680, 395, 'button', 1).setInteractive();
this.pauseResumeButton.name = 'pause';
this.pauseResumeButton.setScale(2, 1.5);
this.pauseResumeButtonText = this.add.bitmapText(640, 387, 'nokia', '', 16);
}
setButtonFrame (button, frame)
{
button.frame = button.scene.textures.getFrame('button', frame);
}
}
/**
* @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
};
const game = new Phaser.Game(config);
Концепция аудиомаркеров и загрузка ресурсов
Вместо загрузки множества маленьких файлов для каждого звукового эффекта, можно использовать один аудиофайл (аудиоспрайт), содержащий все эффекты подряд. Маркеры определяют временные отрезки внутри этого файла, соответствующие каждому эффекту.
В методе preload загружаются все необходимые ресурсы: фон, спрайт кнопки, bitmap-шрифт и аудиофайл. Обратите внимание, что аудио загружается в двух форматах (OGG и MP3) для кроссбраузерной совместимости.
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('bg', 'assets/pics/cougar-dragonsun.png');
this.load.spritesheet('button', 'assets/ui/flixel-button.png', { frameWidth: 80, frameHeight: 20 });
this.load.bitmapFont('nokia', 'assets/fonts/bitmap/nokia16black.png', 'assets/fonts/bitmap/nokia16black.xml');
this.load.audio('sfx', [
'assets/audio/SoundEffects/magical_horror_audiosprite.ogg',
'assets/audio/SoundEffects/magical_horror_audiosprite.mp3'
]);
}
Создание звукового объекта и добавление маркеров
В методе create инициализируется основной звуковой объект this.fx с помощью this.sound.add('sfx'). Затем массив markers обходится в цикле, и для каждого описания маркера вызывается метод addMarker. Этот метод связывает имя маркера (например, 'fireball') с конкретным временным интервалом в аудиофайле.
create ()
{
// ... загрузка фона ...
this.fx = this.sound.add('sfx');
for (let i = 0; i < this.markers.length; i++)
{
const marker = this.markers[i];
this.fx.addMarker(marker);
this.makeButton.call(this, marker.name, 680, 115 + i * 40);
}
// ...
}
После добавления маркера для него создается кнопка вызова. Таким образом, каждый маркер становится отдельной воспроизводимой единицей.
Воспроизведение маркеров и обработка кликов
Обработка кликов по кнопкам настроена через слушатели событий gameobjectdown. Если имя кнопки (button.name) не равно 'pause', то оно соответствует имени маркера. Для воспроизведения конкретного маркера используется метод play, куда передается это имя.
this.input.on('gameobjectdown', (pointer, button) =>
{
if (button.name === 'pause')
{
// Логика паузы/возобновления
}
else
{
this.fx.play(button.name);
this.setButtonFrame(button, 2);
}
});
При вызове this.fx.play('fireball') Phaser возьмет из аудиоспрайта и воспроизведет только тот отрезок, который был определен маркером с именем 'fireball'.
Механизм паузы и возобновления
В примере реализована одна универсальная кнопка для паузы и возобновления воспроизведения. Ее логика основана на проверке свойств звукового объекта isPaused и isPlaying.
if (button.name === 'pause')
{
if (this.fx.isPaused)
{
this.fx.resume();
}
else if (this.fx.isPlaying)
{
this.fx.pause();
}
// ... обновление кадра кнопки ...
}
Важно понимать разницу между остановкой и паузой. Метод pause приостанавливает воспроизведение в текущей позиции, а resume продолжает с того же места. Метод stop полностью прекратил бы воспроизведение и сбросил позицию на начало.
Динамическое обновление интерфейса
Текст на кнопке паузы должен меняться в зависимости от состояния звука. За это отвечает метод updatePauseResumeButton. Он проверяет флаги isPaused и isPlaying и соответствующим образом меняет текст.
updatePauseResumeButton ()
{
if (this.fx.isPaused)
{
this.pauseResumeButtonText.text = 'resume';
}
else if (this.fx.isPlaying)
{
this.pauseResumeButtonText.text = 'pause';
}
else
{
this.pauseResumeButtonText.text = 'stopped';
}
// ... выравнивание текста ...
}
В исходном примере этот метод вызывается в другом месте (например, в update), чтобы интерфейс всегда был актуален. Это хорошая практика для отражения состояния геймплея в UI.
Что попробовать дальше
Использование аудиомаркеров в Phaser — это эффективный способ управления звуковыми эффектами, который сочетает удобство организации и производительность. Механизмы pause и resume дают точный контроль над воспроизведением длинных треков или последовательностей звуков. Для экспериментов попробуйте: добавить маркер для фоновой музыки и переключать ее с эффектами; реализовать плавное затухание звука при паузе (fadeOut); или создать систему очереди воспроизведения маркеров для сложных аудиосцен.
