О чем этот пример
Работа со звуком в играх — это не только загрузка и воспроизведение. Часто возникают тонкие моменты, например, повторное воспроизведение трека, который уже играет, что приводит к наложению и искажению. В этой статье мы разберем пример из официального репозитория Phaser, который демонстрирует потенциальную проблему с управлением аудио и способы её решения. Мы детально изучим код, объясним свойства объекта Sound и покажем, как правильно контролировать воспроизведение, чтобы звуки в вашей игре всегда были четкими и управляемыми.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.audio('explosion', 'assets/audio/SoundEffects/explosion.mp3');
this.load.image('wizball', 'assets/sprites/wizball.png');
}
create ()
{
const music = this.sound.add('explosion', {volume:1});
this.add.image(400, 300, 'wizball').setScale(4).setInteractive().addListener('pointerdown',() => {
const progress = music.seek / music.duration;
if (progress >= 0.9)
{
console.log(music.hasEnded, progress);
}
music.play();
});
this.progressText = this.add.text(50, 50, '1');
this.events.addListener('update', () => {
const progress = music.seek / music.duration;
// var rando = Math.random() * (1 - 0.9) + 0.9;
// // console.log(rando)
// if (progress > rando) {
// music.play();
// }
this.progressText.setText(progress);
});
//this.sound.pauseOnBlur = true;
}
}
const config = {
type: Phaser.AUTO,
parent: 'phaser-example',
width: 800,
height: 600,
pixelArt: true,
scene: Example
};
const game = new Phaser.Game(config);
Разбор примера: что происходит?
В данном примере загружается звук взрыва и спрайт. При клике на спрайт воспроизводится звук explosion. Однако в коде есть особенность: звук запускается по клику, но не проверяется, не играется ли он уже в данный момент.
Основные объекты:
- this.sound.add('explosion', {volume:1}) — создает и возвращает экземпляр объекта Sound.
- music.seek — текущая позиция воспроизведения звука в секундах.
- music.duration — общая длительность звукового файла.
Ключевая проблема скрыта в закомментированном коде и условной проверке if (progress >= 0.9). Разработчик пытался отследить, когда трек почти закончился, но логика не завершена.
Свойства объекта Sound и их использование
Чтобы понять, как избежать наложения звуков, нужно знать состояние аудио. Объект Sound в Phaser предоставляет несколько полезных свойств и методов.
const music = this.sound.add('explosion', {volume: 1});
После создания объекта, мы можем проверять его состояние:
- music.isPlaying — логическое значение, true, если звук в данный момент воспроизводится.
- music.seek — текущая позиция (в секундах) от начала звукового файла.
- music.duration — полная длительность звукового файла.
- music.hasEnded — логическое значение, которое становится true, когда воспроизведение звука завершено (полезно для событийных сценариев).
Рассчет прогресса воспроизведения:
const progress = music.seek / music.duration;
Эта переменная изменяется от 0 (начало) до 1 (конец) и используется для отображения в текстовом поле.
Правильная проверка перед воспроизведением
Самый простой способ предотвратить наложение — проверять, не играет ли звук уже сейчас. Для этого используем свойство isPlaying.
Вместо прямого вызова music.play() в обработчике клика, нужно добавить условие:
this.add.image(400, 300, 'wizball').setScale(4).setInteractive().addListener('pointerdown',() => {
if (!music.isPlaying) {
music.play();
}
});
Такой подход гарантирует, что новый экземпляр звука начнет играть, только если предыдущее воспроизведение завершилось. Это базовое и самое надежное решение для большинства звуковых эффектов в игре.
Управление звуком через прогресс и события
Иногда нужно более тонкое управление. Например, разрешить повторное воспроизведение, только если трек почти доиграл до конца (прогресс > 90%). Исходный пример содержит зачатки этой логики.
Вот как можно её реализовать, используя как isPlaying, так и расчет прогресса:
this.add.image(400, 300, 'wizball').setScale(4).setInteractive().addListener('pointerdown',() => {
const progress = music.seek / music.duration;
// Воспроизводим, если звук НЕ играет ИЛИ он почти закончился
if (!music.isPlaying || progress >= 0.9) {
music.play();
}
});
В методе update (который вызывается каждый кадр) мы можем обновлять текстовое поле, чтобы визуализировать прогресс:
this.events.addListener('update', () => {
const progress = music.seek / music.duration;
this.progressText.setText(progress.toFixed(2)); // Округляем для удобства
});
Обратите внимание: свойство seek для звуков, которые не играются, может быть неопределенным (undefined), поэтому в реальном проекте нужна дополнительная проверка.
Продвинутый контроль: пауза при сворачивании окна
Phaser предоставляет удобную настройку для автоматической паузы всех звуков, когда игровое окно теряет фокус (например, когда пользователь переключается на другую вкладку браузера).
this.sound.pauseOnBlur = true;
Если раскомментировать эту строку в методе create, то все звуки, управляемые через this.sound, будут автоматически ставиться на паузу при потере фокуса и возобновляться при возвращении. Это важная деталь пользовательского опыта, которую легко реализовать одной строчкой.
Что попробовать дальше
Управление звуком в Phaser — это просто, если знать ключевые свойства объекта Sound, такие как isPlaying, seek и duration. Основной вывод: всегда проверяйте состояние звука перед вызовом play(), чтобы избежать наложения. Для экспериментов попробуйте
- Создать пул из нескольких звуковых объектов для одного эффекта, чтобы можно было играть их с перекрытием
- Реализовать систему приоритетов, где новый важный звук может прервать играющий
- Добавить плавное затухание звука (fade out) перед его повторным запуском
