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

При разработке игр часто возникает задача вписать один элемент в границы другого, сохранив пропорции. Ручные расчёты соотношения сторон и позиционирования отнимают время и генерируют лишний код. Класс `Phaser.Structs.Size` предоставляет элегантное решение для декларативного описания размеров и их взаимосвязей. В этой статье разберем, как использовать режим `FIT` для автоматического масштабирования одного размера относительно другого, что особенно полезно для интерфейсов, камер и адаптивных игровых объектов.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    create ()
    {
        const debug = this.add.graphics();
        const text = this.add.text(10, 540, '', { fill: '#00ff00' });

        const parent = new Phaser.Structs.Size(400, 300);
        const child = new Phaser.Structs.Size(320, 190, Phaser.Structs.Size.FIT, parent);

        const draw = () =>
        {
            debug.clear().translateCanvas(10, 10);
            debug.lineStyle(1.5, 0xffff00).strokeRect(1, 1, parent.width, parent.height);
            debug.fillStyle(0x00ff00, 0.5).fillRect(1, 1, child.width, child.height);

            text.setText([
                `width: ${child.width}`,
                `height: ${child.height}`,
                `aspect ratio: ${child.aspectRatio}`
            ]);
        };

        this.input.on('pointermove', pointer =>
        {

            child.setSize(pointer.x, pointer.y);

            draw();

        });

        draw();
    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example
};

const game = new Phaser.Game(config);

Создание и связывание размеров

Класс Phaser.Structs.Size представляет собой контейнер для ширины, высоты и правил их изменения. В примере создаются два объекта: родительский и дочерний размер.

const parent = new Phaser.Structs.Size(400, 300);
const child = new Phaser.Structs.Size(320, 190, Phaser.Structs.Size.FIT, parent);

Первый аргумент конструктора — ширина, второй — высота. Для дочернего размера ключевыми являются третий и четвёртый параметры. Phaser.Structs.Size.FIT — это режим, который заставляет дочерний размер "вписаться" в границы родительского, сохраняя свои исходные пропорции (aspect ratio). Последний аргумент parent указывает, относительно какого размера происходит масштабирование. После создания объект child автоматически пересчитывает свои width и height, чтобы поместиться внутри parent, не вылезая за его границы.

Визуализация и обновление в реальном времени

Чтобы увидеть результат, в сцене создаётся графика для отладки (debug) и текстовое поле (text). Функция draw очищает холст, рисует рамку родительского размера и залитый прямоугольник дочернего.

const draw = () => {
    debug.clear().translateCanvas(10, 10);
    debug.lineStyle(1.5, 0xffff00).strokeRect(1, 1, parent.width, parent.height);
    debug.fillStyle(0x00ff00, 0.5).fillRect(1, 1, child.width, child.height);

    text.setText([
        `width: ${child.width}`,
        `height: ${child.height}`,
        `aspect ratio: ${child.aspectRatio}`
    ]);
};

Метод translateCanvas смещает начало координат отрисовки, чтобы визуализация не накладывалась на текст. Важно: мы рисуем прямоугольники, используя актуальные свойства child.width и child.height, которые уже содержат скорректированные под режим FIT значения.

Интерактивное изменение и пересчёт

Механика демонстрации завязана на движении указателя. При событии pointermove мы меняем исходный (желаемый) размер дочернего элемента и вызываем перерисовку.

this.input.on('pointermove', pointer => {
    child.setSize(pointer.x, pointer.y);
    draw();
});

Метод setSize обновляет внутренние цели дочернего размера. После этого вызова объект child автоматически применяет правило FIT относительно своего родителя parent. Если новые переданные ширину и высоту невозможно одновременно вписать в родительский контейнер с сохранением пропорций, будет выбран максимально возможный вариант. Например, если указать очень широкий размер, то ограничивающим фактором станет ширина родителя (400), а высота будет вычислена пропорционально.

Практическое применение в играх

Хотя пример рисует простые прямоугольники, технология полезна для реальных задач.

* **Адаптивный UI**: Создайте Size для контейнера интерфейса (например, панели навыков) и Size в режиме FIT для иконки внутри неё. Иконка всегда будет помещаться в панель, не искажаясь. * **Динамическая камера**: Определите Size для игровой области и Size в режиме FIT для области просмотра камеры. Это поможет автоматически рассчитывать zoom камеры, чтобы игровой уровень всегда был виден целиком. * **Обработка ресайза окна**: При событии изменения размера окна браузера обновите родительский Size (например, установите в него новые размеры игрового канваса). Все связанные дочерние размеры в режиме FIT пересчитаются автоматически.

// Пример: создание адаптивной текстуры для контейнера
const uiContainerSize = new Phaser.Structs.Size(300, 150);
const iconSize = new Phaser.Structs.Size(200, 100, Phaser.Structs.Size.FIT, uiContainerSize);

// Создаём спрайт с вычисленными размерами
this.add.sprite(x, y, 'icon').setDisplaySize(iconSize.width, iconSize.height);

Что попробовать дальше

Класс Phaser.Structs.Size с режимом FIT избавляет от рутинной математики при масштабировании, обеспечивая чистый и поддерживаемый код. Для экспериментов попробуйте заменить режим на Phaser.Structs.Size.ENVELOP (когда элемент должен полностью покрыть родительскую область) или привяжите изменение родительского размера к жесту pinch-zoom для создания интерактивного редактора макетов.