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

При работе с визуальными эффектами в Phaser 3 вы можете столкнуться с неочевидным поведением: добавление эффектов PreFX к одному объекту может повлиять на отрисовку других, казалось бы, независимых элементов, например, простых геометрических фигур. Этот пример наглядно демонстрирует эту особенность рендеринга и объясняет, почему это происходит. Понимание этого механизма поможет избежать визуальных артефактов и глубже разобраться в конвейере отрисовки движка.

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

Живой запуск

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

Исходный код


    class Example extends Phaser.Scene
{
    offset;
    graphics;
    bob;

    preload ()
    {
        
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.atlas('atlas', 'assets/atlas/megaset-2.png', 'assets/atlas/megaset-2.json');
    }

    create ()
    {
        let text = this.add.text( 512, 325, 'TEST TEXT', { fontFamily: 'Arial', fontSize: 48, color: '#808080' } );
        text.preFX?.addShine();

        this.add.rectangle( 512, 640, 800, 20, 0xffffff, 0.2 );
    }
}

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

const game = new Phaser.Game(config);

Суть проблемы

В предоставленном примере создаются два визуальных объекта: текстовый элемент с эффектом свечения (Shine) и простой белый прямоугольник с прозрачностью. Код выглядит корректным, и ожидается, что прямоугольник будет отрисован как обычно. Однако из-за внутренней оптимизации Phaser 3, связанной с системой эффектов PreFX, прямоугольник может отображаться некорректно (например, не отрисовываться вовсе или иметь неверный цвет).

Это происходит потому, что PreFX эффекты требуют рендера в отдельный текстуру (Frame Buffer). Процесс активации этого буфера для одного объекта может непреднамеренно затронуть состояние рендерера для последующих примитивов, если они отрисовываются в том же кадре без явного сброса контекста.

Разбор кода

Давайте посмотрим на ключевые моменты создания объектов в методе create().

Сначала создается текстовый объект, и к нему применяется эффект addShine(). Оператор ?. (optional chaining) используется для безопасного вызова, так как свойство preFX может быть не доступно в некоторых конфигурациях рендерера (например, WebGL должен быть активен).

let text = this.add.text( 512, 325, 'TEST TEXT', { fontFamily: 'Arial', fontSize: 48, color: '#808080' } );
text.preFX?.addShine();

Затем создается прямоугольник. Обратите внимание, что это простой графический примитив, не использующий FX.

this.add.rectangle( 512, 640, 800, 20, 0xffffff, 0.2 );

Конфигурация игры стандартная, с чёрным фоном.

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

Почему так происходит? Взгляд на рендерер

Phaser 3 для повышения производительности минимизирует переключения состояний графического конвейера (как в Canvas, так и в WebGL рендерере). Когда вы добавляете эффект PreFX к объекту (например, addShine()), этот объект помечается для рендера в специальный off-screen буфер, где применяются эффекты, а затем результат выводится на основной канвас.

Алгоритм отрисовки кадра пытается сгруппировать объекты. Если после отрисовки объекта с PreFX движок сразу начинает рисовать следующий объект (наш прямоугольник), он может продолжить использовать настройки или целевой буфер, связанный с FX-пайплайном. Для простого прямоугольника, который не должен рендериться в FX-буфер, это приводит к ошибке: он либо рисуется не туда, либо состояние blending (смешивания цветов, crucial для прозрачности) оказывается сброшенным или неверным.

Ключевая причина — отсутствие явного "сброса" контекста рендерера к состоянию по умолчанию для основных (не-FX) объектов между разнотипными операциями отрисовки в рамках одного кадра.

Решение и обходные пути

Хотя это поведение может быть исправлено в будущих версиях Phaser, на данный момент есть несколько практических способов его избежать.

1. **Использование PostFX вместо PreFX:** Если возможно, рассмотрите переход на PostFX эффекты. Они применяются после того, как весь сцена (или камера) отрендерена, и не вмешиваются в процесс отрисовки отдельных игровых объектов.

text.postFX?.addShine(); // Используем PostFX

2. **Изменение порядка отрисовки:** Иногда проблема решается, если сначала нарисовать все простые объекты, а затем — объекты с PreFX. Попробуйте поменять местами создание прямоугольника и текста в коде.

// Сначала прямоугольник
    this.add.rectangle( 512, 640, 800, 20, 0xffffff, 0.2 );
    // Затем текст с FX
    let text = this.add.text( 512, 325, 'TEST TEXT', { fontFamily: 'Arial', fontSize: 48, color: '#808080' } );
    text.preFX?.addShine();

3. **Принудительный рендер группы:** Поместите объекты с PreFX и без в разные слои (Phaser.GameObjects.Layer) или группы. Рендерер более корректно обрабатывает смену состояний между разными контейнерами.

// Создаем слой для FX-объектов
    const fxLayer = this.add.layer();
    const text = this.add.text(512, 325, 'TEST TEXT', { fontFamily: 'Arial', fontSize: 48, color: '#808080' });
    text.preFX?.addShine();
    fxLayer.add(text);
    // Прямоугольник остается в основном списке отображения сцены
    this.add.rectangle(512, 640, 800, 20, 0xffffff, 0.2);

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

Некорректная отрисовка простых объектов рядом с PreFX-элементами — это известная особенность текущей реализации графического конвейера Phaser 3, связанная с оптимизацией переключений состояний рендерера. Для стабильного результата рекомендуется использовать PostFX, тщательно контролировать порядок добавления объектов или группировать их по типам рендеринга. Для экспериментов попробуйте воспроизвести проблему с другими геометрическими примитивами (circle, triangle), поэкспериментируйте с разными эффектами PreFX (например, addGlow или addShadow) и проверьте, как меняется поведение при использовании Canvas рендерера вместо WebGL (установите type: Phaser.CANVAS в конфиге).