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

Создание атмосферных ретро-интерфейсов — важная часть геймдева. Phaser 3 предлагает мощный инструмент `DynamicBitmapText`, который позволяет не только выводить растровый шрифт, но и анимировать его, создавая эффекты бегущей строки или мерцания. Эта статья покажет, как загрузить кастомный растровый шрифт, создать динамический текстовый объект и управлять его прокруткой в реальном времени, что идеально подходит для титров, внутриигровых сообщений или стилизации под классические аркадные игры.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('knighthawks', 'assets/fonts/retro/knight3.png');
        this.load.image('rain', 'assets/pics/thalion-rain.png');
        this.load.image('contra', 'assets/pics/contra1.png');
    }

    create ()
    {
        this.add.image(0, 0, 'rain').setOrigin(0).setScale(4);

        const config = {
            image: 'knighthawks',
            width: 31,
            height: 25,
            chars: Phaser.GameObjects.RetroFont.TEXT_SET6,
            charsPerRow: 10,
            spacing: { x: 1, y: 1 }
        };

        this.cache.bitmapFont.add('knighthawks', Phaser.GameObjects.RetroFont.Parse(this, config));

        this.dynamic = this.add.dynamicBitmapText(0, 0, 'knighthawks', '               PHASER 3 IS IN THE HOUSE');

        this.dynamic.setScale(4);

        this.tweens.add({
            targets: this.dynamic,
            duration: 4000,
            y: 175*4,
            ease: 'Sine.easeInOut',
            repeat: -1,
            yoyo: true
        });

        this.add.image(1280, 800, 'contra').setOrigin(1).setScale(4);
    }

    update (time, delta)
    {
        this.dynamic.scrollX += 0.15 * delta;

        if (this.dynamic.scrollX > 1300)
        {
            this.dynamic.scrollX = 0;
        }
    }
}

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    pixelArt: true,
    width: 1280,
    height: 800,
    scene: Example
};

const game = new Phaser.Game(config);

Загрузка ресурсов и настройка сцены

В методе preload загружаются все необходимые ресурсы: фоновое изображение, картинка для шрифта и дополнительный графический элемент. Ключевой момент — использование this.load.setBaseURL для указания базового URL, что позволяет использовать относительные пути.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('knighthawks', 'assets/fonts/retro/knight3.png');
    this.load.image('rain', 'assets/pics/thalion-rain.png');
    this.load.image('contra', 'assets/pics/contra1.png');
}

Создание и кеширование растрового шрифта

В методе create сначала выводится фоновое изображение. Затем определяется конфигурация для создания растрового шрифта из изображения knighthawks. Конфиг включает размер одного символа, набор символов, их расположение на изображении и отступы.

const config = {
    image: 'knighthawks',
    width: 31,
    height: 25,
    chars: Phaser.GameObjects.RetroFont.TEXT_SET6,
    charsPerRow: 10,
    spacing: { x: 1, y: 1 }
};

Статический метод Phaser.GameObjects.RetroFont.Parse анализирует изображение на основе конфига и создаёт данные шрифта. Эти данные добавляются в кеш под ключом 'knighthawks', чтобы в дальнейшем использовать их как идентификатор шрифта.

this.cache.bitmapFont.add('knighthawks', Phaser.GameObjects.RetroFont.Parse(this, config));

Работа с DynamicBitmapText и анимация

После подготовки шрифта создаётся объект DynamicBitmapText. Его ключевое отличие от обычного BitmapText — наличие свойств scrollX и scrollY, позволяющих сдвигать текст внутри своей области отображения.

this.dynamic = this.add.dynamicBitmapText(0, 0, 'knighthawks', '               PHASER 3 IS IN THE HOUSE');
this.dynamic.setScale(4);

Обратите внимание на пробелы в начале строки — это простой способ задать начальный отступ для прокрутки. Для объекта также создаётся твин, который плавно перемещает его по вертикали, создавая дополнительный эффект движения.

this.tweens.add({
    targets: this.dynamic,
    duration: 4000,
    y: 175*4,
    ease: 'Sine.easeInOut',
    repeat: -1,
    yoyo: true
});

Основная логика прокрутки в update

Сердце эффекта бегущей строки находится в методе update. Каждый кадр мы увеличиваем свойство scrollX динамического текста. Умножение на delta (время, прошедшее с предыдущего кадра) делает скорость прокрутки независимой от частоты кадров.

update (time, delta)
{
    this.dynamic.scrollX += 0.15 * delta;

    if (this.dynamic.scrollX > 1300)
    {
        this.dynamic.scrollX = 0;
    }
}

Когда прокрутка превышает определённое значение (в данном случае 1300), она сбрасывается в ноль, создавая впечатление бесконечно зацикленной строки. Значение подобрано эмпирически под длину текста и масштаб.

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

Использование DynamicBitmapText открывает простой путь к созданию плавно движущегося текста в стиле старых игр. Для экспериментов попробуйте изменить скорость прокрутки, направление (scrollY), добавить эффект мерцания через setAlpha в твине или использовать несколько строк с разными задержками. Также можно поиграть с другими наборами символов TEXT_SET из RetroFont или создать собственное изображение шрифта.