О чем этот пример
При разработке игр с пиксель-артом или требующих идеального выравнивания графики, разработчики часто сталкиваются с едва заметными, но критичными визуальными артефактами — тонкими разрывами между спрайтами. Эти артефакты возникают из-за особенностей рендеринга при работе с дробными координатами и нецелыми размерами отображения. В этой статье мы разберем конкретный пример кода, демонстрирующий эту проблему, и объясним, как настройка `roundPixels` и правильное позиционирование помогают достичь бесшовного отображения графики, что особенно важно для игр с видом сверху, платформеров или тактических RPG.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
// this.load.image('bg', 'assets/pics/checker.png');
this.load.image('bg', 'assets/skies/spookysky.jpg');
}
create ()
{
// this.add.image(0, 0, 'bg').setOrigin(0, 0).setDisplaySize(256, 256);
// this.add.image(1, 1, 'bg').setOrigin(0, 0).setDisplaySize(256, 256);
// this.add.image(140, 140, 'bg').setDisplaySize(257, 257);
// this.add.image(140+257, 140, 'bg').setDisplaySize(257, 257);
// this.add.image(140+257+257, 140, 'bg').setDisplaySize(257, 257);
// this.add.image(201, 300, 'bg').setDisplaySize(101, 101);
// 49 x 249 (3.80)
// 50 x 249 (dev)
const p1 = this.add.image(100, 100, 'bg').setDisplaySize(101, 101);
this.add.image(251.32, 103, 'bg').setDisplaySize(101, 101);
this.add.image(401, 123, 'bg').setOrigin(0, 0).setDisplaySize(101, 101);
// const p2 = this.add.image(201, 100, 'bg').setDisplaySize(101, 101);
// console.log/(p1.x, p1.y, 'pic2', p2.x, p2.y);
// console.log(p1.getBounds()); // x: 49.5, y: 249.5, wh: 101 (3.80 and dev)
}
}
const config = {
type: Phaser.WEBGL,
backgroundColor: "00dd00",
parent: 'phaser-example',
scene: Example,
roundPixels: true,
width: 800,
height: 600
};
const game = new Phaser.Game(config);
Корень проблемы: дробные координаты и рендеринг
Графический движок Phaser, как и большинство других, оперирует координатами с плавающей точкой. Однако физические пиксели на экране — дискретны. Когда движок пытается отрисовать текстуру в позиции с дробной частью (например, x=251.32), он вынужден интерполировать цвет пикселей, что может привести к их смещению или прозрачным промежуткам на границах спрайтов.
Особенно это заметно при использовании спрайтов одинакового размера, которые должны плотно прилегать друг к другу, образуя цельное изображение (например, плитки для тайловой карты или фоновое небо).
const config = {
type: Phaser.WEBGL,
roundPixels: true,
width: 800,
height: 600
};
Спасательный круг: параметр `roundPixels`
Конфигурационный параметр roundPixels — это первая линия обороны против разрывов. Когда он установлен в true, движок автоматически округляет конечные координаты *всех* игровых объектов до целых чисел перед их отправкой на рендеринг.
Это гарантирует, что каждый пиксель текстуры будет отрисован четко в границах физических пикселей экрана, устраняя размытие и смещение.
Однако, как видно из примера, одного этого параметра может быть недостаточно. Если исходная позиция или расчеты логики игры генерируют сильно "сбитые" дробные координаты, простое округление на этапе рендеринга может не спасти ситуацию, особенно при сложных цепочках преобразований.
// В конфиге игры активируем округление пикселей
const config = {
// ... другие настройки ...
roundPixels: true,
};
Практическое позиционирование: `setOrigin` и целочисленные координаты
Ключ к полному контролю — управление позиционированием на уровне кода сцены. В примере видно три подхода:
1. **Объект с центром по умолчанию:** Первое изображение создается с центром в середине (origin=0.5). Его метод getBounds() показал бы, что его левый верхний угол начинается с дробной координаты (49.5, 49.5), если размер нечетный (101).
2. **Объект с дробной позицией:** Второе изображение имеет явную дробную координату X (251.32). Даже с roundPixels: true такое смещение может вызвать проблемы.
3. **Контролируемый объект:** Третье изображение использует setOrigin(0, 0) для привязки точки отсчета к левому верхнему углу спрайта и устанавливается на целые координаты (401, 123). Это самый предсказуемый и надежный метод для создания сеток и бесшовных областей.
// Надежный способ позиционирования для бесшовной стыковки
// 1. Устанавливаем точку отсчета (origin) в левый верхний угол.
// 2. Задаем целочисленные координаты.
// 3. Устанавливаем нужный размер.
this.add.image(401, 123, 'bg')
.setOrigin(0, 0)
.setDisplaySize(101, 101);
Комбинированная стратегия для идеального результата
Для достижения гарантированно чистого результата без артефактов рекомендуется двухуровневая стратегия:
1. **На уровне глобальной конфигурации:** Всегда включайте roundPixels: true. Это базовая защита от случайных дробных значений, возникающих в физике или анимациях.
2. **На уровне кода сцены:** При создании статических или критически важных для визуала объектов (фоны, тайлы, элементы интерфейса) сознательно устанавливайте их координаты в целые числа. Для объектов, которые должны стыковаться, используйте setOrigin(0, 0) для упрощения расчетов сетки.
Таким образом, roundPixels обрабатывает динамику, а ручное позиционирование обеспечивает точность статики.
create ()
{
// Создаем бесшовный ряд тайлов
const tileSize = 101;
for (let x = 0; x < 800; x += tileSize) {
for (let y = 0; y < 600; y += tileSize) {
// Целочисленные координаты и origin в (0,0) гарантируют стыковку
this.add.image(x, y, 'bg')
.setOrigin(0, 0)
.setDisplaySize(tileSize, tileSize);
}
}
}
Что попробовать дальше
Борьба с пиксельными артефактами — это внимание к деталям. Включение roundPixels: true в конфиг и осознанное использование целочисленных координат с правильным origin для статичных объектов — залог чистой и аккуратной графики. Для экспериментов попробуйте отключить roundPixels в примере и увидьте разницу, или создайте плавно движущийся спрайт с дробной скоростью — с включенной настройкой его движение будет чуть более "ступенчатым", но зато четким, что может стать стилистическим выбором для пиксельных игр.
