О чем этот пример
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.
