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

Звуковой чип SID (Sound Interface Device) из легендарного компьютера Commodore 64 — это не просто ретро, а целая культура. Его характерное звучание до сих пор используется в инди-играх для создания атмосферы. В этой статье мы разберем, как интегрировать настоящую SID-музыку в вашу игру на Phaser, используя готовый плагин. Вы научитесь загружать бинарные файлы .sid, переключать треки и сабтреки, а также отображать метаданные композиции прямо в игре.

Версия 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.image('title1', 'assets/tests/c64/thrust.png');
        this.load.image('title2', 'assets/tests/c64/cybernoid.png');
        this.load.image('title3', 'assets/tests/c64/robocop.png');
        this.load.image('title4', 'assets/tests/c64/cybernoid2.png');
        this.load.image('title5', 'assets/tests/c64/warhawk.png');
        this.load.image('title6', 'assets/tests/c64/stormlord.png');
        this.load.image('title7', 'assets/tests/c64/zoids.png');

        this.load.binary('tune1', 'assets/audio/sid/thrust.sid');
        this.load.binary('tune2', 'assets/audio/sid/cybernoid.sid');
        this.load.binary('tune3', 'assets/audio/sid/robocop.sid');
        this.load.binary('tune4', 'assets/audio/sid/cybernoid2.sid');
        this.load.binary('tune5', 'assets/audio/sid/warhawk.sid');
        this.load.binary('tune6', 'assets/audio/sid/stormlord.sid');
        this.load.binary('tune7', 'assets/audio/sid/zoids.sid');

        this.load.plugin('SIDPlayerPlugin', 'assets/audio/sid/SIDPlayerPluginES5.js', true);
        this.load.script('jsSID', 'assets/audio/sid/jsSID.js');
    }

    create ()
    {
        this.currenTune = 1;

        let tune = 1;

        this.title = this.add.image(400, 350, `title${tune}`);

        this.add.text(400, 570, 'Click to change Tune. Left / Right cursor changes Sub-Tune', { font: '16px Courier', fill: '#ffffff' }).setShadow(1, 1).setOrigin(0.5, 0);

        const text = this.add.text(10, 10, 'SID Player', { font: '16px Courier', fill: '#ffffff' }).setShadow(1, 1);

        const SIDplayer = this.plugins.get('SIDPlayerPlugin');

        let sidData = this.cache.binary.get(`tune${tune}`);

        SIDplayer.loadLocal(sidData);

        SIDplayer.setmodel(6581);

        let i = 0;
        let max = SIDplayer.getsubtunes();

        this.updateText(tune, text, SIDplayer, i);

        this.input.keyboard.on('keyup-LEFT', () =>
        {
            if (i > 0)
            {
                i--;

                SIDplayer.loadLocal(sidData, i);

                this.updateText(tune, text, SIDplayer, i);
            }
        });

        this.input.keyboard.on('keyup-RIGHT', () =>
        {
            if (i < max)
            {
                i++;

                SIDplayer.loadLocal(sidData, i);

                this.updateText(tune, text, SIDplayer, i);
            }
        });

        this.input.on('pointerdown', () =>
        {
            if (tune < 7)
            {
                tune++;
            }
            else
            {
                tune = 1;
            }

            sidData = this.cache.binary.get(`tune${tune}`);

            SIDplayer.loadLocal(sidData);

            i = 0;
            max = SIDplayer.getsubtunes();

            this.updateText(tune, text, SIDplayer, i);

        });
    }

    updateText (tune, text, SIDplayer, i)
    {
        const title = SIDplayer.gettitle().replace(/\0/g, '');
        const author = SIDplayer.getauthor().replace(/\0/g, '');
        const info = SIDplayer.getinfo().replace(/\0/g, '');

        text.setText([
            'Title: ' + title,
            'Author: ' + author,
            'Info: ' + info,
            'Current Sub-Tune: ' + i,
            'Total Sub-Tunes: ' + SIDplayer.getsubtunes(),
            'Pref. Model: ' + SIDplayer.getprefmodel(),
            'Playtime: ' + SIDplayer.getplaytime(),
            'Playback Model: ' + SIDplayer.getmodel()
        ]);

        if (this.currenTune !== tune)
        {
            this.currenTune = tune;

            this.title.setTexture(`title${tune}`);
        }
    }
}

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

const game = new Phaser.Game(config);

Загрузка ресурсов: картинки, музыка и плагин

В методе preload() происходит подготовка всех необходимых ресурсов. Для каждого музыкального трека загружается его обложка (изображение) и сам бинарный файл .sid. Ключевой момент — загрузка внешнего плагина SIDPlayerPlugin и скрипта jsSID, которые и обеспечивают декодирование и воспроизведение SID-формата.

this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('title1', 'assets/tests/c64/thrust.png');
this.load.binary('tune1', 'assets/audio/sid/thrust.sid');
this.load.plugin('SIDPlayerPlugin', 'assets/audio/sid/SIDPlayerPluginES5.js', true);
this.load.script('jsSID', 'assets/audio/sid/jsSID.js');

Инициализация и запуск первого трека

В create() происходит начальная настройка. Сначала из кэша извлекаются бинарные данные первого трека с помощью this.cache.binary.get. Затем через this.plugins.get получаем экземпляр загруженного плагина SIDPlayerPlugin. Метод loadLocal() плагина загружает данные трека, а setmodel() явно устанавливает модель эмулируемого чипа SID (в данном случае 6581).

const SIDplayer = this.plugins.get('SIDPlayerPlugin');
let sidData = this.cache.binary.get(`tune${tune}`);
SIDplayer.loadLocal(sidData);
SIDplayer.setmodel(6581);

Управление воспроизведением: треки и сабтреки

В примере реализовано два типа управления. Клик мыши (или тап) переключает между семью основными мелодиями. При этом обновляются данные в кэше и происходит перезагрузка трека в плагин. Клавиши LEFT и RIGHT на клавиатуре переключают сабтреки (sub-tunes) внутри текущей композиции. Это типично для SID-файлов, где одна композиция может содержать несколько вариаций (интро, основной трек, аутро).

this.input.on('pointerdown', () => {
    if (tune < 7) { tune++; } else { tune = 1; }
    sidData = this.cache.binary.get(`tune${tune}`);
    SIDplayer.loadLocal(sidData);
});

this.input.keyboard.on('keyup-LEFT', () => {
    if (i > 0) { i--; SIDplayer.loadLocal(sidData, i); }
});

Отображение метаданных композиции

Плагин SIDPlayer предоставляет методы для извлечения метаинформации, которая хранится в заголовке файла .sid. Метод updateText() форматирует эти данные и выводит на экран. Важный нюанс: строки из плагина могут содержать нулевые символы \0, которые необходимо обрезать с помощью .replace(/\0/g, '') для корректного отображения. Также здесь обновляется текстура изображения-обложки.

const title = SIDplayer.gettitle().replace(/\0/g, '');
const author = SIDplayer.getauthor().replace(/\0/g, '');
const info = SIDplayer.getinfo().replace(/\0/g, '');
text.setText([
    'Title: ' + title,
    'Author: ' + author,
    'Info: ' + info,
    'Current Sub-Tune: ' + i
]);
if (this.currenTune !== tune) {
    this.title.setTexture(`title${tune}`);
}

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

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