3. Простые задачи адаптации описываются декларативно
Если элемент нужно лишь центрировать, расположить у правого края или растянуть как фон, я не пишу отдельный TypeScript-компонент.
В движке уже есть компоненты вёрстки:
ResponsiveNodePositionComponentResponsiveNodeSizeComponentFitStackLayoutComponent
Например, кнопка настроек всегда находится у правого края:
{
"components": {
"responsivePosition": {
"type": "ResponsiveNodePositionComponent",
"enabled": true,
"data": {
"mode": "screen",
"position": {
"right": 10
}
}
}
}
}
Это описание взято из game/assets/json/layouts/features/settings/_button.json.
Индикатор загрузки центрируется:
{
"type": "ResponsiveNodePositionComponent",
"enabled": true,
"data": {
"mode": "screen",
"position": {
"cx": 0,
"cy": 0
}
}
}
Таким случаям место в JSON-вёрстке. Это проще и позволяет сосредоточить код исполнения на его задачах.
4. Заполнение фона — часть игровой вёрстки
В нескольких сценах фон состоит из gradient и pattern. Он должен заполнять весь виртуальный экран.
Пример из game/assets/json/layouts/scenes/item/layout.json:
{
"responsiveNodeSize": {
"type": "ResponsiveNodeSizeComponent",
"enabled": true,
"data": {
"mode": "fill_width_height_ratio",
"minScale": 1,
"maxScale": 1,
"fill": {
"resizeTargets": ["gradient", "pattern"],
"widthPercent": 1,
"heightRatio": 1,
"minHeightPx": 1,
"maxHeightPx": 5000,
"horizontalAlign": "left",
"verticalAlign": "top"
}
}
}
}
Это не DOM-фон. Он является частью вёрстки сцены и использует ту же систему координат, что и остальные игровые элементы.
5. Равномерное масштабирование сцены заставки
В загрузочной сцене есть вступительная анимация с наклейками и логотипом. Ей не нужна отдельная композиция для каждой ориентации. Достаточно центрирования и равномерного масштабирования.
Пример из game/assets/json/layouts/scenes/boot/layout.json:
{
"responsivePosition": {
"type": "ResponsiveNodePositionComponent",
"enabled": true,
"data": {
"mode": "screen",
"position": {
"cx": 0,
"cy": 0
}
}
},
"responsiveSize": {
"type": "ResponsiveNodeSizeComponent",
"enabled": true,
"data": {
"mode": "uniform_scale",
"minScale": 0.68,
"maxScale": 1,
"uniform": {
"axis": "min",
"referenceScreenWidthPx": 720,
"referenceScreenHeightPx": 1280
}
}
}
}
axis: "min" означает, что масштаб определяется наиболее ограничивающей осью.
Этот подход подходит для:
- логотипов;
- групп вступительной анимации;
- декоративных групп;
- небольших блоков интерфейса, которым не требуется менять композицию.
6. Вписывание всплывающих окон по реальным границам
Для интерфейса всплывающих окон я использую fit_bounds.
Реальный размер окна зависит от содержимого: фона, надписей, кнопок и отступов. Если масштабировать его только относительно эталонной ширины, на узких экранах окно может обрезаться.
Окно настроек:
{
"responsiveSize": {
"type": "ResponsiveNodeSizeComponent",
"enabled": true,
"data": {
"mode": "fit_bounds",
"minScale": 0.25,
"maxScale": 3,
"fit": {
"primaryAxisByOrientation": {
"port": "width",
"land": "height"
},
"widthPercent": 0.9,
"heightPercent": 0.9,
"topPaddingPx": 12,
"bottomPaddingPx": 12,
"leftPaddingPx": 12,
"rightPaddingPx": 12,
"alignX": "center",
"alignY": "center",
"offsetYPx": 10
}
}
}
}
В вертикальной ориентации окно вписывается по ширине, в горизонтальной — по высоте. Это точнее одного универсального правила масштабирования.
Внутри движка fit_bounds измеряет фактическое поддерево:
const bounds = targetNode.view.getLocalBounds();
const availableWidth = widthBeforePadding - fit.leftPaddingPx - fit.rightPaddingPx;
const availableHeight = heightBeforePadding - fit.topPaddingPx - fit.bottomPaddingPx;
const widthFitScale = availableWidth / bounds.width;
const heightFitScale = availableHeight / bounds.height;
const scale = this.clampScale(fitScale, config.minScale, config.maxScale);
Этот код находится в engine/layouts/components/ResponsiveNodeSizeComponent.ts.
Важная деталь: компонент использует реальные отрисованные границы, а не предполагаемый размер.