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

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

Версия 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'
        ]);
    }

    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: {
        noAudio: true
    }
};

const game = new Phaser.Game(config);

Подготовка данных: массив аудио-маркеров

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

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

Этот массив является центральным справочником. По этим данным Phaser будет точно знать, какую часть общего аудиофайла sfx нужно воспроизвести при выборе того или иного эффекта. Имена маркеров мы позже используем для подписи кнопок.

Загрузка ресурсов: один файл на все эффекты

В методе preload() загружаются все необходимые ресурсы. Ключевой момент — загрузка аудио. Вместо множества файлов мы загружаем один файл sfx в двух форматах (OGG и MP3) для кросс-браузерной совместимости. Именно этот файл содержит все звуковые эффекты, размещенные в нем последовательно.

this.load.audio('sfx', [
    'assets/audio/SoundEffects/fx_mixdown.ogg',
    'assets/audio/SoundEffects/fx_mixdown.mp3'
]);

Также загружаются изображение для фона, спрайтшит для кнопок (три кадра для состояний: over, out, down) и растровый шрифт для текста. Обратите внимание на конфигурацию pixelArt: true в основном конфиге игры — она включает сглаживание текстур для пиксель-арт графики.

Создание интерфейса: динамические кнопки

В методе create() создается пользовательский интерфейс. Для каждого маркера из массива вызывается метод makeButton(), который создает интерактивную кнопку.

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

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

const button = this.add.image(680, 115 + index * 40, 'button', 1).setInteractive();
button.setData('index', index);

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

Сердце примера — обработка событий ввода. Мы настраиваем слушатели для событий gameobjectover, gameobjectout, gameobjectdown и gameobjectup, чтобы кнопки визуально реагировали на наведение и клик. Метод setButtonFrame() меняет кадр спрайтшита кнопки, создавая эффект нажатия.

Самое важное происходит в обработчике gameobjectdown:

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

Здесь мы извлекаем из кнопки сохраненный индекс, берем из массива markers объект маркера с этим индексом и передаем его вторым параметром в метод this.sound.play(). Phaser воспроизводит аудио sfx, начиная со времени start и в течение duration, указанных в маркере. Передача всего объекта маркера (this.markers[index]) — это компактная замена явного указания параметров { start: 1, duration: 1.0 }.

Важная деталь: конфигурация "noAudio"

В конфигурационном объекте игры есть необычная настройка audio. Это важный момент для тестирования или для случаев, когда аудио не требуется.

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

Параметр noAudio: true указывает Phaser создать заглушку для аудиосистемы. Все вызовы звуковых методов (включая this.sound.play()) будут работать без ошибок, но реального звука не будет. Это полезно для быстрого прототипирования или для платформ с ограничениями. Если вам нужен звук, просто удалите или закомментируйте этот блок конфигурации.

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

Аудио-маркеры в Phaser — это мощный и экономичный инструмент для работы со звуковыми эффектами. Они позволяют организовать библиотеку звуков в одном файле, упрощая загрузку и управление. На основе этого примера можно экспериментировать: попробуйте добавить кнопку остановки всех звуков, реализовать случайное воспроизведение маркеров из группы или создать систему предзагрузки (this.sound.addMarker), чтобы определять маркеры динамически, а не жестко в коде. Это отличная база для создания собственных инструментов звукового дизайна прямо внутри игры.