О чем этот пример

Работа со звуком в играх — это не только загрузка и воспроизведение. Часто возникают тонкие моменты, например, повторное воспроизведение трека, который уже играет, что приводит к наложению и искажению. В этой статье мы разберем пример из официального репозитория 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(), чтобы избежать наложения. Для экспериментов попробуйте

  1. Создать пул из нескольких звуковых объектов для одного эффекта, чтобы можно было играть их с перекрытием
  2. Реализовать систему приоритетов, где новый важный звук может прервать играющий
  3. Добавить плавное затухание звука (fade out) перед его повторным запуском