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

Часто в играх нужны нестандартные способы отображения информации: пиксельный текст, эффекты старого терминала или элементы стиля ретро. В этом примере мы используем 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 файл для изменения стиля шрифта. Наконец, оберните логику в переиспользуемый класс для удобного вывода любого текста в любом месте сцены.