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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.setPath('assets/tests/piano');

        this.load.atlas('piano', 'piano.png', 'piano.json');

        this.load.audio('C3', 'C3.mp3');
        this.load.audio('Db3', 'Db3.mp3');
        this.load.audio('D3', 'D3.mp3');
        this.load.audio('Eb3', 'Eb3.mp3');
        this.load.audio('E3', 'E3.mp3');
        this.load.audio('F3', 'F3.mp3');
        this.load.audio('Gb3', 'Gb3.mp3');
        this.load.audio('G3', 'G3.mp3');
        this.load.audio('Ab3', 'Ab3.mp3');
        this.load.audio('A3', 'A3.mp3');
        this.load.audio('Bb3', 'Bb3.mp3');
        this.load.audio('B3', 'B3.mp3');
    }

    create ()
    {
        if (this.sound.locked)
        {
            const text = this.add.text(10, 10, 'Tap to unlock audio', { font: '16px Courier', fill: '#00ff00' });

            this.sound.once('unlocked', function ()
            {
                text.destroy();
                this.createPiano();
            }, this);
        }
        else
        {
            this.createPiano();
        }
    }

    createPiano ()
    {
        this.input.addPointer(9);

        const x = 100;
        const y = 0;

        this.add.image(x, y, 'piano', 'panel').setOrigin(0);

        const keys = [
            [ 'key1', 'C3' ],
            [ 'key2', 'Db3' ],
            [ 'key3', 'D3' ],
            [ 'key4', 'Eb3' ],
            [ 'key5', 'E3' ],
            [ 'key6', 'F3' ],
            [ 'key7', 'Gb3' ],
            [ 'key8', 'G3' ],
            [ 'key9', 'Ab3' ],
            [ 'key10', 'A3' ],
            [ 'key11', 'Bb3' ],
            [ 'key12', 'B3' ]
        ];

        const black = [ 'key2', 'key4', 'key7', 'key9', 'key11' ];

        for (let i = 0; i < keys.length; i++)
        {
            const key = keys[i][0];
            const note = keys[i][1];

            const singleKey = this.add.image(x, y, 'piano', key);

            singleKey.setName(note);
            singleKey.setOrigin(0);

            if (black.indexOf(key) !== -1)
            {
                singleKey.setDepth(1);
            }

            const frame = singleKey.frame;

            const hitArea = new Phaser.Geom.Rectangle(frame.x, frame.y, frame.width, frame.height);

            singleKey.setInteractive(hitArea, Phaser.Geom.Rectangle.Contains);

            const sound = this.sound.add(note);

            singleKey.on('pointerdown', (sound =>
            {
                sound.play();

            }).bind(this, sound));

            singleKey.on('pointerover', ((sound, pointer) =>
            {
                if (pointer.isDown)
                {
                    sound.play();
                }
            }).bind(this, sound));
        }
    }
}

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

const game = new Phaser.Game(config);

Загрузка ресурсов: звуки и изображения

В методе preload происходит подготовка всех необходимых для сцены ресурсов. Загружается атлас с изображениями клавиш пианино и отдельные аудиофайлы для каждой ноты.

this.load.atlas('piano', 'piano.png', 'piano.json');
this.load.audio('C3', 'C3.mp3');
// ... загрузка остальных звуков (Db3, D3 и т.д.)

Использование atlas позволяет эффективно хранить множество спрайтов в одном изображении. Каждый звук загружается с уникальным ключом, который позже будет использоваться для его создания и воспроизведения.

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

Современные браузеры требуют взаимодействия пользователя перед первым воспроизведением звука. Метод create проверяет состояние звуковой системы.

if (this.sound.locked) {
    const text = this.add.text(10, 10, 'Tap to unlock audio', { font: '16px Courier', fill: '#00ff00' });
    this.sound.once('unlocked', function () {
        text.destroy();
        this.createPiano();
    }, this);
} else {
    this.createPiano();
}

Если звук заблокирован (this.sound.locked), на экране появляется текст с инструкцией. После первого касания (и разблокировки) срабатывает событие unlocked, текст удаляется и вызывается основная функция создания пианино createPiano. Если звук уже доступен, пианино создается сразу.

Подготовка к мультитачу и создание клавиш

Перед созданием клавиш необходимо увеличить количество доступных указателей ввода, чтобы одновременно можно было нажимать несколько клавиш.

this.input.addPointer(9);

Эта строка добавляет 9 дополнительных указателей (вместе с основным всего 10). После этого создается фон (panel) и массив данных о клавишах, где каждый элемент содержит имя кадра из атласа и ключ звука.

const keys = [
    [ 'key1', 'C3' ],
    [ 'key2', 'Db3' ],
    // ... остальные клавиши
];

Настройка интерактивности и реакция на ввод

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

singleKey.setName(note);
if (black.indexOf(key) !== -1) {
    singleKey.setDepth(1);
}

Важный момент — определение точной области для взаимодействия. Хитбокс создается на основе размеров кадра (frame) спрайта, что гарантирует точное совпадение видимой и интерактивной областей.

const hitArea = new Phaser.Geom.Rectangle(frame.x, frame.y, frame.width, frame.height);
singleKey.setInteractive(hitArea, Phaser.Geom.Rectangle.Contains);

Для каждой клавиши заранее создается объект звука this.sound.add(note). Это позволяет избежать задержек при первом нажатии.

Воспроизведение звука по событиям указателя

Клавиша реагирует на два события: pointerdown (касание/клик) и pointerover (перемещение указателя над объектом). Обработчики используют bind для передачи заранее созданного звукового объекта.

singleKey.on('pointerdown', (sound => {
    sound.play();
}).bind(this, sound));

Событие pointerover воспроизводит звук только если перемещение происходит при уже нажатой кнопке мыши или удержании касания. Это имитирует проведение пальцем по клавишам.

singleKey.on('pointerover', ((sound, pointer) => {
    if (pointer.isDown) {
        sound.play();
    }
}).bind(this, sound));

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

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