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

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

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

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

Класс Example расширяет Phaser.Scene. В методе init() подготавливается массив кадров для анимации лошади. Каждый кадр описывается объектом с ключом и ссылкой на базовый фрейм. Это стандартный подход для создания анимации из набора отдельных изображений.

Метод preload() загружает необходимые ресурсы: растровый шрифт, 12 отдельных PNG-изображений для анимации и два аудиофайла (левый и правый каналы) в форматах OGG и MP3 для кросс-браузерной совместимости.

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

    // Загрузка анимации лошади
    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`);
    }

    // Загрузка музыки
    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() создается анимация 'horse' из подготовленных кадров. Два спрайта (horseLeft и horseRight) отображаются на сцене с небольшим масштабированием.

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

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

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)

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

Свойство anims.timeScale спрайта устанавливается равным totalRate соответствующего звукового объекта. totalRate — это рассчитанная итоговая скорость воспроизведения, учитывающая как глобальную настройку rate менеджера звуков, так и индивидуальную rate самого звука, а также значение detune. Это заставляет анимацию лошади ускоряться или замедляться вместе с музыкой.

Прозрачность (alpha) спрайта зависит от произведения глобальной громкости (this.sound.volume) и громкости конкретного звука (soundLeft.volume или soundRight.volume). Так достигается плавное затемнение при уменьшении громкости.

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

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

Интерактивная панель управления с dat.GUI

Метод start() запускает анимацию лошадей и создает интерактивную панель управления с помощью библиотеки dat.GUI. Это позволяет в реальном времени изменять параметры и видеть их влияние.

Создаются три папки: 1. **Sound Manager**: управляет глобальными настройками — mute, volume, rate, detune. 2. **Left** и **Right**: управляют индивидуальными настройками для каждого звукового канала — mute, volume, rate, detune.

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

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

Конфигурация игры и инициализация

Финальная часть кода определяет конфигурационный объект для Phaser.Game и создает экземпляр игры. Ключевые настройки: - type: Phaser.AUTO — автоматический выбор рендерера (WebGL или Canvas). - pixelArt: true — включает линейную фильтрацию текстур, что критично для пиксель-арта, чтобы сохранить четкие края. - scene: Example — указывает класс основной сцены.

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example,
    pixelArt: true
};
const game = new Phaser.Game(config);

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

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

  1. Привязать detune не к скорости, а к оттенку цвета спрайта
  2. Создать ритм-игру, где игрок должен нажимать клавиши в такт, а визуальная обратная связь будет основана на volume и rate
  3. Управлять параметрами звука не через GUI, а вводом с клавиатуры или геймпада для более игрового ощущения