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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    constructor ()
    {
        super();

        this.analyser;
        this.dataArray;
        this.bufferLength;
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.video('robot', 'assets/video/robot-dance.webm');
        this.load.audio('tune', 'assets/audio/aquakitty-kittyrock.m4a');
        this.load.glsl('gridback', 'assets/shaders/gridback.frag');
    }

    create ()
    {
        const text = this.add.text(10, 10, 'Click to start', { font: '16px Courier', fill: '#00ff00' });

        let analyser = this.sound.context.createAnalyser();

        this.sound.masterVolumeNode.connect(analyser);

        analyser.connect(this.sound.context.destination);

        analyser.smoothingTimeConstant = 1;

        this.bufferLength = analyser.frequencyBinCount;

        this.dataArray = new Uint8Array(this.bufferLength);

        this.analyser = analyser;

        this.input.once('pointerdown', () => {

            text.destroy();

            this.sound.play('tune', { loop: true });

            // this.add.shader('GridBack', 512, 300, 1024, 600);
            this.add.shader({
                name: 'GridBack',
                fragmentKey: 'gridback',
                initialUniforms: {
                    resolution: [ 1024, 600 ]
                },
                setupUniforms: (setUniform, drawingContext) => {
                    setUniform('time', this.game.loop.getDuration());
                }
            }, 512, 300, 1024, 600);

            this.graphics = this.add.graphics();

            this.add.video(512, 300, 'robot').play(true);

        });
    }

    update ()
    {
        if (!this.graphics)
        {
            return;
        }

        this.analyser.getByteTimeDomainData(this.dataArray);

        this.graphics.clear();
        this.graphics.lineStyle(2, 0x00ff00);

        this.graphics.beginPath();

        var sliceWidth = 1024 / this.bufferLength;
        var x = 0;

        for (var i = 0; i < this.bufferLength; i++)
        {
            var v = this.dataArray[i] / 128;
            var y = v * 300;

            if (i === 0)
            {
                this.graphics.moveTo(x, y);
            }
            else
            {
                this.graphics.lineTo(x, y);
            }

            x += sliceWidth;
        }

        this.graphics.lineTo(1024, 300);

        this.graphics.stroke();
    }

}

const config = {
    type: Phaser.AUTO,
    width: 1024,
    height: 600,
    backgroundColor: '#000000',
    parent: 'phaser-example',
    scene: Example
};

let game = new Phaser.Game(config);

Загрузка ресурсов и настройка анализатора

В методе preload мы загружаем необходимые ресурсы: видеофайл, аудиотрек и шейдер. Ключевой момент происходит в create, где настраивается анализ звука.

Мы получаем доступ к аудиоконтексту Phaser (this.sound.context) и создаем AnalyserNode. Этот узел позволяет получать данные о звуковом сигнале в реальном времени. Мы подключаем его к мастер-узлу громкости звуковой системы Phaser, чтобы анализировать итоговый микшер, а затем подключаем обратно к выходу контекста, чтобы звук не прерывался.

let analyser = this.sound.context.createAnalyser();
this.sound.masterVolumeNode.connect(analyser);
analyser.connect(this.sound.context.destination);

Параметр smoothingTimeConstant сглаживает данные между кадрами. Значение `1` дает максимальное сглаживание, делая визуализацию плавной. Мы также подготавливаем массивы для хранения данных анализатора.

analyser.smoothingTimeConstant = 1;
this.bufferLength = analyser.frequencyBinCount;
this.dataArray = new Uint8Array(this.bufferLength);
this.analyser = analyser;

Запуск сцены по клику и создание фона

Сцена стартует по клику пользователя. После клика уничтожается текст-подсказка, запускается зацикленное аудио и создается визуальное оформление.

Сначала мы создаем фоновый шейдер. В примере показан альтернативный синтаксис добавления шейдера через объект конфигурации. В initialUniforms передается статичное разрешение, а в setupUniforms — динамическое время работы игры, которое будет обновлять анимацию шейдера каждый кадр.

this.add.shader({
    name: 'GridBack',
    fragmentKey: 'gridback',
    initialUniforms: {
        resolution: [ 1024, 600 ]
    },
    setupUniforms: (setUniform, drawingContext) => {
        setUniform('time', this.game.loop.getDuration());
    }
}, 512, 300, 1024, 600);

Затем создается пустой объект Graphics для отрисовки волновой формы и добавляется видео, которое сразу начинает проигрываться.

this.graphics = this.add.graphics();
this.add.video(512, 300, 'robot').play(true);

Визуализация звука в реальном времени

Сердце примера — метод update. Здесь каждый кадр мы получаем новые данные о звуке и перерисовываем графику.

Метод getByteTimeDomainData анализатора заполняет наш массив dataArray значениями амплитуды волны в текущий момент времени. Каждое значение — это число от 0 до 255.

this.analyser.getByteTimeDomainData(this.dataArray);

Перед каждой отрисовкой графический объект очищается, и задается стиль линии.

this.graphics.clear();
this.graphics.lineStyle(2, 0x00ff00);

Затем строится путь. Мы проходим по всем данным массива, рассчитываем координату `Y` для каждой точки (нормализуя значение к диапазону от 0 до 600) и последовательно соединяем их линиями. В конце линия доводится до центра экрана.

var sliceWidth = 1024 / this.bufferLength;
var x = 0;
for (var i = 0; i < this.bufferLength; i++) {
    var v = this.dataArray[i] / 128; // Нормализация к ~0.0-2.0
    var y = v * 300; // Масштабирование под высоту сцены
    // ... построение пути
}
this.graphics.lineTo(1024, 300);
this.graphics.stroke();

Именно этот код создает знаменитую «осциллограмму», которая танцует под музыку.

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

Вы создали динамическую аудиовизуальную композицию, используя встроенные возможности Phaser для работы со звуком, графикой и шейдерами. Этот паттерн — отличная основа для множества экспериментов. Попробуйте визуализировать не временную область (getByteTimeDomainData), а частотный спектр (getByteFrequencyData), чтобы получить классический эквалайзер. Измените цвет и толщину линии в зависимости от громкости. Замените простую линию на частицы (this.add.particles), которые будут разлетаться от волны. Используйте полученные данные амплитуды для управления uniform-переменными в шейдере (например, для искажения фона) или для масштабирования видео-спрайта.