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

Создание игр — это не только графика и геймплей, но и создание атмосферы. Часто мы хотим, чтобы визуальные эффекты идеально соответствовали звуковому сопровождению. В этой статье мы разберем пример, где анимация скачущих лошадей синхронизирована с музыкой с помощью управления параметрами аудио. Вы научитесь контролировать громкость (`volume`), скорость воспроизведения (`rate`), высоту тона (`detune`) и заглушку (`mute`) как для отдельных звуков, так и для всего звукового менеджера, и применять эти значения для динамического изменения анимации в реальном времени. Это полезно для создания ритмичных игр, музыкальных визуализаторов или просто для добавления дополнительного слоя отзывчивости вашего геймдизайна к звуку.

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

Живой запуск

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

Исходный код


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

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

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

        // 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', function (soundManager)
            {
                text.visible = false;

                this.start.call(this);

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

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

const game = new Phaser.Game(config);

Подготовка ресурсов и создание анимации

Прежде чем управлять звуком, необходимо загрузить ресурсы и создать визуальную составляющую. В примере используется спрайт-лист с кадрами лошади и стерео-дорожки музыкальной композиции.

В методе init() мы заранее подготавливаем массив объектов с ключами для кадров анимации. Это удобно для последующей загрузки и создания анимации.

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

Затем в preload() загружаются шрифт, все кадры анимации и два аудиофайла в форматах OGG и MP3 для кроссбраузерной совместимости.

После загрузки, в create(), создается сама анимация 'horse' из подготовленных кадров.

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

Создаются два спрайта (horseLeft и horseRight), которые будут воспроизводить эту анимацию, и два звуковых объекта (soundLeft и soundRight), которые сразу запускаются в режиме повтора.

Обработка аудио-блокировки браузера

Современные браузеры блокируют воспроизведение аудио до первого взаимодействия пользователя (клика или тапа). Phaser предоставляет удобный механизм для обработки этой ситуации.

Свойство this.sound.locked указывает, заблокирован ли в данный момент звук. Если да, то на экран выводится текст с инструкцией, и мы подписываемся на событие 'unlocked' менеджера звуков. Как только пользователь взаимодействует со страницей, событие срабатывает, текст скрывается, и вызывается основной метод инициализации start().

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', function (soundManager)
    {
        text.visible = false;
        this.start.call(this);
    }, this);
}
else
{
    this.start.call(this);
}

Если звук изначально не заблокирован, метод start() вызывается сразу. Этот подход гарантирует, что игра будет корректно работать на всех платформах.

Связывание параметров звука и анимации в update()

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

Свойство totalRate объекта звука (рассчитываемое из rate и detune) задает timeScale анимации. Это заставляет лошадь скакать быстрее или медленнее в полном соответствии с темпом музыки.

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

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

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

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

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

Таким образом, любое изменение звуковых параметров через GUI мгновенно отражается на анимации.

Создание панели управления с dat.GUI

Для интерактивного управления параметрами в примере используется библиотека dat.GUI. В методе start() создаются папки с контролами для глобального менеджера звуков и для каждого звукового объекта отдельно.

Метод .listen() заставляет элементы GUI автоматически обновлять свое значение при изменении соответствующего свойства в коде (например, из другого контрола). Это создает двустороннюю связь.

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

Аналогичные панели создаются для левого и правого каналов, что позволяет независимо настраивать их. Изменяя rate и detune, вы влияете на totalRate, который, как мы видели, управляет скоростью анимации.

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

Этот пример наглядно демонстрирует мощь и гибкость аудиосистемы Phaser. Вы можете не просто проигрывать звуки, но и тонко управлять их параметрами, а также использовать эти параметры для создания глубоко синхронизированного визуального ряда. Механика работает на двух уровнях: глобальном (this.sound) и локальном (конкретный звуковой объект), что дает полный контроль над звуковым дизайном. **Идеи для экспериментов:** 1. Привяжите totalRate не только к скорости анимации, но и к скорости движения игрового объекта. 2. Используйте detune для создания эффекта "пьяного" или поврежденного персонажа, у которого плывет картинка и искажается звук. 3. На основе комбинации volume и mute реализуйте систему "тихого режима" для скрытия определенных визуальных эффектов, когда звук отключен.