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

В игровых проектах часто требуется не просто проигрывать звуки, а тонко управлять ими: ставить на паузу, возобновлять с нужного момента или запускать конкретные фрагменты аудиодорожки. Phaser предлагает для этого мощный инструмент — аудиомаркеры, работающие в связке с Web Audio API. В этой статье мы разберем практический пример, где одна аудиодорожка содержит несколько звуковых эффектов. Вы научитесь добавлять маркеры, создавать кнопки для их воспроизведения и реализуете универсальный механизм паузы и возобновления для всего аудио. Этот подход экономит ресурсы и упрощает управление звуком в игре.

Версия 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) для кроссбраузерной совместимости.

Ключевой элемент — массив markers. Каждый маркер — это объект с именем (name), временем начала в секундах (start), продолжительностью (duration) и конфигурацией (config). Таким образом, мы размечаем одну длинную аудиодорожку на логические фрагменты — отдельные звуковые эффекты.

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: {} }
];
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 для каждого маркера выполняется два действия: 1. **Добавление маркера в аудиообъект** с помощью метода this.fx.addMarker(marker). После этого к звуковому эффекту можно обращаться по его имени. 2. **Создание кнопки** для воспроизведения этого маркера. Функция makeButton создает интерактивное изображение и подписывает его bitmap-текстом.

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);
}

Обработка кликов и воспроизведение маркеров

Вся интерактивность построена на обработчиках событий ввода Phaser. Наиболее важное событие — gameobjectdown (нажатие на кнопку).

В его обработчике проверяется свойство button.name. Если имя кнопки не 'pause' (т.е. это кнопка маркера), то вызывается this.fx.play(button.name). Метод play, вызванный с именем строкового маркера, воспроизводит именно тот фрагмент аудио, который был ассоциирован с этим именем через addMarker.

this.input.on('gameobjectdown', (pointer, button) =>
{
    if (button.name === 'pause')
    {
        // ... логика паузы
    }
    else
    {
        // Воспроизведение конкретного маркера по его имени
        this.fx.play(button.name);
        this.setButtonFrame(button, 2);
    }
});

Универсальная кнопка паузы и возобновления

Отдельная кнопка pause управляет глобальным состоянием аудиообъекта. Её логика опирается на два свойства объекта this.fx: * isPlaying: имеет значение true, когда звук воспроизводится. * isPaused: имеет значение true, когда звук поставлен на паузу.

В обработчике нажатия мы проверяем эти состояния:
1.  Если звук на паузе (`isPaused === true`) — вызываем `this.fx.resume()` для продолжения воспроизведения.
2.  Если звук играет (`isPlaying === true`) — вызываем `this.fx.pause()` для приостановки.
3.  Если звук не играет и не на паузе — ничего не делаем.

Метод updatePauseResumeButton обновляет текст на кнопке в зависимости от текущего состояния, делая интерфейс понятным для игрока.

if (button.name === 'pause')
{
    if (this.fx.isPaused)
    {
        this.fx.resume();
    }
    else if (this.fx.isPlaying)
    {
        this.fx.pause();
    }
    this.setButtonFrame(button, 2);
}
updatePauseResumeButton ()
{
    if (this.fx.isPaused)
    {
        this.pauseResumeButtonText.text = 'resume';
    }
    else if (this.fx.isPlaying)
    {
        this.pauseResumeButtonText.text = 'pause';
    }
}

Что попробовать дальше

Использование аудиомаркеров в Phaser — это профессиональный и эффективный способ работы со звуком. Он позволяет хранить связанные эффекты в одном файле, уменьшая количество HTTP-запросов и упрощая загрузку. Механизм паузы и возобновления, основанный на свойствах isPlaying и isPaused, интуитивно понятен и надежен. Для экспериментов попробуйте: 1. Динамически создавать маркеры на основе данных из JSON-конфига. 2. Добавить кнопку остановки (this.fx.stop()) и функцию перемотки, изменяя свойство seek у аудиообъекта. 3. Реализовать параллельное воспроизведение нескольких маркеров, создавая дополнительные аудиообъекты из того же звукового ключа.