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

Phaser — это не только спрайты и анимации. Его мощная система загрузки позволяет работать с бинарными данными, открывая доступ к парсингу специфичных форматов файлов прямо в браузере. Это полезно для создания музыкальных проигрывателей, редакторов уровней, утилит для анализа сохранений игр или любого инструмента, которому нужно читать «сырые» данные файлов. В этой статье на примере загрузки и анализа трека в формате MOD (популярный формат времен Amiga и трекерной музыки) мы покажем, как загружать бинарные файлы, извлекать из них текстовую информацию и отображать её на игровом экране. Вы научитесь основам работы с `Uint8Array` и сможете адаптировать этот подход для своих задач.

Версия 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.binary({
            key: 'mod',
            url: 'assets/audio/protracker/global_trash_3_v2.mod',
            dataType: Uint8Array
        });
    }

    create ()
    {
        const buffer = this.cache.binary.get('mod');

        //   getString scans the binary file between the two values given, 
        //   returning the characters it finds there as a string

        const signature = this.getString(buffer, 1080, 1084);

        const text = this.add.text(32, 32, `Signature: ${signature}`, { fill: '#ffffff' });
        text.setShadow(2, 2, 'rgba(0,0,0,0.5)', 0);

        const title = this.getString(buffer, 0, 20);
        const text2 = this.add.text(32, 64, `Title: ${title}`, { fill: '#ffffff' });
        text2.setShadow(2, 2, 'rgba(0,0,0,0.5)', 0);

        //  Get the sample data
        const sampleText = [];

        for (let i = 0; i < 31; i++)
        {
            const st = 20 + i * 30;
            sampleText.push(this.getString(buffer, st, st + 22));
        }

        const text3 = this.add.text(400, 32, sampleText, { fill: '#ffffff' });
        text3.setShadow(2, 2, 'rgba(0,0,0,0.5)', 0);
    }

    getString (buffer, start, end)
    {
        let output = '';

        for (let i = start; i < end; i++)
        {
            output += String.fromCharCode(buffer[i]);
        }

        return output;
    }
}

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

const game = new Phaser.Game(config);

Настройка загрузчика и загрузка файла

Всё начинается в методе preload(). Первым делом мы указываем базовый URL для загрузки ресурсов с помощью this.load.setBaseURL(). Это удобно, если все ваши ассеты лежат в одной папке на сервере.

Затем происходит самое важное — загрузка бинарного файла. Для этого используется метод this.load.binary(), который принимает объект конфигурации. Ключевые параметры: - key: уникальный строковый идентификатор, по которому мы получим данные позже. - url: путь к файлу относительно базового URL. - dataType: тип JavaScript-объекта, в который будут преобразованы загруженные бинарные данные. В нашем примере это Uint8Array — типизированный массив для работы с байтами.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.binary({
        key: 'mod',
        url: 'assets/audio/protracker/global_trash_3_v2.mod',
        dataType: Uint8Array
    });
}

Извлечение данных из кэша

После успешной загрузки файл попадает в специальный кэш Phaser. В методе create() мы извлекаем оттуда наши бинарные данные.

Для доступа к бинарному кэшу используется this.cache.binary. Метод get() возвращает ранее загруженный Uint8Array по ключу, который мы указали при загрузке (в нашем случае — 'mod').

create ()
{
    const buffer = this.cache.binary.get('mod');
    // ... дальнейшая работа с buffer (Uint8Array)
}

Теперь переменная buffer содержит весь загруженный файл как массив чисел от 0 до 255, где каждое число — это один байт данных. С этой структурой мы и будем работать.

Парсинг заголовка: сигнатура и название

Формат MOD имеет определённую структуру. Зная смещения (оффсеты) в файле, можно извлечь нужную информацию. В примере это делается с помощью вспомогательного метода getString(), который конвертирует последовательность байтов в строку.

Сначала извлекается сигнатура файла — специальная метка, идентифицирующая формат. В MOD она находится между 1080-м и 1084-м байтами. Затем читается название трека, которое расположено в самом начале файла (байты с 0 по 20).

//   getString сканирует бинарный файл между двумя заданными значениями,
//   возвращая найденные символы в виде строки
const signature = this.getString(buffer, 1080, 1084);
const title = this.getString(buffer, 0, 20);

Полученные строки отображаются на экране с помощью this.add.text(). Обратите внимание на использование setShadow() для улучшения читаемости текста.

Вспомогательный метод getString

Этот метод — сердце нашего простого парсера. Он вручную проходит по переданному Uint8Array в заданном диапазоне индексов (от start до end).

Для каждого байта (значения от 0 до 255) метод String.fromCharCode() преобразует числовой код символа в сам символ. Все символы конкатенируются в итоговую строку.

getString (buffer, start, end)
{
    let output = '';

    for (let i = start; i < end; i++)
    {
        output += String.fromCharCode(buffer[i]);
    }

    return output;
}

Именно так мы и читаем текстовые поля, «зашитые» в бинарный файл. Этот подход универсален и может быть использован для многих форматов.

Извлечение данных о сэмплах

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

В примере показано, как циклически извлечь названия 31 сэмпла. Для каждого сэмпла известно его смещение от начала файла и длина названия (22 символа). Цикл рассчитывает начальную позицию для каждого сэмпла и использует уже знакомый getString() для чтения.

const sampleText = [];

for (let i = 0; i < 31; i++)
{
    const st = 20 + i * 30; // Рассчитываем смещение для i-го сэмпла
    sampleText.push(this.getString(buffer, st, st + 22));
}

const text3 = this.add.text(400, 32, sampleText, { fill: '#ffffff' });

Результат — массив строк, который передаётся в this.add.text(). Phaser автоматически отрисовывает элементы массива, разделяя их переносами строк.

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

Вы освоили базовый, но мощный паттерн работы с бинарными данными в Phaser: загрузка через this.load.binary(), извлечение из кэша и ручной парсинг с использованием Uint8Array. Этот подход открывает двери для множества экспериментов: попробуйте загрузить и проанализировать другие форматы (например, WAV-заголовки или простые форматы уровней .map), создать визуализатор структуры файла или даже простой hex-редактор прямо в игре. Поэкспериментируйте с другими dataType (например, ArrayBuffer) и методами DataView для чтения чисел разных размеров и endianness.