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

Работа с bitmap-шрифтами в Phaser позволяет создавать стилизованный текст для игр, но иногда скрытые баги могут испортить весь вид. В этом примере мы столкнемся с тонкой ошибкой при динамическом добавлении текста в объект `BitmapText`, у которого установлено ограничение по ширине (`maxWidth`). Понимание этой проблемы и её обходного решения поможет вам избежать неожиданного поведения UI в ваших проектах и писать более надежный код.

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

Живой запуск

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

Исходный код


let goodText, badText;
const message = "Look at this beautiful text!";

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

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.bitmapFont('desyrel-pink', 'assets/fonts/bitmap/desyrel-pink.png', 'assets/fonts/bitmap/desyrel-pink.xml');
    }

    create ()
    {
        goodText = this.add.bitmapText(50, 50, 'desyrel-pink', '', 24);
        badText = this.add.bitmapText(50, 100, 'desyrel-pink', '', 24);
        badText.setMaxWidth(500);
    }

    update ()
    {
        for (const t of [goodText, badText]) {
            if (t.text.length < message.length) {
            const nextChar = message[t.text.length];
            t.setText(t.text + nextChar);
            // uncomment the next line to workaround the bug
            // t._bounds.maxWidth--;
          }
        }
    }
}

const config = {
  type: Phaser.AUTO,
  width: 800,
  height: 600,
  parent: 'phaser-example',
  scene: Example
};

const game = new Phaser.Game(config);

Суть проблемы: maxWidth ломает динамический текст

В примере создаются два текстовых объекта с bitmap-шрифтом. Оба должны постепенно, посимвольно, выводить одну и ту же фразу. Ключевое отличие: для второго объекта badText установлено ограничение максимальной ширины с помощью метода setMaxWidth(500).

Логика динамического добавления символов реализована в методе update(), который вызывается каждый кадр. Он проверяет, не достиг ли текущий текст длины целевого сообщения, и если нет — добавляет следующий символ.

for (const t of [goodText, badText]) {
    if (t.text.length < message.length) {
        const nextChar = message[t.text.length];
        t.setText(t.text + nextChar);
    }
}

Ожидается, что текст будет появляться одинаково в обоих случаях. Однако объект с установленным maxWidth перестает отображать новые символы после того, как его текст достигает границы переноса строки. Это и есть баг.

Анализ кода: что происходит под капотом

Метод setText() объекта BitmapText не только обновляет строку, но и пересчитывает её отображение, включая перенос по словам, если задан maxWidth. Похоже, в момент этого пересчета внутреннее состояние объекта (в частности, свойство _bounds.maxWidth) сбрасывается или некорректно обрабатывается для уже перенесенной строки при последующих вызовах.

В результате, после первого переноса, система считает, что текст уже занял всю допустимую ширину, и просто игнорирует добавление новых символов, хотя визуально место еще есть. Это внутреннее свойство _bounds не является частью публичного API Phaser, но его можно использовать для отладки и создания временных решений, как показано в комментарии к исходнику.

Обходное решение (workaround)

В закомментированной строке кода предложен неофициальный способ заставить текст отображаться корректно. Он заключается в ручном уменьшении внутреннего счетчика максимальной ширины после каждого обновления текста.

// uncomment the next line to workaround the bug
// t._bounds.maxWidth--;

Этот код нужно добавить внутрь цикла в методе update(), сразу после вызова setText(). Уменьшая значение _bounds.maxWidth на 1, мы "обманываем" внутренний механизм рендеринга, заставляя его каждый раз заново проверять и применять перенос для обновленной строки. Это временное решение, пока баг не будет исправлен в самом движке. Помните, что обращение к свойствам с подчеркиванием (вроде _bounds) всегда рискованно, так как они могут измениться в будущих версиях Phaser.

Как избежать проблемы в своих играх

Поскольку работа с приватными свойствами — это крайняя мера, рассмотрим более стабильные альтернативы.

1. **Отложить установку maxWidth.** Если текст сначала формируется полностью, а только потом применяется ограничение по ширине, баг не проявится.

// Сначала задаем весь текст
    dynamicText.setText(fullMessage);
    // Потом устанавливаем maxWidth для его форматирования
    dynamicText.setMaxWidth(500);

2. **Использовать другой тип текста.** Для сложных динамических текстовых блоков рассмотрите возможность использования HTML-элементов поверх canvas или другого подхода к UI.

3. **Обновлять текст реже.** Вместо добавления по одному символу каждый кадр (в update()), можно использовать таймеры или события, формируя текст более крупными блоками перед установкой maxWidth.

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

Данный пример наглядно показывает, как внутренняя оптимизация рендеринга BitmapText может конфликтовать с динамическим обновлением контента. Баг с maxWidth — это повод всегда тщательно тестировать UI-элементы в разных сценариях. Для экспериментов попробуйте изменить значение в setMaxWidth(), использовать другой bitmap-шрифт или реализовать эффект печатающегося текста (typewriter effect) через отдельный слой без ограничения ширины, а затем применить maxWidth к конечному результату.