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

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

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

Живой запуск

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

Исходный код


let s = {
    y: -32
};

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('1984', 'assets/pics/1984-nocooper-space.png');
        this.load.image('contra', 'assets/pics/contra1.png');
    }

    create ()
    {
        this.add.image(0, 0, '1984').setOrigin(0).setScale(2);

        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, 100, 'knighthawks', '             PHASER 3 IS IN THE HOUSE');

        this.dynamic.setSize(320, 100);
        this.dynamic.setScale(2);
        this.dynamic.setDisplayCallback(this.textCallback);

        this.tweens.add({
            targets: s,
            duration: 500,
            y: 32,
            ease: 'Sine.easeInOut',
            repeat: -1,
            yoyo: true
        });

        this.add.image(640, 400, 'contra').setOrigin(1).setScale(2);
    }

    //  data = { index: index, charCode: charCode, x: x, y: y, scaleX: scaleX, scaleY: scaleY, data: Object }
    textCallback (data)
    {
        data.y += 32 + s.y * Math.sin(data.index);

        return data;
    }


    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: 640,
    height: 480,
    scene: Example
};

const game = new Phaser.Game(config);


Подготовка ресурсов и настройка сцены

В методе preload() загружаются необходимые ресурсы: изображение для ретро-шрифта (knighthawks), фоновое изображение (1984) и дополнительное изображение для декора (contra). Базовый 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('1984', 'assets/pics/1984-nocooper-space.png');
    this.load.image('contra', 'assets/pics/contra1.png');
}

В create() сначала добавляется фоновое изображение. Ключевой шаг — создание растрового шрифта из изображения. Для этого используется утилита Phaser.GameObjects.RetroFont.Parse. Конфигурация точно описывает, как изображение разбито на символы.

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));

Создание и настройка динамического текста

Теперь можно создать объект динамического растрового текста. Его главное преимущество — возможность модифицировать свойства каждого символа во время рендеринга через callback-функцию.

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

После создания задаются важные свойства: - setSize(320, 100) — определяет видимую область (окно) для текста. - setScale(2) — увеличивает текст. - setDisplayCallback(this.textCallback) — привязывает функцию, которая будет вызываться для каждого символа перед отрисовкой. Это сердце динамического эффекта.

Также создаётся бесконечная tween-анимация для объекта `s, которая плавно меняет его свойствоy` от -32 до 32 и обратно. Это значение будет использоваться в callback-функции для создания волны.

Магия callback-функции рендеринга

Функция textCallback вызывается для каждого символа в строке. Она получает объект data с параметрами символа: индекс, код, координаты, масштаб и другие данные. Функция может изменять эти параметры, и её возвращаемое значение определяет, как символ будет отрисован.

textCallback (data)
{
    data.y += 32 + s.y * Math.sin(data.index);
    return data;
}

Здесь мы модифицируем вертикальную позицию data.y каждого символа. Формула 32 + s.y * Math.sin(data.index) создаёт волнообразный эффект: - 32 — базовый сдвиг. - s.y — анимируемое значение от твина (колеблется от -32 до 32). - Math.sin(data.index) — использует индекс символа для создания последовательной волны. Каждый следующий символ будет сдвинут по синусоиде относительно предыдущего. Таким образом, вся текстовая строка плавно «колышется» вверх и вниз.

Анимация горизонтального скролла

Чтобы текст плавно двигался справа налево, как бегущая строка, в методе update() изменяется свойство scrollX объекта this.dynamic.

update (time, delta)
{
    this.dynamic.scrollX += 0.15 * delta;
    if (this.dynamic.scrollX > 1300)
    {
        this.dynamic.scrollX = 0;
    }
}

Приращение 0.15 * delta обеспечивает плавное движение, не зависящее от частоты кадров. Когда значение scrollX превышает заданный предел (1300), оно сбрасывается в ноль, создавая эффект бесконечного зацикленного скролла. Это стандартный приём для создания панорамного движения в играх.

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

Комбинируя DynamicBitmapText, callback-функцию рендеринга и tween-анимации, вы можете создавать сложные и привлекательные текстовые эффекты с минимальным кодом. Для экспериментов попробуйте изменить формулу в textCallback: используйте Math.cos, умножьте индекс на коэффициент для изменения частоты волны или добавьте влияние времени (time) для пульсирующих эффектов. Также можно поиграть со свойством data.scaleX или data.scaleY для анимации размеров отдельных символов.