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

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

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

Живой запуск

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

Исходный код


/**
 * @author    Pavle Goloskokovic <pgoloskokovic@gmail.com> (http://prunegames.com)
 *
 * Prometheus Brings Fire To Mankind - Painting by Heinrich Füger, 1817, Public Domain
 * The Creatures of Prometheus, Op. 43, Overture - Music by Ludwig van Beethoven, 1801, Public Domain
 */
var text;
var first;
var second;
var audioSprite;

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

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        const head  = document.getElementsByTagName('head')[0];
        const link  = document.createElement('link');
        link.rel  = 'stylesheet';
        link.href = 'https://fonts.googleapis.com/css?family=Sorts+Mill+Goudy';
        head.appendChild(link);

        this.load.image('prometheus', 'assets/pics/Prometheus Brings Fire To Mankind.jpg');

        this.load.audio('overture', [
            'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/Overture.ogg',
            'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/Overture.mp3'
        ],{
            instances: 2
        });

        this.load.audioSprite('creatures', 'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/sprites.json', [
            'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/sprites.ogg',
            'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/sprites.mp3'
        ]);
    }

    create ()
    {
        this.sound.pauseOnBlur = false;

        var prometheus = this.add.image(400, 300, 'prometheus');
        prometheus.setScale(600/prometheus.height);

        text = this.add.text(400, 300, 'Loading...', {
            fontFamily: '\'Sorts Mill Goudy\', serif',
            fontSize: 80,
            color: '#fff',
            align: 'center'
        });
        text.setOrigin(0.5);
        text.setShadow(0, 1, "#888", 2);

        first = this.sound.add('overture', { loop: true });
        second = this.sound.add('overture', { loop: true });
        audioSprite = this.sound.addAudioSprite('creatures');

        this.enableInput();
    }

    chain(i)
    {
        return function()
        {
            if (tests[i])
            {
                tests[i].call(this, this.chain(++i));
            }
            else
            {
                text.setText('Complete!');

                this.time.addEvent({
                    delay: 5000,
                    callback: this.enableInput,
                    callbackScope: this
                });
            }
        }.bind(this);
    }

    enableInput()
    {
        text.setText('Click to start');

        this.input.once('pointerdown', function (pointer)
        {
            tests[0].call(this, this.chain(1));
        }, this);
    }
}

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

const game = new Phaser.Game(config);



var tests = [

    function (fn)
    {
        first.once('play', function (sound)
        {
            text.setText('Playing');
            this.time.addEvent({
                delay: 2000,
                callback: fn,
                callbackScope: this
            });
        }, this);

        first.play();
    },

    function (fn)
    {
        first.once('pause', function (sound)
        {
            text.setText('Paused');
            this.time.addEvent({
                delay: 1500,
                callback: fn,
                callbackScope: this
            });
        }, this);

        first.pause();
    },

    function(fn)
    {
        first.once('resume', function (sound)
        {
            text.setText('Resuming');
            this.time.addEvent({
                delay: 2000,
                callback: fn,
                callbackScope: this
            });
        }, this);

        first.resume();
    },

    function(fn)
    {
        first.once('stop', function (sound)
        {
            text.setText('Stopped');
            this.time.addEvent({
                delay: 1500,
                callback: fn,
                callbackScope: this
            });
        }, this);

        first.stop();
    },

    function(fn)
    {
        first.once('play', function (sound)
        {
            text.setText('Play from start');
            this.time.addEvent({
                delay: 2000,
                callback: fn,
                callbackScope: this
            });
        }, this);

        first.play();
    },

    function(fn)
    {
        first.once('rate', function (sound, value)
        {
            text.setText('Speed up rate');
            this.time.addEvent({
                delay: 2000,
                callback: fn,
                callbackScope: this
            });
        }, this);

        first.rate = 1.5;
    },

    function(fn)
    {
        first.once('detune', function (sound, value)
        {
            text.setText('Speed up detune');
            this.time.addEvent({
                delay: 2000,
                callback: fn,
                callbackScope: this
            });
        }, this);

        first.detune = 600;
    },

    function(fn)
    {
        first.once('rate', function (sound, value)
        {
            text.setText('Slow down rate');
            this.time.addEvent({
                delay: 2000,
                callback: fn,
                callbackScope: this
            });
        }, this);

        first.rate = 1;
    },

    function(fn)
    {
        first.once('detune', function (sound, value)
        {
            text.setText('Slow down detune');
            this.time.addEvent({
                delay: 2000,
                callback: fn,
                callbackScope: this
            });
        }, this);

        first.detune = 0;
    },

    function(fn)
    {
        this.tweens.add({

            onStart: function ()
            {
                text.setText('Fade out');
            },

            targets: first,
            volume: 0,

            ease: 'Linear',
            duration: 2000,

            onComplete: fn
        });
    },

    function(fn)
    {
        this.tweens.add({

            onStart: function ()
            {
                text.setText('Fade in');
            },

            targets: first,
            volume: 1,

            ease: 'Linear',
            duration: 2000,

            onComplete: fn
        });
    },

    function(fn)
    {
        first.once('mute', function()
        {
            text.setText('Mute');
            this.time.addEvent({
                delay: 1500,
                callback: fn,
                callbackScope: this
            });
        }, this);

        first.mute = true;
    },

    function(fn)
    {
        first.once('mute', function()
        {
            text.setText('Unmute');
            this.time.addEvent({
                delay: 2000,
                callback: fn,
                callbackScope: this
            });
        }, this);

        first.mute = false;
    },

    function(fn)
    {
        first.once('volume', function()
        {
            text.setText('Half volume');
            this.time.addEvent({
                delay: 2000,
                callback: fn,
                callbackScope: this
            });
        }, this);

        first.volume = 0.5;
    },

    function(fn)
    {
        first.once('volume', function()
        {
            text.setText('Full volume');
            this.time.addEvent({
                delay: 2000,
                callback: fn,
                callbackScope: this
            });
        }, this);

        first.volume = 1;
    },

    function(fn)
    {
        first.once('seek', function()
        {
            text.setText('Seek to start');
            this.time.addEvent({
                delay: 2000,
                callback: fn,
                callbackScope: this
            });
        }, this);

        first.seek = 0;
    },

    function(fn)
    {
        second.once('play', function()
        {
            text.setText('Play 2nd');
            this.time.addEvent({
                delay: 2000,
                callback: fn,
                callbackScope: this
            });
        }, this);

        second.play();
    },

    function(fn)
    {
        this.sound.once('mute', function (soundManager, value)
        {
            text.setText('Mute global');
            this.time.addEvent({
                delay: 1500,
                callback: fn,
                callbackScope: this
            });
        }, this);

        this.sound.mute = true;
    },

    function(fn)
    {
        this.sound.once('mute', function (soundManager, value)
        {
            text.setText('Unmute global');
            this.time.addEvent({
                delay: 2000,
                callback: fn,
                callbackScope: this
            });
        }, this);

        this.sound.mute = false;
    },

    function(fn)
    {
        this.sound.once('volume', function (soundManager, value)
        {
            text.setText('Half volume global');
            this.time.addEvent({
                delay: 2000,
                callback: fn,
                callbackScope: this
            });
        }, this);

        this.sound.volume = 0.5;
    },

    function(fn)
    {
        this.tweens.add({

            onStart: function ()
            {
                text.setText('Fade out global');
            },

            targets: this.sound,
            volume: 0,

            ease: 'Linear',
            duration: 2000,

            onComplete: fn
        });
    },

    function(fn)
    {
        this.tweens.add({

            onStart: function ()
            {
                text.setText('Fade in global');
            },

            targets: this.sound,
            volume: 1,

            ease: 'Linear',
            duration: 2000,

            onComplete: fn
        });
    },

    function(fn)
    {
        this.sound.once('pauseall', function (soundManager)
        {
            text.setText('Pause all');
            this.time.addEvent({
                delay: 1500,
                callback: fn,
                callbackScope: this
            });
        }, this);

        this.sound.pauseAll();
    },

    function(fn)
    {
        this.sound.once('resumeall', function (soundManager)
        {
            text.setText('Resume all');
            this.time.addEvent({
                delay: 2000,
                callback: fn,
                callbackScope: this
            });
        }, this);

        this.sound.resumeAll();
    },

    function(fn)
    {
        this.sound.once('stopall', function (soundManager)
        {
            text.setText('Stop all');
            this.time.addEvent({
                delay: 1500,
                callback: fn,
                callbackScope: this
            });
        }, this);

        this.sound.stopAll();
    },

    function(fn)
    {
        audioSprite.once('play', function (sound)
        {
            text.setText('Play sprite');
            this.time.addEvent({
                delay: 1500,
                callback: fn,
                callbackScope: this
            });
        }, this);

        audioSprite.play('07');
    },

    function(fn)
    {
        audioSprite.once('pause', function (sound)
        {
            text.setText('Pause sprite');
            this.time.addEvent({
                delay: 1000,
                callback: fn,
                callbackScope: this
            });
        }, this);

        audioSprite.pause();
    },

    function(fn)
    {
        audioSprite.once('resume', function (sound)
        {
            text.setText('Resume sprite');
            this.time.addEvent({
                delay: 1500,
                callback: fn,
                callbackScope: this
            });
        }, this);

        audioSprite.resume();
    },

    function(fn)
    {
        audioSprite.once('play', function (sound)
        {
            text.setText('Multiple sprites');
            this.time.addEvent({
                delay: 10000,
                callback: fn,
                callbackScope: this
            });
        }, this);

        var sounds = ['01', '02', '03', '03', '05'];

        for (var i=0; i<sounds.length; i++)
        {
            this.time.addEvent({
                delay: i * 2000,
                callback: audioSprite.play.bind(audioSprite, sounds[i]),
                callbackScope: audioSprite
            });
        }
    },

    function(fn)
    {
        audioSprite.once('play', function (sound)
        {
            text.setText('Loop sprite');
            this.time.addEvent({
                delay: 4000,
                callback: fn,
                callbackScope: this
            });
        }, this);

        audioSprite.play('06', {
            loop: true
        });
    },

    function(fn)
    {
        this.tweens.add({

            onStart: function ()
            {
                text.setText('Fade out sprite');
            },

            targets: audioSprite,
            volume: 0,

            ease: 'Linear',
            duration: 4000,

            onComplete: function()
            {
                audioSprite.volume = 1;
                audioSprite.stop();

                fn();
            }
        });
    }
];

Настройка и загрузка аудио

Перед использованием звуков их необходимо загрузить. В методе preload() сцены мы видим три разных способа загрузки аудиоресурсов.

Одиночный аудиофайл загружается с помощью load.audio(). Важно указать несколько форматов (например, .ogg и .mp3) для кроссбраузерной совместимости. Параметр instances определяет, сколько независимых экземпляров этого звука можно создать одновременно — это полезно для наложения одного и того же звука.

Аудиоспрайт — это один аудиофайл, содержащий множество коротких звуков (спрайтов), и JSON-файл с метаданными (временными отметками для каждого спрайта). Он загружается методом load.audioSprite(). Это эффективно для управления множеством звуковых эффектов.

this.load.audio('overture', [
    'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/Overture.ogg',
    'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/Overture.mp3'
],{
    instances: 2
});

this.load.audioSprite('creatures', 'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/sprites.json', [
    'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/sprites.ogg',
    'assets/audio/Ludwig van Beethoven - The Creatures of Prometheus, Op. 43/sprites.mp3'
]);

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

const config = {
    // ... другие настройки
    audio: {
        disableWebAudio: true
    }
};

Создание звуковых объектов и управление ими

В методе create() загруженные аудиоресурсы превращаются в игровые объекты, с которыми можно взаимодействовать.

Из одного аудиоключа 'overture' создаются два независимых экземпляра звука (first и second). Это позволяет, например, проигрывать одну и ту же музыку с наложением или с разными параметрами. Аудиоспрайт создаётся одним вызовом addAudioSprite() — он даёт доступ ко всем звуковым фрагментам, описанным в JSON.

first = this.sound.add('overture', { loop: true });
second = this.sound.add('overture', { loop: true });
audioSprite = this.sound.addAudioSprite('creatures');

У каждого экземпляра Sound есть набор свойств и методов для контроля воспроизведения. В примере последовательно демонстрируются: - Базовые операции: play(), pause(), resume(), stop(). - Контроль параметров: изменение rate (скорости), detune (стройки), volume (громкости), mute (отключения звука). - Перемотка с помощью свойства seek.

Важный нюанс: многие свойства, такие как rate или volume, являются сеттерами, которые генерируют одноимённые события при изменении. Именно на эти события ('rate', 'volume') и подписываются обработчики в примере, чтобы синхронизировать цепочку тестов.

Работа со звуковыми событиями

Phaser звуковая система построена на событиях. Это позволяет легко реагировать на изменения состояния: начало воспроизведения, паузу, изменение громкости и т.д.

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

first.once('pause', function (sound) {
    text.setText('Paused');
    this.time.addEvent({
        delay: 1500,
        callback: fn, // fn — это следующий тест в цепочке
        callbackScope: this
    });
}, this);

first.pause(); // Вызов метода генерирует событие 'pause'

Таким образом, каждый тест инициирует действие (например, pause()), ждёт соответствующего события и по его получению, через задержку delay, запускает следующий тест. Это создаёт чёткую, управляемую последовательность демонстрации.

Глобальное управление звуком

Помимо управления отдельными звуками, Phaser предоставляет доступ к глобальному менеджеру звуков this.sound. Через него можно управлять всеми звуками в игре одновременно.

В примере показаны ключевые методы:
- `this.sound.pauseAll()` и `this.sound.resumeAll()` для приостановки и возобновления всего звука.
- `this.sound.stopAll()` для полной остановки.
- Свойства `this.sound.mute` и `this.sound.volume` для глобального отключения звука и управления общей громкостью. Изменения этих свойств также генерируют события (`'mute'`, `'volume'`).
// Глобальное отключение звука
this.sound.mute = true;

// Глобальное уменьшение громкости
this.sound.volume = 0.5;

// Остановка всех звуков
this.sound.stopAll();

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

Использование аудиоспрайтов и твинов

Аудиоспрайты идеально подходят для коротких звуковых эффектов (выстрелов, шагов, кликов). В примере audioSprite проигрывает конкретные спрайты по их ключам ('01', '07' и т.д.).

audioSprite.play('07'); // Воспроизвести спрайт с ключом '07'

Можно задавать параметры воспроизведения, например, зацикливание:

audioSprite.play('06', {
    loop: true
});

Для плавного изменения свойств (например, для fade-in/fade-out эффектов) пример использует систему твинов Phaser. Твин анимирует значение свойства volume у звукового объекта от текущего до целевого за заданное время.

this.tweens.add({
    targets: first, // Цель — звуковой объект
    volume: 0,      // Конечное значение громкости
    duration: 2000, // Длительность анимации в миллисекундах
    ease: 'Linear',
    onComplete: fn  // Колбэк по завершении твина
});

Этот подход гораздо эффективнее и плавнее, чем ручное изменение volume в цикле обновления игры.

Архитектура цепочки тестов

Весь пример построен вокруг массивов tests и chain(). Это умное архитектурное решение для последовательного выполнения демонстраций.

Массив tests содержит функции. Каждая функция принимает колбэк fn (следующий тест) и выполняет три действия: 1. Подписывается на событие звукового объекта с помощью once(). 2. В обработчике события устанавливает задержку и планирует вызов колбэка fn. 3. Вызывает метод звукового объекта, который инициирует событие.

Функция chain() создает замыкание, которое по очереди вызывает функции из массива tests. Когда тесты заканчиваются, она выводит "Complete!" и через 5 секунд перезапускает демонстрацию.

function(fn) {
    first.once('play', function (sound) {
        text.setText('Playing');
        this.time.addEvent({
            delay: 2000,
            callback: fn, // Запуск следующего теста
            callbackScope: this
        });
    }, this);
    first.play(); // Запуск события
}

Такая архитектура делает код модульным и легко расширяемым — новый тест это просто новая функция в массиве.

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

Пример охватывает весь спектр работы со звуком в Phaser 3: от загрузки и базового воспроизведения до тонкого контроля параметров, глобального управления и использования аудиоспрайтов. Ключевые моменты для запоминания: используйте события для синхронизации, твины для плавных переходов, а аудиоспрайты — для эффективной работы со звуковыми эффектами. Для экспериментов попробуйте: создать свой аудиоспрайт из набора эффектов; реализовать систему микширования, где громкость музыки плавно снижается при появлении диалогов; привязать изменение rate или detune звука к игровой механике (например, замедление времени).