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

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

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

Живой запуск

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

Исходный код


/**
 * @author    Pavle Goloskokovic <pgoloskokovic@gmail.com> (http://prunegames.com)
 */
class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    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.audioSprite('sfx', 'assets/audio/SoundEffects/fx_mixdown.json', [
            'assets/audio/SoundEffects/fx_mixdown.ogg',
            'assets/audio/SoundEffects/fx_mixdown.mp3'
        ]);
    }

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

        const spritemap = this.cache.json.get('sfx').spritemap;

        let i = 0;
        for (let spriteName in spritemap)
        {
            if (!spritemap.hasOwnProperty(spriteName))
            {
                continue;
            }
            this.makeButton(spriteName, 680, 115 + i*40);
            i++;
        }

        this.input.on('gameobjectover', function (pointer, button)
        {
            this.setButtonFrame(button, 0);
        }, this);

        this.input.on('gameobjectout', function (pointer, button)
        {
            this.setButtonFrame(button, 1);
        }, this);

        this.input.on('gameobjectdown', function (pointer, button)
        {
            this.sound.playAudioSprite('sfx', button.name);
            this.setButtonFrame(button, 2);
        }, this);

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

    }

    makeButton(name, x, y)
    {
        const button = this.add.image(x, y, 'button', 1)
            .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;
    }

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

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

Загрузка Audio Sprite и ресурсов

В методе preload() загружаются все необходимые ресурсы для сцены. Ключевой момент — использование this.load.audioSprite для загрузки аудиоспрайта. Этот метод принимает уникальный ключ, путь к JSON-файлу с метаданными и массив путей к самим аудиофайлам в разных форматах для совместимости с браузерами. JSON-файл описывает временные отрезки (спрайты) внутри единого аудиофайла, каждый со своим именем и временными метками начала и окончания.

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.audioSprite('sfx', 'assets/audio/SoundEffects/fx_mixdown.json', [
    'assets/audio/SoundEffects/fx_mixdown.ogg',
    'assets/audio/SoundEffects/fx_mixdown.mp3'
]);

Создание интерфейса и обработка событий

В методе create() создаётся интерфейс для тестирования звуков. Сначала из кэша извлекается объект spritemap, который содержит описание всех звуковых спрайтов, загруженных по ключу 'sfx'. Затем для каждого имени звука в этом объекте создаётся кнопка с помощью метода makeButton. Важно, что имя звука (spriteName) присваивается свойству button.name — это позволит позже идентифицировать, какой звук нужно проиграть.

const spritemap = this.cache.json.get('sfx').spritemap;
let i = 0;
for (let spriteName in spritemap)
{
    if (!spritemap.hasOwnProperty(spriteName))
    {
        continue;
    }
    this.makeButton(spriteName, 680, 115 + i*40);
    i++;
}

Далее настраиваются обработчики событий ввода для интерактивных кнопок. События gameobjectover, gameobjectout, gameobjectdown и gameobjectup меняют кадр кнопки для визуальной обратной связи. Ключевое событие — gameobjectdown: при клике на кнопку вызывается this.sound.playAudioSprite('sfx', button.name), который воспроизводит конкретный звуковой спрайт из общего файла по его имени.

this.input.on('gameobjectdown', function (pointer, button)
{
    this.sound.playAudioSprite('sfx', button.name);
    this.setButtonFrame(button, 2);
}, this);

Вспомогательные методы: makeButton и setButtonFrame

Метод makeButton создаёт визуальный элемент кнопки. Он создаёт изображение из спрайтшита, делает его интерактивным, задаёт масштаб и добавляет текстовую метку с именем звука с помощью bitmap-шрифта. Имя звука сохраняется в свойстве name объекта кнопки.

makeButton(name, x, y)
{
    const button = this.add.image(x, y, 'button', 1)
        .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;
}

Метод setButtonFrame изменяет кадр (frame) кнопки, что используется для визуальной обратной связи при наведении и клике. Он напрямую обращается к текстуре и устанавливает нужный кадр.

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

Конфигурация игры и важная настройка audio

Конфигурация игры включает стандартные параметры, такие как тип рендерера, размеры и основную сцену. Особое внимание стоит обратить на свойство audio. В этом примере установлен флаг disableWebAudio: true. Это означает, что Phaser будет использовать HTML5 Audio API вместо Web Audio API. Это может быть необходимо для совместимости в некоторых специфических средах или для работы с аудиоспрайтами определённым образом, но в большинстве современных проектов рекомендуется использовать Web Audio API (значение по умолчанию) из-за его больших возможностей и лучшего контроля.

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

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

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

  1. заменить HTML5 Audio на Web Audio API, убрав настройку disableWebAudio, и сравните поведение
  2. создать свой аудиоспрайт с помощью инструментов вроде Audiosprite или подобных онлайн-генераторов
  3. добавить к кнопкам слайдеры для управления громкостью или скоростью воспроизведения (rate) конкретного звукового спрайта