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

При создании пиксель-арт игр на Phaser разработчики часто сталкиваются с неприятным эффектом: тайлы или спрайты начинают «дрожать» или «протекать» при скроллинге камеры, особенно в определенных разрешениях. Эта статья разбирает ключевую причину проблемы — взаимодействие режима масштабирования `Phaser.Scale.RESIZE` с параметром `autoRound`. Мы наглядно покажем, как настройки `scale` в конфиге влияют на рендеринг, и объясним, почему стандартные методы вроде `pixelArt: true` или `roundPixels: true` не всегда помогают. Понимание этой механики сэкономит вам часы отладки визуальных артефактов.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        // 1198 = ok horizontal
        // 865 = bad vertical

        // 1209 - bad horizontal
        // 867  - bad vertical

        // 1210 = good horizontal
        // 870 = good vertical

        /*
            872 = good vertical
            902 = good vertical
            1028



            903 = bad vertical!
            947 = bad vertical!
            1031 = bad vertical

            1213 = bad horizontal
            1220 - good horizontal
            1026 good h
            1041 bad h


        */




        // this.scale.displaySize.setSnap(1, 1);

        this.load.image('tiles', 'assets/tilemaps/tiles/catastrophi_tiles_16.png');
        this.load.tilemapCSV('map', 'assets/tilemaps/csv/catastrophi_level2.csv');
    }

    create ()
    {
        const map = this.make.tilemap({ key: 'map', tileWidth: 16, tileHeight: 16 });
        const tileset = map.addTilesetImage('tiles');
        const layer = map.createLayer(0, tileset, 0, 0);

        // this.cameras.main.roundPixels = true;
        // Visual test to make sure tiles don't bleed while scrolling at certain speeds
    }

    update (time, delta)
    {
        this.cameras.main.scrollX = (200 + Math.cos(time / 1000) * 200);
        this.cameras.main.scrollY = (100 + Math.sin(time / 1000) * 500);
    }
}

const config = {
    type: Phaser.WEBGL,
    scale: { mode: Phaser.Scale.RESIZE, autoRound: false }, // <--- Just add this to reproduce
    width: 800,
    height: 600,
    backgroundColor: '#2d2d2d',
    parent: 'phaser-example',
    pixelArt: true, // <--- These do not improve bleeding
    roundPixels: true, // <--- These do not improve bleeding
    scene: Example
};

const game = new Phaser.Game(config);

Корень проблемы: autoRound и дробные координаты

Исходный код примера специально создан для демонстрации бага (bugs/6674). Ключевая настройка находится в объекте scale конфигурации игры:

scale: { mode: Phaser.Scale.RESIZE, autoRound: false }

Режим Phaser.Scale.RESIZE заставляет канвас игры растягиваться или сжиматься под размеры родительского контейнера. Параметр autoRound управляет округлением размеров канваса. Если autoRound: false, Phaser может установить канвасу дробные значения ширины и высоты (например, 800.5px). Это ломает четность пиксельной сетки.

В update камера постоянно движется по синусоиде, вычисляя дробные значения для скролла:

this.cameras.main.scrollX = (200 + Math.cos(time / 1000) * 200);
this.cameras.main.scrollY = (100 + Math.sin(time / 1000) * 500);

Когда дробный скролл накладывается на канвас с дробными размерами, позиции тайлов при отрисовке получают субпиксельные сдвиги. Для пиксель-арт это фатально: движок пытается отрисовать графику «между» пикселями экрана, вызывая размытие и дрожание.

Почему pixelArt и roundPixels бессильны?

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

pixelArt: true,
roundPixels: true

pixelArt: true автоматически включает текстуру фильтр NEAREST для загруженных изображений, предотвращая билинейную интерполяцию при масштабировании. roundPixels: true округляет координаты *спрайтов* при рендеринге. Однако в примере используется тайловая карта (Tilemap), созданная через this.make.tilemap. Параметр roundPixels не применяется к тайлам так же, как к спрайтам. Более того, главный источник проблемы — дробные размеры канваса (autoRound: false) — не исправляется этими настройками. Они борются со следствиями, но не с причиной.

Эксперимент: изменение разрешений и артефакты

В закомментированном коде preload() автор оставил заметки о конкретных разрешениях, где артефакты проявляются («bad») или нет («good»). Например: - 1210 = good horizontal - 903 = bad vertical!

Это связано с тем, что при определенных ширине или высоте контейнера расчетный размер канваса становится целым (good) или дробным (bad). Воспроизвести эффект можно, меняя размер окна браузера. При «плохих» размерах тайлы будут заметно подрагивать или между ними появятся тонкие щели (bleeding).

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

Решение: включаем autoRound или меняем режим масштабирования

Самый прямой способ устранить дрожание — гарантировать целочисленные размеры канваса. Для этого в конфигурации scale нужно установить autoRound: true (значение по умолчанию).

scale: { mode: Phaser.Scale.RESIZE, autoRound: true }

Теперь Phaser будет округлять размеры канваса до целых пикселей. Это немедленно уберет субпиксельные сдвиги и стабилизирует рендеринг тайлов.

Альтернативный подход — сменить режим масштабирования, если RESIZE не обязателен. Например, Phaser.Scale.FIT также подстраивает игру под контейнер, но добавляет черные полосы (letterbox), сохраняя точное соотношение сторон и целые пиксельные размеры игрового мира.

scale: { mode: Phaser.Scale.FIT }

Дополнительные меры для тайловых карт

Если вам критично использовать autoRound: false (например, для максимально плавного RESIZE), можно применить точечные решения для камеры, работающей с тайлами.

Включите округление пикселей непосредственно для основной камеры. Раскомментируйте строку в методе create:

this.cameras.main.roundPixels = true;

Это заставит камеру округлять свою позицию скролла перед отрисовкой. В сочетании с pixelArt: true это может значительно улучшить ситуацию, хотя и не устранит причину полностью, если размер канваса дробный.

Также убедитесь, что размер тайла (16x16 в примере) кратен размеру базового тайлсета. Несоответствие может давать дополнительные артефакты.

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

Дрожание тайлов в Phaser при использовании Scale.RESIZE — распространенная проблема, которая решается настройкой autoRound: true. Запомните: параметры pixelArt и roundPixels не панацея, особенно для тайловых карт. Они не компенсируют дробные размеры канваса. Для экспериментов попробуйте

  1. Сравнить поведение при autoRound: true/false, меняя размер окна
  2. Протестировать другие режимы масштабирования, например FIT или ENVELOP
  3. Включить cameras.main.roundPixels и pixelArt одновременно и по отдельности, чтобы увидеть разницу в качестве рендеринга