О чем этот пример
При разработке игр для современных устройств важно учитывать разную плотность пикселей на экранах. Особенно это критично для элементов интерфейса и графики, где чёткость линий напрямую влияет на восприятие. В этой статье мы разберём, как использовать `window.devicePixelRatio` в Phaser для создания масок и графики, которые корректно отображаются на Retina и других экранах с высокой плотностью пикселей. Вы научитесь адаптировать размеры графических примитивов под реальное разрешение устройства, избегая размытости и артефактов.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
constructor ()
{
super();
}
create ()
{
const DPR = window.devicePixelRatio;
// const DPR = 2;
const { width, height } = this.scale
const overlayFade = this.add.rectangle(width / 2, height / 2, width, height, 0x000000);
const help1Mask = this.add.graphics().fillRoundedRect(0, 0, 115*DPR, 38*DPR, 16*DPR).setVisible(false)
const geoMask = help1Mask.createGeometryMask()
geoMask.invertAlpha = true
overlayFade.setMask(geoMask)
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#00ff00',
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Что такое devicePixelRatio и зачем он нужен
Свойство window.devicePixelRatio (DPR) — это соотношение между физическими пикселями на устройстве и логическими (CSS) пикселями, используемыми для отрисовки. На Retina-экранах DPR обычно равен 2 или 3, что означает: один логический пиксель отображается несколькими физическими. Если игнорировать DPR, графика (особенно тонкие линии, скругления) может выглядеть размытой или неровной.
В контексте Phaser, когда мы создаём графические объекты (например, через this.add.graphics()), их размеры задаются в логических пикселях. Но для масок и других элементов, где важна точность, нужно умножать размеры на DPR, чтобы они соответствовали физическому разрешению.
Разбор примера: создание инвертированной маски
Рассмотрим исходный код примера. В нём создаётся сцена, где на зелёном фоне размещается чёрный прямоугольник (overlayFade), который перекрывает весь экран. Затем создаётся маска с отверстием, через которое будет виден фон.
Ключевые шаги:
1. Получаем DPR устройства.
2. Создаём графический объект (help1Mask) для маски. Его размеры (ширина, высота, радиус скругления) умножаются на DPR.
3. Из этого графического объекта создаётся геометрическая маска (createGeometryMask).
4. Свойство invertAlpha устанавливается в true, что инвертирует маску: область графического объекта становится прозрачной, а всё вокруг — непрозрачным.
5. Маска применяется к чёрному прямоугольнику.
const DPR = window.devicePixelRatio;
const { width, height } = this.scale
const overlayFade = this.add.rectangle(width / 2, height / 2, width, height, 0x000000);
const help1Mask = this.add.graphics().fillRoundedRect(0, 0, 115*DPR, 38*DPR, 16*DPR).setVisible(false)
const geoMask = help1Mask.createGeometryMask()
geoMask.invertAlpha = true
overlayFade.setMask(geoMask)
Почему умножение на DPR важно для маски
В коде размеры скруглённого прямоугольника для маски заданы так: 115*DPR, 38*DPR, 16*DPR. Если DPR=2, то физически будет нарисован прямоугольник шириной 230 пикселей, высотой 76 пикселей с радиусом скругления 32 пикселя. После создания маски Phaser масштабирует её обратно до логических размеров (115x38), сохраняя при этом чёткие края благодаря изначально высокому разрешению.
Без умножения на DPR (например, если бы мы использовали фиксированные значения 115, 38, 16) границы маски на Retina-экране выглядели бы сглаженными или ступенчатыми, так как Phaser пытался бы растянуть малое количество пикселей на большую физическую область.
// Без учёта DPR - возможна размытость на Retina
graphics.fillRoundedRect(0, 0, 115, 38, 16);
// С учётом DPR - чёткие границы на любом экране
graphics.fillRoundedRect(0, 0, 115*DPR, 38*DPR, 16*DPR);
Особенности конфигурации и отрисовки
В примере используется стандартная конфигурация Phaser с зелёным фоном. Обратите внимание, что width и height в конфиге заданы в логических пикселях (800x600). Phaser автоматически масштабирует весь canvas в соответствии с настройками родительского контейнера и DPR, но внутренняя логика игры (координаты, размеры объектов) по-прежнему работает в логических пикселях.
Маска создаётся с setVisible(false), потому что сам графический объект help1Mask не должен отображаться — он нужен только как источник данных для маски (createGeometryMask).
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#00ff00',
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Что попробовать дальше
Использование window.devicePixelRatio для расчёта размеров графических примитивов — простой, но эффективный способ обеспечить чёткость UI и эффектов на экранах с высокой плотностью пикселей. Этот подход особенно важен для масок, линий, текста и мелких деталей интерфейса.
Для экспериментов попробуйте:
1. Изменить значение DPR вручную (например, const DPR = 3;), чтобы увидеть, как маска адаптируется под разные экраны.
2. Применить аналогичный принцип к другим графическим объектам: this.add.line, this.add.circle.
3. Создать сложную составную маску из нескольких фигур, каждая с учётом DPR, для реализации эффекта «подсветки области» в обучающих интерфейсах.
