О чем этот пример

В мобильных и десктопных играх часто требуется режим `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. Для экспериментов попробуйте

  1. изменить форму маски, рисуя на холсте круги или сложные path
  2. анимировать саму маску, изменяя ее текстуру в реальном времени
  3. применить этот подход не к слою, а к контейнеру (Phaser.GameObjects.Container) или группе спрайтов