О чем этот пример
В мобильных и десктопных играх часто требуется режим `Phaser.Scale.RESIZE`, который подстраивает игровое поле под размер окна браузера. Однако при использовании стандартных битмасок (`BitmapMask`) возникает проблема: маска остаётся статичной и не масштабируется вместе с игрой. Это приводит к визуальным артефактам — контент может обрезаться не там, где нужно. В этой статье мы разберем практическое решение по созданию динамической маски, которая корректно работает при любом изменении размеров окна или экрана.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Demo extends Phaser.Scene {
constructor() {
super({
key: 'examples'
})
}
preload() {
}
create() {
var layer = this.add.layer();
for (var i = 0; i < 8; i++) {
var gameObject = this.add.rectangle(0, 0, 1, 1, 0x888888)
.setStrokeStyle(2, 0xFFFFFF)
.setOrigin(0)
layer.add(gameObject);
}
this.myLayer = layer;
CreateCanvasTexture(this, 'mask');
var maskImage = this.add.image(0, 0, 'mask').setOrigin(0);
layer.setMask(maskImage.createBitmapMask());
}
update(time, delta) {
ResizeGameObjects(this.myLayer.getAll());
}
}
var CreateCanvasTexture = function (scene, key) {
var texture = scene.textures.createCanvas('mask', 2048, 2048);
var canvas = texture.getCanvas();
var context = texture.getContext();
context.clearRect(0, 0, canvas.width, canvas.height);
context.rect(0, 0, canvas.width, canvas.height);
context.fillStyle = 'rgba(255,0,0,0.2)';
context.fill();
texture.refresh();
}
var ResizeGameObjects = function (gameObjects) {
var scene = gameObjects[0].scene;
var { width, height } = scene.scale.displaySize;
if ((scene.savedWidth === width) && (scene.savedHeight === height)) {
return;
}
scene.savedWidth = width;
scene.savedHeight = height;
var cellWidth = width / gameObjects.length;
gameObjects.forEach(function (gameObject, i) {
gameObject.setSize(cellWidth - 2, cellWidth - 2)
gameObject.setPosition(i * cellWidth, 0);
})
}
var config = {
type: Phaser.AUTO,
parent: 'phaser-example',
width: 800,
height: 600,
scale: {
mode: Phaser.Scale.RESIZE,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
scene: [Demo]
};
var game = new Phaser.Game(config);
Проблема статичной маски
Битмаска в Phaser создается из текстуры (Phaser.Textures.Texture) и привязывается к конкретным пиксельным координатам. Когда вы включаете режим RESIZE, игровой канвас и все его внутренние системы координат автоматически пересчитываются, но текстура маски остается неизменной. В результате область отсечения перестает соответствовать новым границам экрана.
Рассмотрим типичный сценарий: у вас есть слой (Phaser.GameObjects.Layer) с несколькими объектами, и вы применяете к нему маску. При растягивании окна объекты должны перераспределиться, но маска останется прежнего размера, обрезая лишнее или показывая скрытые области.
Создание динамической текстуры для маски
Ключ к решению — создание маски из Canvas-текстуры, размер которой мы можем контролировать. В примере функция CreateCanvasTexture создает такую текстуру под ключом 'mask'.
var CreateCanvasTexture = function (scene, key) {
var texture = scene.textures.createCanvas('mask', 2048, 2048);
var canvas = texture.getCanvas();
var context = texture.getContext();
context.clearRect(0, 0, canvas.width, canvas.height);
context.rect(0, 0, canvas.width, canvas.height);
context.fillStyle = 'rgba(255,0,0,0.2)';
context.fill();
texture.refresh();
}
Здесь scene.textures.createCanvas('mask', 2048, 2048) создает текстуру-холст с большим разрешением (2048x2048). Это буфер, который гарантирует, что маска покроет любые возможные размеры окна после ресайза. Цвет заливки rgba(255,0,0,0.2) задает полупрозрачную красную область — визуально это будет видно только при отладке. Важный вызов texture.refresh() обновляет текстуру на GPU после рисования.
Затем в сцене мы создаем изображение из этой текстуры и превращаем его в битмаску для слоя:
var maskImage = this.add.image(0, 0, 'mask').setOrigin(0);
layer.setMask(maskImage.createBitmapMask());
Логика перерасчета размеров объектов
Чтобы объекты внутри замаскированного слоя корректно заполняли доступное пространство, нам нужно пересчитывать их размеры и позиции при каждом изменении размеров окна. Эта логика вынесена в функцию ResizeGameObjects, которая вызывается в update.
var ResizeGameObjects = function (gameObjects) {
var scene = gameObjects[0].scene;
var { width, height } = scene.scale.displaySize;
if ((scene.savedWidth === width) && (scene.savedHeight === height)) {
return;
}
scene.savedWidth = width;
scene.savedHeight = height;
var cellWidth = width / gameObjects.length;
gameObjects.forEach(function (gameObject, i) {
gameObject.setSize(cellWidth - 2, cellWidth - 2)
gameObject.setPosition(i * cellWidth, 0);
})
}
Функция берет текущие размеры дисплея из scene.scale.displaySize. Проверка if ((scene.savedWidth === width) && (scene.savedHeight === height)) предотвращает лишние пересчеты, если размер не изменился. Затем вычисляется ширина ячейки (cellWidth) на основе общей ширины и количества объектов. Каждый объект (в примере это прямоугольники Phaser.GameObjects.Rectangle) получает новый размер через setSize и позицию через setPosition. Отступ -2 добавлен для визуального разделения прямоугольников (чтобы были видны их белые границы).
Интеграция в игровой цикл
Вызов функции перерасчета в методе update сцены обеспечивает мгновенную реакцию на изменение размера окна.
update(time, delta) {
ResizeGameObjects(this.myLayer.getAll());
}
Метод layer.getAll() возвращает все дочерние игровые объекты слоя. Поскольку update выполняется каждый кадр, проверка на изменение размера внутри ResizeGameObjects критически важна для производительности.
Обратите внимание на конфигурацию масштабирования игры:
scale: {
mode: Phaser.Scale.RESIZE,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
Режим RESIZE заставляет игровой канвас подстраиваться под размер контейнера (окна браузера), а CENTER_BOTH центрирует игровую область.
Что попробовать дальше
Использование Canvas-текстуры в качестве основы для маски и перерасчет геометрии объектов в update — надежный способ создать адаптивный интерфейс или эффекты обрезки в Phaser при работе с Scale.RESIZE. Для экспериментов попробуйте
- изменить форму маски, рисуя на холсте круги или сложные path
- анимировать саму маску, изменяя ее текстуру в реальном времени
- применить этот подход не к слою, а к контейнеру (
Phaser.GameObjects.Container) или группе спрайтов
