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

Обработка и воспроизведение звуков — ключевая часть геймдева. Часто нам нужно проигрывать не весь аудиофайл, а его небольшие, точно размеченные фрагменты, например, звук выстрела или прыжка. 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
};

const game = new Phaser.Game(config);

Что такое аудио-маркеры и зачем они нужны

Аудио-маркер — это метка внутри звукового файла, определяющая его фрагмент по времени начала (start) и длительности (duration). Вместо загрузки десятков маленьких файлов для каждого звука вы можете использовать один скомпилированный файл со всеми эффектами. Это сокращает количество HTTP-запросов и упрощает управление аудио-ресурсами.

В примере массив markers описывает девять различных звуковых эффектов в одном файле fx_mixdown. Например, маркер 'shot' начинается на 17-й секунде и длится 1 секунду.

Структура данных и загрузка ресурсов

Класс сцены Example начинается с определения массива маркеров. Каждый маркер — это объект с именем и временными параметрами.

В методе preload() загружаются все необходимые ресурсы: фоновое изображение, спрайтшит для кнопок, bitmap-шрифт и сам аудиофайл. Обратите внимание, что аудио загружается в форматах Ogg и MP3 для кросс-браузерной совместимости.

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

Метод create() выводит фоновое изображение и в цикле создаёт кнопки для каждого маркера, вызывая this.makeButton().

Создание интерактивных кнопок

Функция makeButton() создаёт визуальный элемент управления для каждого звукового маркера.

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

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

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

Основная логика взаимодействия реализована через обработчики событий ввода gameobjectover, gameobjectout, gameobjectdown и gameobjectup. Они меняют кадр кнопки (состояние: наведение, отпущена, нажата), создавая визуальную обратную связь.

Ключевой момент происходит в событии gameobjectdown. Из данных кнопки извлекается сохранённый индекс, и с помощью this.sound.play() проигрывается звук 'sfx'. Вторым аргументом в этот метод передаётся объект конфигурации — в нашем случае это объект маркера из массива. Phaser сам вырежет и воспроизведёт нужный отрезок.

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

Смена состояний кнопки через кадры спрайтшита

Функция setButtonFrame() отвечает за визуальное изменение состояния кнопки. Она изменяет свойство frame игрового объекта, подставляя нужный кадр из текстуры 'button'. Это классический подход для работы с кнопками в Phaser.

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

Первый кадр (0) используется для состояния наведения, второй (1) — для обычного состояния, третий (2) — для нажатого состояния. Спрайтшит загружен с параметром frameWidth: 80, что означает, что каждый кадр имеет ширину 80 пикселей.

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

Аудио-маркеры в Phaser — это мощный и эффективный способ управления звуковыми эффектами. Используя один файл, вы улучшаете производительность загрузки и сохраняете код чистым. Для экспериментов попробуйте: добавить воспроизведение маркера не по клику, а в ответ на игровое событие (столкновение, сбор предмета); реализовать остановку звука или изменение его громкости для конкретного маркера; создать систему случайного выбора одного из нескольких маркеров для вариативности звуков (например, разных шагов).