О чем этот пример
Создание игр для множества устройств — это всегда компромисс между сохранением задуманного вида и адаптацией под любые размеры экрана. Пример с двумя сценами демонстрирует мощный паттерн для разделения логики масштабирования и игрового контента. Вы научитесь фиксировать соотношение сторон у основного игрового мира, в то время как фон и UI могут растягиваться на весь браузер, создавая бесшовный и профессиональный вид на любом разрешении.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
// This Scene has no aspect ratio lock, it will scale to fit the browser window, but will zoom to match the Game
class BackgroundScene extends Phaser.Scene
{
gameScene;
layer;
constructor ()
{
super('BackgroundScene');
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('logo', 'assets/sprites/phaser3-logo-x2.png');
this.load.image('fakegame', 'assets/pics/ninja-masters2.png');
}
create ()
{
this.layer = this.add.container();
const bg = this.add.image(0, 0, 'fakegame');
this.layer.add(bg);
// We launch this Scene first because we can't use `getZoom` otherwise
this.scene.launch('GameScene');
this.gameScene = this.scene.get('GameScene');
}
updateCamera ()
{
const width = this.scale.gameSize.width;
const height = this.scale.gameSize.height;
const camera = this.cameras.main;
camera.setViewport(0, 0, width, height);
this.layer.setPosition(width / 2, height / 2);
this.layer.setScale(this.gameScene.getZoom());
}
resize ()
{
this.updateCamera();
}
}
// This Scene is aspect ratio locked at 640 x 960 (and scaled and centered accordingly)
class GameScene extends Phaser.Scene
{
backgroundScene;
parent;
sizer;
constructor ()
{
super('GameScene');
}
create ()
{
const width = this.scale.gameSize.width;
const height = this.scale.gameSize.height;
this.parent = new Phaser.Structs.Size(width, height);
this.sizer = new Phaser.Structs.Size(640, 960, Phaser.Structs.Size.HEIGHT_CONTROLS_WIDTH, this.parent);
this.parent.setSize(width, height);
this.sizer.setSize(width, height);
this.backgroundScene = this.scene.get('BackgroundScene');
this.updateCamera();
this.scale.on('resize', this.resize, this);
// Normal game stuff from here on down
this.add.image(640 / 2, 960 / 2, 'fakegame');
}
updateCamera ()
{
const camera = this.cameras.main;
const x = Math.ceil((this.parent.width - this.sizer.width) * 0.5);
const y = 0;
const scaleX = this.sizer.width / 640;
const scaleY = this.sizer.height / 960;
camera.setViewport(x, y, this.sizer.width, this.sizer.height);
camera.setZoom(Math.max(scaleX, scaleY));
camera.centerOn(320, 480);
this.backgroundScene.updateCamera();
}
getZoom ()
{
return this.cameras.main.zoom;
}
resize (gameSize, baseSize, displaySize, resolution)
{
const width = gameSize.width;
const height = gameSize.height;
this.parent.setSize(width, height);
this.sizer.setSize(width, height);
this.updateCamera();
}
}
const config = {
type: Phaser.AUTO,
backgroundColor: '#000000',
scale: {
mode: Phaser.Scale.RESIZE,
parent: 'phaser-example',
width: 640,
height: 960,
min: {
width: 320,
height: 480
},
max: {
width: 1920,
height: 1400
}
},
scene: [ BackgroundScene, GameScene ]
};
const game = new Phaser.Game(config);
Разделение ответственности: Зачем две сцены?
Ключевая идея примера — разделение. BackgroundScene отвечает за фон, который должен заполнять всё окно браузера без учёта соотношения сторон. GameScene содержит основной игровой мир с фиксированным соотношением сторон (640x960).
Это позволяет имитировать популярный в мобильных играх эффект, когда игровая область сохраняет пропорции и центрируется, а фон и интерфейс вокруг неё плавно заполняют оставшееся пространство. Обе сцены запускаются одновременно и синхронизируют свои камеры через вызовы методов.
// Запуск GameScene из BackgroundScene после создания
this.scene.launch('GameScene');
this.gameScene = this.scene.get('GameScene');
// Получение ссылки на BackgroundScene из GameScene
this.backgroundScene = this.scene.get('BackgroundScene');
Ядро масштабирования: Использование Phaser.Structs.Size
В GameScene для расчётов используется утилита Phaser.Structs.Size. Она автоматически вычисляет размеры области отображения, сохраняя заданное соотношение сторон и вписываясь в доступное пространство.
Мы создаём два объекта: parent (размер окна браузера) и sizer (размер нашей игровой области, 640x960). Флаг Phaser.Structs.Size.HEIGHT_CONTROLS_WIDTH указывает, что высота является ведущим измерением для расчётов — область будет масштабироваться, сохраняя пропорции, по высоте окна.
this.parent = new Phaser.Structs.Size(width, height);
this.sizer = new Phaser.Structs.Size(640, 960, Phaser.Structs.Size.HEIGHT_CONTROLS_WIDTH, this.parent);
При каждом ресайзе мы обновляем размеры обоих объектов, и sizer автоматически пересчитывает свои width и height, оставаясь вписанным в новый parent.
this.parent.setSize(width, height);
this.sizer.setSize(width, height);
Синхронизация камер: updateCamera в GameScene
Метод updateCamera() в GameScene — сердце логики отображения. Он использует рассчитанные sizer значения, чтобы позиционировать и масштабировать основную камеру.
1. **Вьюпорт**: Камере задаётся область отрисовки (setViewport), которая равна рассчитанным размерам sizer и смещена для центрирования по горизонтали.
2. **Зум**: Масштаб камеры (zoom) вычисляется как максимальное соотношение между целевым и реальным размером. Это гарантирует, что игровая область 640x960 всегда будет полностью помещаться в выделенный для неё sizer, возможны чёрные полосы (letterbox).
3. **Центрирование**: Камера центрируется на середине игрового мира.
const x = Math.ceil((this.parent.width - this.sizer.width) * 0.5);
const y = 0;
const scaleX = this.sizer.width / 640;
const scaleY = this.sizer.height / 960;
camera.setViewport(x, y, this.sizer.width, this.sizer.height);
camera.setZoom(Math.max(scaleX, scaleY));
camera.centerOn(320, 480);
После настройки своей камеры, GameScene вызывает updateCamera() у BackgroundScene, передавая эстафету.
Реакция на ресайз: Обработчик события scale.resize
Чтобы игра реагировала на изменение размера окна, в GameScene подписываемся на событие resize от менеджера масштабирования this.scale. В конфигурации игры используется Phaser.Scale.RESIZE, которое делает canvas резиновым.
Обработчик resize обновляет размеры parent и sizer, а затем вызывает updateCamera(), которая пересчитывает вьюпорты и зум для обеих сцен.
this.scale.on('resize', this.resize, this);
// ...
resize (gameSize, baseSize, displaySize, resolution) {
const width = gameSize.width;
const height = gameSize.height;
this.parent.setSize(width, height);
this.sizer.setSize(width, height);
this.updateCamera();
}
Важно: BackgroundScene также имеет метод resize, который вызывается автоматически системой Phaser для каждой сцены при срабатывании глобального события. Он просто делегирует логику своему updateCamera().
Фон на весь экран: updateCamera в BackgroundScene
Логика фона проще. Его камера всегда занимает весь экран (setViewport(0, 0, width, height)). Контейнер this.layer, содержащий фоновое изображение, позиционируется в центр экрана.
Ключевой момент — его масштаб. Он запрашивает текущий зум у GameScene через метод getZoom() и применяет его к себе. Это заставляет фон масштабироваться в точности так же, как и основная игровая камера, создавая иллюзию единого целого, даже если игровая область меньше окна.
updateCamera () {
const width = this.scale.gameSize.width;
const height = this.scale.gameSize.height;
const camera = this.cameras.main;
camera.setViewport(0, 0, width, height);
this.layer.setPosition(width / 2, height / 2);
this.layer.setScale(this.gameScene.getZoom()); // Синхронизация зума!
}
Что попробовать дальше
Этот паттерн с двумя сценами — отличная основа для сложной системы отзывчивого интерфейса. Вы можете расширить его: добавить третью сцену для HUD-элементов (здоровье, очки), которые будут привязаны к краям экрана, а не к игровому миру. Поэкспериментируйте с другими флагами Phaser.Structs.Size, например WIDTH_CONTROLS_HEIGHT, чтобы игровая область масштабировалась по ширине. Также попробуйте менять метод расчёта зума в GameScene на Math.min(scaleX, scaleY) — это включит режим "обрезки" (crop), когда игровая область будет заполнять весь sizer, но часть контента может уйти за края.
