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

В разработке игр на Phaser каждая мелочь может привести к неожиданным багам. Этот пример демонстрирует коварную проблему, связанную с рендерингом текста при использовании флага `rtl`. Вы узнаете, как один разрушенный текстовый объект с поддержкой письма справа налево может "заразить" контекст рендеринга и скрыть последующие тексты в браузерах на движке Chromium. Понимание этой особенности сэкономит часы отладки и поможет избежать критических визуальных ошибок в вашем проекте.

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

Живой запуск

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

Исходный код


class MainScene extends Phaser.Scene
{

    constructor()
    {
        super({ key: "MainScene" });
    }

    create ()
    {
        const rtlText1 = this.add.text(200, 100, 'Text 1', { rtl: true });
        rtlText1.destroy();

        const regularText2 = this.add.text(200, 200, 'Text 2', { rtl: false }); // In Chrome / Edge, Text 2 would not be shown!

        const regularText3 = this.add.text(200, 300, 'Text 3', { rtl: false }); // In Chrome / Edge, Text 2 would not be shown!
        
        const regularText4 = this.add.text(200, 400, 'Text 4', { rtl: false }); // In Chrome / Edge, Text 2 would not be shown!
        
        const regularText5 = this.add.text(200, 500, 'Text 5', { rtl: true }); // In Chrome / Edge, Text 2 would not be shown!
    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    pixelArt: true,
    scale: {
        mode: Phaser.Scale.FIT,
        autoCenter: Phaser.Scale.CENTER_BOTH
    },
    scene: [ MainScene ]
};

const game = new Phaser.Game(config);

Суть проблемы: контекст Canvas и RTL

Phaser для рендеринга текста использует Canvas API. Флаг rtl в стиле текста влияет на состояние контекста рисования (CanvasRenderingContext2D), в частности, на свойство direction. Проблема возникает, когда текстовый объект с rtl: true уничтожается (destroy()). Внутренние механизмы Phaser пытаются очистить контекст, но в некоторых браузерах (Chrome, Edge) это может оставить контекст в "испорченном" состоянии для последующих операций рисования.

Ключевой момент: состояние контекста (например, direction: rtl) является общим для всех операций на этом Canvas. Если оно некорректно сброшено после удаления RTL-текста, последующие тексты, даже с rtl: false, могут быть отрисованы за пределами видимой области или не отрисованы вовсе.

Разбор проблемного кода

Давайте построчно пройдемся по коду примера, чтобы увидеть цепочку событий, ведущую к багу.

Сначала создается и тут же уничтожается текстовый объект с включенным RTL. Это триггерит проблему.

const rtlText1 = this.add.text(200, 100, 'Text 1', { rtl: true });
rtlText1.destroy();

Затем создается серия текстовых объектов. В исходном комментарии указано, что Text 2 не отображается, но на самом деле проблема может затронуть все последующие объекты, в зависимости от версии браузера и Phaser.

const regularText2 = this.add.text(200, 200, 'Text 2', { rtl: false });
const regularText3 = this.add.text(200, 300, 'Text 3', { rtl: false });
const regularText4 = this.add.text(200, 400, 'Text 4', { rtl: false });

Интересно, что создание нового объекта с rtl: true в конце также может не сработать корректно, так как контекст уже в неопределенном состоянии.

const regularText5 = this.add.text(200, 500, 'Text 5', { rtl: true });

Почему это происходит?

Основная причина кроется во внутреннем методе Text.destroy. При уничтожении текстового объекта Phaser пытается очистить контекст Canvas, который использовался для рендеринга этого текста. Для текста с rtl: true контекст был переключен в режим отрисовки справа налево (устанавливается свойство direction).

В идеальном сценарии, после вызова destroy(), Phaser должен полностью сбросить состояние контекста для этого текстового объекта, включая сброс direction обратно в 'ltr'. Однако, из-за особенностей реализации или специфики браузера, этот сброс может не произойти. В результате контекст, который переиспользуется для следующих вызовов this.add.text(), остается с direction: rtl.

Когда вы создаете новый текст с rtl: false, движок устанавливает позицию X (в данном случае 200), но контекст с direction: rtl интерпретирует эту координату как позицию *правого* края текста, а не левого. Если ширина текста меньше 200 пикселей, он будет отрисован левее заданной точки X и может легко выйти за пределы видимой области или канваса, создавая иллюзию его отсутствия.

Как избежать проблемы: практические решения

Есть несколько стратегий, чтобы обойти эту ошибку в ваших проектах.

**1. Избегайте уничтожения отдельных RTL-текстовых объектов.** Если нужно скрыть текст, используйте setVisible(false). Если объект действительно больше не нужен, убедитесь, что после его уничтожения не происходит немедленное создание нового текста.

**2. Явно сбрасывайте контекст.** Можно попробовать принудительно отрисовать что-то с rtl: false после уничтожения RTL-текста. Создайте временный невидимый текстовый объект.

// После уничтожения проблемного текста
const resetText = this.add.text(-1000, -1000, '', { rtl: false });
resetText.destroy();

**3. Используйте отдельные Canvas-слои.** Для сложных сцен с混合 RTL и LTR текстом рассмотрите возможность рендеринга текста на отдельный Canvas или использование контейнеров, которые можно целиком уничтожать и создавать заново.

**4. Обновляйте Phaser.** Эта проблема (баг #7077) могла быть исправлена в более новых версиях Phaser 3. Всегда проверяйте, используете ли вы актуальную стабильную сборку.

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

Баг с RTL-текстом — наглядный пример того, как внутреннее состояние графического контекста может повлиять на стабильность рендеринга. Главный вывод: операции с Canvas требуют аккуратного управления состоянием, особенно при работе с экзотическими настройками вроде направления текста. Для экспериментов попробуйте воспроизвести баг в разных браузерах (Chrome, Firefox, Safari). Исследуйте, влияет ли на проблему использование pixelArt: true или определенного scale.mode. Также можно поэкспериментировать с отрисовкой простых примитивов (например, this.add.rectangle) между созданием текстовых объектов, чтобы проверить, затрагивает ли "испорченное" состояние только текст или весь рендеринг на Canvas.