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

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

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    markers = [
        { name: 'alien death', start: 1, duration: 1.0, config: {} },
        { name: 'boss hit', start: 3, duration: 0.5, config: {} },
        { name: 'escape', start: 4, duration: 3.2, config: {} },
        { name: 'meow', start: 8, duration: 0.5, config: {} },
        { name: 'numkey', start: 9, duration: 0.1, config: {} },
        { name: 'ping', start: 10, duration: 1.0, config: {} },
        { name: 'death', start: 12, duration: 4.2, config: {} },
        { name: 'shot', start: 17, duration: 1.0, config: {} },
        { name: 'squit', start: 19, duration: 0.3, config: {} }
    ];

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('title', 'assets/pics/catastrophi.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/fx_mixdown.ogg',
            'assets/audio/SoundEffects/fx_mixdown.mp3'
        ], {
            instances: 4
        });
    }

    create ()
    {
        this.add.image(400, 300, 'title');

        for (let i = 0; i < this.markers.length; i++)
        {
            this.makeButton.call(this, this.markers[i].name, i);
        }

        this.input.on('gameobjectover', (pointer, button) =>
        {
            this.setButtonFrame(button, 0);
        });
        this.input.on('gameobjectout', (pointer, button) =>
        {
            this.setButtonFrame(button, 1);
        });
        this.input.on('gameobjectdown', function (pointer, button)
        {
            const index = button.getData('index');

            this.sound.play('sfx', this.markers[index]);

            this.setButtonFrame(button, 2);

        }, this);
        this.input.on('gameobjectup', (pointer, button) =>
        {
            this.setButtonFrame(button, 0);
        });
    }

    makeButton (name, index)
    {
        const button = this.add.image(680, 115 + index * 40, 'button', 1).setInteractive();
        button.setData('index', index);
        button.setScale(2, 1.5);

        const text = this.add.bitmapText(button.x - 40, button.y - 8, 'nokia', name, 16);
        text.x += (button.width - text.width) / 2;
    }

    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,
    audio: {
        disableWebAudio: true
    }
};

const game = new Phaser.Game(config);

Подготовка аудиофайла и загрузка ресурсов

Ключевая идея — использовать один аудиофайл, содержащий все звуковые эффекты, и набор маркеров, которые описывают временные отрезки для каждого эффекта.

В методе preload мы загружаем аудиофайл с ключом 'sfx'. Обратите внимание на опцию instances: 4. Она указывает, сколько независимых экземпляров этого звука можно воспроизводить одновременно. Это полезно для быстрых повторяющихся звуков, например, выстрелов.

this.load.audio('sfx', [
    'assets/audio/SoundEffects/fx_mixdown.ogg',
    'assets/audio/SoundEffects/fx_mixdown.mp3'
], {
    instances: 4
});

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

markers = [
    { name: 'alien death', start: 1, duration: 1.0, config: {} },
    // ... другие маркеры
];

Создание интерактивных кнопок для каждого маркера

Для удобного тестирования звуков в примере создаются кнопки — по одной для каждого маркера. Это происходит в методе create.

Цикл проходит по массиву маркеров и для каждого вызывает метод makeButton, передавая имя эффекта и его индекс в массиве.

for (let i = 0; i < this.markers.length; i++)
{
    this.makeButton.call(this, this.markers[i].name, i);
}

Метод makeButton создает спрайт кнопки из загруженного спрайтшита и делает его интерактивным с помощью setInteractive(). Индекс маркера сохраняется в данных объекта кнопки через setData(). Это позволит позже понять, какую кнопку нажали. Рядом с кнопкой создается текст с именем эффекта, используя битмап-шрифт.

makeButton (name, index)
{
    const button = this.add.image(680, 115 + index * 40, 'button', 1).setInteractive();
    button.setData('index', index);
    // ... создание текста
}

Обработка событий ввода и воспроизведение звука

Интерактивность кнопок обеспечивается обработчиками событий, которые навешиваются на входную систему Phaser (this.input.on). В примере обрабатываются наведение, уход курсора, нажатие и отпускание кнопки мыши.

Самое важное происходит в обработчике gameobjectdown (когда кнопку нажали). Из данных кнопки извлекается сохраненный индекс. Затем вызывается this.sound.play(). Первый аргумент — ключ загруженного звука ('sfx'), второй — объект маркера из массива, найденный по этому индексу.

this.input.on('gameobjectdown', function (pointer, button)
{
    const index = button.getData('index');
    this.sound.play('sfx', this.markers[index]);
    this.setButtonFrame(button, 2);
}, this);

Phaser, получив объект маркера, проиграет не весь файл, а только его фрагмент, начиная с start и продолжительностью duration. Благодаря предзагрузке с несколькими instances, эффекты могут накладываться друг на друга, если нажимать кнопки быстро.

Вспомогательный метод setButtonFrame меняет кадр спрайтшита кнопки, обеспечивая визуальный feedback при наведении и нажатии.

setButtonFrame (button, frame)
{
    button.frame = button.scene.textures.getFrame('button', frame);
}

Особенности конфигурации: HTML5 Audio вместо Web Audio

Обратите внимание на конфигурацию игры. В ней явно отключен Web Audio API и используется стандартный HTML5 Audio.

const config = {
    // ... другие настройки
    audio: {
        disableWebAudio: true
    }
};

Это может быть необходимо для совместимости в некоторых браузерах или мобильных устройствах. Важно знать, что поведение и возможности API (например, точность маркеров или частота одновременного воспроизведения) могут различаться в зависимости от выбранного аудио-бэкенда.

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

Использование маркеров — это мощный и экономичный способ управления звуковыми эффектами в Phaser. Он позволяет сократить количество HTTP-запросов и упростить организацию аудио-контента. Для экспериментов попробуйте создать свой набор маркеров в длинном аудиофайле, изменить количество instances при загрузке, чтобы увидеть разницу в наложении звуков, или настроить параметры в объекте config каждого маркера (например, громкость или петлю).