О чем этот пример
Часто в играх нужны нестандартные способы отображения информации: пиксельный текст, эффекты старого терминала или элементы стиля ретро. В этом примере мы используем ASCII-шрифт из файла .flf для отрисовки текста 'PHASER 3' с помощью спрайтов шариков. Этот подход демонстрирует, как можно парсить внешние данные (шрифты) и использовать их для динамического создания графики прямо во время выполнения игры, открывая двери для кастомных систем частиц, процедурной генерации интерфейсов и уникальных визуальных эффектов.
Версия 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.text('3x5', 'assets/loader-tests/3x5.flf');
this.load.spritesheet('balls', 'assets/sprites/balls.png', { frameWidth: 17, frameHeight: 17 });
}
create ()
{
// https://github.com/Marak/asciimo/issues/3
const font = this.cache.text.get('3x5').split('\n');
// flf2a$ 6 5 20 15 3 0 143 229 NOTE: The first five characters in
// | | | | | | | | | | the entire file must be "flf2a".
// / / | | | | | | | \
// Signature / / | | | | | \ Codetag_Count
// Hardblank / / | | | \ Full_Layout*
// Height / | | \ Print_Direction
// Baseline / \ Comment_Lines
// Max_Length Old_Layout*
// flf2a$ 6 4 6 -1 4
const data = font[0].split(' ');
const header = data[0];
const height = parseInt(data[1]);
const width = parseInt(data[2]);
const comments = parseInt(data[5]) + 2;
// The letters start at space (ASCII 32) and go in ASCII order up to 126
const text = 'PHASER 3';
let x = 32;
for (let i = 0; i < text.length; i++)
{
const letter = text.charCodeAt(i);
const offset = comments + ((letter - 32) * height);
this.getCharacter(font, x, 32, offset, width, height);
x += (width * 17);
}
}
getCharacter (font, dx, dy, offset, width, height)
{
let sx = dx;
let sy = dy;
for (let y = offset; y < offset + height; y++)
{
sx = dx;
for (let x = 0; x < width; x++)
{
sx += 17;
if (font[y][x] === '#')
{
this.add.image(sx, sy, 'balls');
}
}
sy += 17;
}
}
}
const config = {
type: Phaser.AUTO,
parent: 'phaser-example',
width: 800,
height: 600,
scene: Example
};
const game = new Phaser.Game(config);
Загрузка данных: текстовый файл и спрайт
В методе preload мы загружаем два критически важных ресурса. Текстовый файл с расширением .flf содержит описание ASCII-шрифта. Это стандартный формат для фигурных шрифтов (FIGlet). Также загружается спрайтшит с изображениями шариков, которые станут 'пикселями' нашего текста.
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.text('3x5', 'assets/loader-tests/3x5.flf');
this.load.spritesheet('balls', 'assets/sprites/balls.png', { frameWidth: 17, frameHeight: 17 });
Парсинг заголовка шрифта FIGlet
В методе create мы сначала получаем загруженный текст из кэша с помощью this.cache.text.get('3x5') и разбиваем его на массив строк. Первая строка файла .flf содержит заголовок с метаданными шрифта.
const font = this.cache.text.get('3x5').split('\n');
const data = font[0].split(' ');
const header = data[0];
const height = parseInt(data[1]);
const width = parseInt(data[2]);
const comments = parseInt(data[5]) + 2;
Из заголовка извлекаются ключевые параметры: высота символа (height), ширина символа (width) и количество строк комментариев (comments), которые нужно пропустить, чтобы добраться до данных символов.
Определяем позицию символа в данных
Символы в файле FIGlet идут в порядке ASCII, начиная с пробела (код 32). Чтобы получить данные для конкретной буквы, нужно рассчитать смещение (offset) от начала массива строк font. Формула учитывает пропущенные строки комментариев и то, что на каждый символ отводится блок строк высотой height.
const text = 'PHASER 3';
let x = 32;
for (let i = 0; i < text.length; i++) {
const letter = text.charCodeAt(i);
const offset = comments + ((letter - 32) * height);
this.getCharacter(font, x, 32, offset, width, height);
x += (width * 17);
}
Цикл проходит по каждой букве строки 'PHASER 3', вычисляет её ASCII-код, находит смещение для её данных в массиве и вызывает метод getCharacter для отрисовки. Переменная `x` увеличивается на ширину символа, умноженную на размер 'пикселя' (17), чтобы буквы не накладывались друг на друга.
Отрисовка символа посимвольно
Метод getCharacter — это сердце примера. Он принимает данные шрифта, начальные координаты отрисовки, смещение до нужного символа, а также его ширину и высоту.
getCharacter (font, dx, dy, offset, width, height) {
let sx = dx;
let sy = dy;
for (let y = offset; y < offset + height; y++) {
sx = dx;
for (let x = 0; x < width; x++) {
sx += 17;
if (font[y][x] === '#') {
this.add.image(sx, sy, 'balls');
}
}
sy += 17;
}
}
Два вложенных цикла проходят по 'сетке' символа размером width x height. Внешний цикл двигается по строкам данных символа (от offset до offset + height). Внутренний цикл проходит по каждому символу в строке. Если в исходных данных встречается символ '#', это означает, что в данной позиции должен быть 'пиксель'. В этой точке с координатами (sx, sy) создаётся изображение (this.add.image) из спрайтшита 'balls'. Шаг в 17 пикселей соответствует размеру кадра в спрайтшите.
Что попробовать дальше
Этот пример — отличная отправная точка для экспериментов. Попробуйте заменить спрайт шариков на частицы из системы this.add.particles, чтобы текст был анимированным. Измените логику условия font[y][x] === '#' на проверку других символов для создания градаций прозрачности или использования разных спрайтов. Загрузите другой .flf файл для изменения стиля шрифта. Наконец, оберните логику в переиспользуемый класс для удобного вывода любого текста в любом месте сцены.
