О чем этот пример
При работе с масштабированием игры в Phaser 3 можно столкнуться с неочевидным поведением графических масок. В частности, при использовании режима `ScaleModes.RESIZE` маска, созданная из спрайта, может не изменять свой размер вслед за игровым холстом, что приводит к визуальным артефактам. Эта статья объясняет причину этой проблемы и демонстрирует практический подход к её решению, который будет полезен разработчикам, создающим адаптивные интерфейсы или эффекты, зависящие от размера экрана.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Demo extends Phaser.Scene
{
constructor ()
{
super('demo');
}
preload ()
{
// this.load.setBaseURL('https://cdn.phaserfiles.com/v385');
this.load.image('logo', 'https://raw.githubusercontent.com/photonstorm/phaser3-typescript-project-template/master/dist/assets/phaser3-logo.png');
this.load.image('libs', 'https://raw.githubusercontent.com/photonstorm/phaser3-typescript-project-template/master/dist/assets/libs.png');
this.load.image('mask', 'https://raw.githubusercontent.com/photonstorm/phaser3-examples/master/public/assets/sprites/mask1.png');
}
create ()
{
const libImage = this.add.image(400, 300, 'libs');
const logo = this.add.image(400, 300, 'logo');
let maskSprite = this.make.sprite({ x: 400, y: 300, key: 'mask', add: false });
let bitmapMask = new Phaser.Display.Masks.BitmapMask(this, maskSprite);
logo.setMask(bitmapMask);
}
}
const config = {
type: Phaser.WEBGL,
backgroundColor: '#125555',
width: 800,
height: 600,
pixelArt: true,
scale: {
mode: Phaser.Scale.ScaleModes.RESIZE,
},
scene: Demo
};
const game = new Phaser.Game(config);
В чём суть проблемы?
В исходном примере используется конфигурация масштабирования ScaleModes.RESIZE. Этот режим автоматически меняет размер внутреннего игрового холста (canvas) при изменении размеров окна браузера. Однако, спрайт, который используется в качестве источника для BitmapMask, создаётся один раз в начале работы сцены и не привязан к системе автоматического масштабирования.
Когда игровой холст увеличивается или уменьшается, позиция и размер маски остаются прежними, что приводит к рассинхронизации. Маска продолжает отображаться в своих исходных координатах и с исходными размерами, в то время как остальной контент (например, спрайт логотипа) масштабируется корректно.
Анализ кода инициализации
Давайте разберём ключевые моменты создания маски в методе create(). Основная проблема кроется в создании спрайта-источника для маски.
let maskSprite = this.make.sprite({ x: 400, y: 300, key: 'mask', add: false });
let bitmapMask = new Phaser.Display.Masks.BitmapMask(this, maskSprite);
logo.setMask(bitmapMask);
Здесь maskSprite — это обычный спрайт, созданный через фабрику this.make.sprite. Параметр add: false указывает, что спрайт не будет автоматически добавлен на дисплей сцены, но он всё равно существует как текстура в памяти. Его исходные координаты (400, 300) и размеры (равные размеру загруженного изображения 'mask') фиксированы. Этот спрайт не является частью игрового мира, которая реагирует на системные события изменения размера.
Решение: динамическое обновление маски
Чтобы маска корректно масштабировалась, необходимо обновлять свойства её источника (maskSprite) при изменении размеров игрового окна. В Phaser 3 для этого можно использовать событие resize, которое генерируется объектом Scale.ScaleManager.
Первый шаг — сохранить ссылки на маску и её спрайт в области видимости сцены, чтобы иметь к ним доступ в обработчике события.
create() {
const libImage = this.add.image(400, 300, 'libs');
const logo = this.add.image(400, 300, 'logo');
// Сохраняем спрайт маски в свойстве сцены
this.maskSprite = this.make.sprite({ x: 400, y: 300, key: 'mask', add: false });
this.bitmapMask = new Phaser.Display.Masks.BitmapMask(this, this.maskSprite);
logo.setMask(this.bitmapMask);
// Подписываемся на событие изменения размера
this.scale.on('resize', this.resizeMask, this);
}
Теперь нужно реализовать метод resizeMask, который будет вычислять новые параметры для спрайта маски, основываясь на текущих размерах игры.
Реализация обработчика resize
В обработчике события мы должны пересчитать позицию и размер спрайта-маски. Важно понимать, что BitmapMask использует текстуру спрайта как есть. Поэтому мы можем напрямую менять свойства `x,y,scaleXиscaleY` сохранённого спрайта.
resizeMask(gameSize) {
// gameSize содержит новые ширину и высоту (gameSize.width, gameSize.height)
const width = gameSize.width;
const height = gameSize.height;
// Перемещаем центр спрайта маски в центр нового игрового поля
this.maskSprite.x = width / 2;
this.maskSprite.y = height / 2;
// Рассчитываем масштаб. Пример: подгоняем маску под 80% от новой высоты.
// Коэффициент масштабирования = (желаемый размер) / (исходный размер текстуры).
const targetHeight = height * 0.8;
const scaleFactor = targetHeight / this.maskSprite.height;
this.maskSprite.setScale(scaleFactor);
}
Этот метод будет вызываться каждый раз, когда пользователь меняет размер окна браузера. В результате маска всегда будет центрирована и пропорционально масштабирована относительно игровой области.
Важные нюансы и оптимизация
1. **Производительность:** Частый вызов setScale и перерисовка маски могут быть ресурсоёмкими. Стоит добавить проверку на значительное изменение размера (дебаунс) или ограничить частоту обновления, если это критично для вашей игры.
2. **Исходные размеры:** Для корректного расчёта масштаба необходимо знать исходную высоту (this.maskSprite.height) и ширину спрайта-маски. Эти свойства доступны только после полной загрузки текстуры, что гарантировано в методе create.
3. **Другие режимы масштабирования:** Проблема актуальна в основном для RESIZE. В режимах FIT или ENVELOP логика расчёта нового размера и позиции маски может быть сложнее, так как меняется не только размер холста, но и область отображения (camera или zoom).
4. **Очистка:** Не забудьте отписаться от события при перезапуске или уничтожении сцены, чтобы избежать утечек памяти.
this.scale.off('resize', this.resizeMask, this);
Что попробовать дальше
Использование ScaleModes.RESIZE требует от разработчика ручного управления элементами, которые должны реагировать на изменение размеров окна. Маски, созданные из спрайтов, — один из таких элементов. Реализовав простой обработчик события resize, вы получаете полный контроль над размером и положением маски, что открывает возможности для создания сложных адаптивных визуальных эффектов.
**Идеи для экспериментов:** Попробуйте привязать масштаб маски не к размеру окна, а к позиции камеры или курсора мыши. Или создайте анимированную маску, размер которой плавно меняется по таймеру или в ответ на игровые события, используя тот же принцип динамического обновления её свойств.
