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

Управление аудио — ключевой элемент игровой атмосферы. В Phaser звук можно не просто воспроизводить, но и тонко настраивать, синхронизируя его с визуальными эффектами. Этот пример демонстрирует, как связать скорость анимации спрайта с параметрами звукового трека, а также как управлять глобальными и локальными настройками аудио через панель dat.GUI. Вы научитесь контролировать громкость, тон, скорость воспроизведения и режим mute, создавая динамичную и отзывчивую игровую среду.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    soundRight;
    soundLeft;
    horseRight;
    horseLeft;
    i = 0;
    horseFrames = [];

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.bitmapFont('atari-classic', 'assets/fonts/bitmap/atari-classic.png', 'assets/fonts/bitmap/atari-classic.xml');

        for (let i = 0; i < 12; i++)
        {
            this.horseFrames.push({
                key: `horse${(`0${i}`).slice(-2)}`,
                frame: '__BASE'
            });
        }

        // Loading horse animation
        for (let i = 0; i < this.horseFrames.length; i++)
        {
            this.load.image(this.horseFrames[i].key, `assets/animations/horse/frame_${(`0${i}`).slice(-2)}_delay-0.05s.png`);
        }

        // Loading music
        this.load.audio('left', [
            'assets/audio/Rossini - William Tell Overture (8 Bits Version)/left.ogg',
            'assets/audio/Rossini - William Tell Overture (8 Bits Version)/left.mp3'
        ]);
        this.load.audio('right', [
            'assets/audio/Rossini - William Tell Overture (8 Bits Version)/right.ogg',
            'assets/audio/Rossini - William Tell Overture (8 Bits Version)/right.mp3'
        ]);
    }

    create ()
    {
        this.anims.create({
            key: 'horse',
            frames: this.horseFrames,
            frameRate: 20,
            repeat: -1
        });

        this.horseLeft = this.add.sprite(200, 300, 'horse09');
        this.horseLeft.setScale(400 / 480);

        this.horseRight = this.add.sprite(600, 300, 'horse10');
        this.horseRight.setScale(400 / 480);

        this.soundLeft = this.sound.add('left');
        this.soundLeft.play({
            loop: true
        });

        this.soundRight = this.sound.add('right');
        this.soundRight.play({
            loop: true
        });

        if (this.sound.locked)
        {
            const text = this.add.bitmapText(400, 50, 'atari-classic', 'Tap to start', 40);
            text.x -= Math.round(text.width / 2);
            text.y -= Math.round(text.height / 2);

            this.sound.once('unlocked', (soundManager) =>
            {
                text.visible = false;

                this.start();
            });
        }
        else
        {
            this.start();
        }
    }

    update ()
    {
        this.horseLeft.anims.timeScale = this.soundLeft.totalRate;
        this.horseRight.anims.timeScale = this.soundRight.totalRate;

        this.horseLeft.setAlpha(this.sound.volume * this.soundLeft.volume);
        this.horseRight.setAlpha(this.sound.volume * this.soundRight.volume);

        this.horseLeft.visible = !this.sound.mute && !this.soundLeft.mute;
        this.horseRight.visible = !this.sound.mute && !this.soundRight.mute;
    }

    start ()
    {
        this.horseLeft.play('horse');
        this.horseRight.play('horse');

        const gui = new dat.GUI();

        const sm = gui.addFolder('Sound Manager');
        sm.add(this.sound, 'mute').listen();
        sm.add(this.sound, 'volume', 0, 1).listen();
        sm.add(this.sound, 'rate', 0.5, 2).listen();
        sm.add(this.sound, 'detune', -1200, 1200).step(50).listen();
        sm.open();

        const sl = gui.addFolder('Left');
        sl.add(this.soundLeft, 'mute').listen();
        sl.add(this.soundLeft, 'volume', 0, 1).listen();
        sl.add(this.soundLeft, 'rate', 0.5, 2).listen();
        sl.add(this.soundLeft, 'detune', -1200, 1200).step(50).listen();
        sl.open();

        const sr = gui.addFolder('Right');
        sr.add(this.soundRight, 'mute').listen();
        sr.add(this.soundRight, 'volume', 0, 1).listen();
        sr.add(this.soundRight, 'rate', 0.5, 2).listen();
        sr.add(this.soundRight, 'detune', -1200, 1200).step(50).listen();
        sr.open();
    }
}

/**
 * @author    Pavle Goloskokovic <pgoloskokovic@gmail.com> (http://prunegames.com)
 *
 * Images by Walter Newton (http://walternewton.tumblr.com/post/58684376490/like-a-train-in-the-night)
 * Music by Classical 8 Bit (http://classical8bit.tumblr.com/) / CC BY
 */

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example,
    pixelArt: true,
    audio: {
        disableWebAudio: true
    }
};

for (; this.i < 12; this.i++)
{
    this.horseFrames.push({
        key: `horse${(`0${this.i}`).slice(-2)}`,
        frame: '__BASE'
    });
}

const game = new Phaser.Game(config);

Загрузка ресурсов: анимация и стереозвук

В методе preload() происходит подготовка всех необходимых ресурсов. Загружается растровый шрифт для текста, 12 кадров анимации лошади (каждый как отдельное изображение) и два аудиофайла, представляющих левый и правый каналы стереозаписи. Обратите внимание на формирование массива horseFrames: для каждого кадра создается объект с ключом и базовым фреймом, что впоследствии используется при создании анимации.

for (let i = 0; i < 12; i++)
{
    this.horseFrames.push({
        key: `horse${(`0${i}`).slice(-2)}`,
        frame: '__BASE'
    });
}

Аудио загружается с поддержкой нескольких форматов (OGG и MP3) для кросс-браузерной совместимости. Это особенно важно, так как в конфигурации отключен Web Audio API (disableWebAudio: true), и используется HTML5 Audio.

this.load.audio('left', [
    'assets/audio/Rossini - William Tell Overture (8 Bits Version)/left.ogg',
    'assets/audio/Rossini - William Tell Overture (8 Bits Version)/left.mp3'
]);

Создание сцены: анимация, звук и обработка блокировки

В create() создается анимация horse из загруженных кадров с частотой 20 кадров в секунд и бесконечным повтором. Добавляются два спрайта лошадей, которым задается начальный масштаб.

this.anims.create({
    key: 'horse',
    frames: this.horseFrames,
    frameRate: 20,
    repeat: -1
});

Затем создаются и сразу начинают воспроизводиться в цикле два звуковых объекта: soundLeft и soundRight.

this.soundLeft = this.sound.add('left');
this.soundLeft.play({
    loop: true
});

Важный момент — обработка блокировки аудио в браузерах. Если звук заблокирован (автозапуск запрещен), выводится текст с предложением коснуться экрана. После разблокировки (событие unlocked) вызывается метод start(), который запускает анимацию и панель управления. Если блокировки нет, start() вызывается сразу.

if (this.sound.locked)
{
    // ... создание текста ...
    this.sound.once('unlocked', (soundManager) =>
    {
        text.visible = false;
        this.start();
    });
}

Динамическая связь: метод update()

Сердце примера — метод update(), который выполняется каждый кадр. Здесь происходит синхронизация анимации со звуком и управление видимостью спрайтов.

Скорость анимации каждой лошади (anims.timeScale) привязывается к итоговой скорости воспроизведения соответствующего звука. Итоговая скорость (totalRate) вычисляется с учетом и глобальной скорости (sound.rate), и локальной (soundLeft.rate или soundRight.rate).

this.horseLeft.anims.timeScale = this.soundLeft.totalRate;
this.horseRight.anims.timeScale = this.soundRight.totalRate;

Прозрачность (alpha) спрайта вычисляется как произведение глобальной громкости (sound.volume) и громкости конкретного звука. Это позволяет визуализировать как общее, так и индивидуальное управление громкостью.

this.horseLeft.setAlpha(this.sound.volume * this.soundLeft.volume);

Видимость спрайта (visible) определяется комбинацией глобального и локального режима mute. Лошадь исчезает, если звук заглушен на любом из уровней.

this.horseLeft.visible = !this.sound.mute && !this.soundLeft.mute;

Панель управления: dat.GUI

Метод start() создает интерактивную панель управления с помощью библиотеки dat.GUI. Панель разделена на три группы: общий менеджер звука (Sound Manager), левый канал (Left) и правый канал (Right).

Для каждого объекта настраиваются контролы для свойств mute, volume, rate и detune. Флаг listen() заставляет GUI автоматически обновлять отображение при изменении значения из кода (например, в update()).

const sm = gui.addFolder('Sound Manager');
sm.add(this.sound, 'mute').listen();
sm.add(this.sound, 'volume', 0, 1).listen();
sm.add(this.sound, 'rate', 0.5, 2).listen();
sm.add(this.sound, 'detune', -1200, 1200).step(50).listen();

detune изменяет высоту тона в центах (100 центов = 1 полутон). Параметр step(50) задает шаг изменения на полусемитон. Это мощный инструмент для создания звуковых эффектов, таких как замедление или ускорение времени в игре.

Конфигурация: использование HTML5 Audio

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

const config = {
    // ... другие параметры ...
    audio: {
        disableWebAudio: true
    }
};

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

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

Пример наглядно показывает, как в Phaser звуковая система глубоко интегрирована в игровой цикл. Вы можете управлять аудиопараметрами на лету и мгновенно отражать эти изменения в графике, создавая целостный опыт. Для экспериментов попробуйте

  1. Связать detune с цветовым фильтром спрайта
  2. Заменить плавное изменение прозрачности на дискретное мигание при определенных значениях громкости
  3. Использовать totalRate для управления скоростью частиц, испускаемых от анимированного объекта