О чем этот пример
При создании пиксель-арт игр на 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 не панацея, особенно для тайловых карт. Они не компенсируют дробные размеры канваса. Для экспериментов попробуйте
- Сравнить поведение при
autoRound: true/false, меняя размер окна - Протестировать другие режимы масштабирования, например
FITилиENVELOP - Включить
cameras.main.roundPixelsиpixelArtодновременно и по отдельности, чтобы увидеть разницу в качестве рендеринга
