В этой статье я подробно расскажу как масштабировать вашу HTML5 игру, чтобы она всегда вписывалась в максимальный размер web страницы.
На самом деле весь этот функционал уже реализован в моём классе ScaleManager, который вы могли давно наблюдать в шаблоне проекта для Phaser на TypeScript. Поэтому я просто рассмотрю его подробно, чтобы всё было понятно.
Код класса ScaleManager
class ScaleManager {
private static game: Phaser.Game;
private static dom_id: string = '';
private static dom: HTMLElement;
private static isDesktop = false;
// game maximum size
private static game_w: number;
private static game_h: number;
// game save area size
private static game_sw: number;
private static game_sh: number;
// delta values container position from left and top of the page
public static dtx: number = 0;
public static dty: number = 0;
// true current game view size
public static gameViewW: number;
public static gameViewH: number;
// orientation
public static isPortrait: boolean;
public static onOrientationChange: Phaser.Signal = new Phaser.Signal(); // orientation change event
public static init(aGame: Phaser.Game, aDomId: string, GW: number, GH: number, GSW: number, GSH: number) {
this.game = aGame;
this.dom_id = aDomId;
this.dom = document.getElementById(this.dom_id);
this.game_w = GW;
this.game_h = GH;
this.game_sw = GSW;
this.game_sh = GSH;
aGame.scale.scaleMode = Phaser.ScaleManager.USER_SCALE;
this.isDesktop = this.game.device.desktop;
// TEST
//this.isDesktop = false;
ScaleManager.SizeCalculation();
window.onresize = () => {
ScaleManager.SizeCalculation();
};
}
private static doEventOriChange() {
this.onOrientationChange.dispatch(this.isPortrait);
}
public static SizeCalculation() {
var wnd = {
w: window.innerWidth,
h: window.innerHeight
};
// orientation
var oldOri = this.isPortrait;
this.isPortrait = wnd.h > wnd.w;
// determine game size
var g = {
w: ScaleManager.game_w,
h: ScaleManager.game_h,
sw: ScaleManager.game_sw,
sh: ScaleManager.game_sh
}
var gw: number;
var gh: number;
if (g.h / g.w > wnd.h / wnd.w) {
// game compressed by HEIGHT, black borders on the LEFT and the RIGHT
if (g.sh / g.w > wnd.h / wnd.w) {
// A
gh = wnd.h * g.h / g.sh;
gw = gh * g.w / g.h;
} else {
// B
gw = wnd.w;
gh = gw * g.h / g.w;
}
} else {
// game compressed by WIDTH, black borders on the TOP and the BOT
if (g.h / g.sw > wnd.h / wnd.w) {
// C
gh = wnd.h;
gw = gh * g.w / g.h;
} else {
// D
gw = wnd.w * g.w / g.sw;
gh = gw * g.h / g.w;
}
}
// game scale
var scale_x = gw / g.w;
var scale_y = gh / g.h;
var newScale = Math.min(scale_x, scale_y);
ScaleManager.game.scale.setUserScale(newScale, newScale, 0, 0);
// game dt xy
this.dtx = (wnd.w - gw) / 2;
this.dty = (wnd.h - gh) / 2;
this.gameViewW = this.game_w + 2 * this.dtx / newScale;
if (this.gameViewW > this.game_w) this.gameViewW = this.game_w;
this.gameViewH = this.game_h + 2 * this.dty / newScale;
if (this.gameViewH > this.game_h) this.gameViewH = this.game_h;
// game div position
this.dom.style.marginLeft = Math.round(this.dtx).toString() + 'px';
if (!this.isDesktop && this.isPortrait) {
this.dom.style.marginTop = '0px';
}
else {
this.dom.style.marginTop = Math.round(this.dty).toString() + 'px';
}
this.dom.style.maxWidth = String(gw) + 'px';
this.dom.style.maxHeight = String(gh) + 'px';
ScaleManager.game.scale.refresh();
// ROTATION ICON UPDATE
this.updateRotationIcon();
if (this.isPortrait != oldOri) {
this.doEventOriChange();
}
}
private static updateRotationIcon() {
var MAX_PERC = 24;
if (!this.isDesktop) {
if (this.isPortrait) {
this.showRotateIcon();
}
else {
this.hideRotateIcon();
return;
}
var wnd = {
w: window.innerWidth,
h: window.innerHeight
};
var rp_div = document.getElementById("rp-div");
var rp_img = document.getElementById("rp-img");
var com_h = this.dom.clientHeight + rp_div.clientHeight;
var perc = MAX_PERC;
if (rp_img.style.height != null && rp_img.style.height != undefined && rp_img.style.height != '') {
if (rp_img.style.height.indexOf('%') > 0)
perc = Number(rp_img.style.height.split('%')[0]);
//LogMng.debug('perc = ' + perc);
}
if (com_h > wnd.h) {
while (com_h > wnd.h) {
perc--;
rp_img.style.width = rp_img.style.height = String(perc) + '%';
com_h = this.dom.clientHeight + rp_div.clientHeight;
}
}
else {
while (perc < MAX_PERC && com_h < wnd.h - 10) {
perc++;
rp_img.style.width = rp_img.style.height = String(perc) + '%';
com_h = this.dom.clientHeight + rp_div.clientHeight;
}
}
var bot_h = wnd.h - this.dom.clientHeight;
rp_div.style.paddingTop = String((bot_h - rp_img.clientHeight) / 2) + 'px';
}
}
private static showRotateIcon() {
document.getElementById("rp-div").style.display = "block";
//ScaleManager.game.world.isPaused = true;
}
private static hideRotateIcon() {
document.getElementById("rp-div").style.display = "none";
//ScaleManager.game.world.isPaused = false;
}
}
Суть этого класса в том, что он располагает и масштабирует вашу игру так, чтобы она всегда была по центру страницы и отображалась желаемая рабочая область (SafeArea) игры.
Работает класс автономно, т.к. после инициализации сам подписывается на событие window.onresize. По этому событию выполняется функция ScaleManager.SizeCalculation() в которой и происходит расчёт всех размеров и событий относительно текущих размеров страницы.
Для использования класса его нужно просто инициализировать и лучше это сделать как можно раньше, перед любой значимой сценой, чтобы любая видимая часть игры уже правильно отмасштабировалась, в моём примере это делается в самой первой сцене Boot:
Код класса Boot
module PhaserGame.Client {
export class Boot extends Phaser.State {
preload() {
this.load.atlasJSONArray('loading', './assets/atlases/loading.png', './assets/atlases/loading.json');
}
create() {
this.stage.setBackgroundColor(0x6600CC);
this.input.maxPointers = 1;
// если true то игра старается не останавливаться теряя фокус
this.stage.disableVisibilityChange = true;
// SCALE MNG
ScaleManager.init(this.game, Config.DOM_PARENT_ID, Config.GW, Config.GH, Config.GSW, Config.GSH);
// LogMng settings
if (isRelease()) LogMng.setMode(LogMng.MODE_RELEASE);
LogMng.system('current log mode: ' + LogMng.getMode());
// FILL PARAMS
Params.isTapToStartBtn = //true || // uncomment for testing
this.game.device.iOS ||
this.game.device.iPhone ||
this.game.device.iPhone4 ||
this.game.device.iPad ||
this.game.device.mobileSafari;
// little pause
this.time.events.add(100, this.onWaitComplete, this);
}
onWaitComplete() {
this.game.state.start(States.PRELOADER, true, false);
}
}
}
В функцию ScaleManager.init(…) нужно передать следующие параметры:
ссылку на текущий класс игры
id DOM элемента, в котором расположена игра, в данном случае это div-элемент HTML страницы с игрой, который имеет id = «game»
максимальное разрешение игры GW и GH и разрешение минимальной рабочей области игры (GSW и GSH), так называемая SafeArea область.
SafeArea — это минимальная рабочая область игры, которая не должна обрезаться (выходить за края экрана), область которую игрок должен всегда видеть на любых разрешениях экрана.
Для наглядности рассмотрим пример: наша игровая сцена должна корректно отображаться и на экранах iPad-ов и на экранах длинных телефонов. Максимальное соотношение сторон iPad-а = 4:3, максимальное сплюснутое разрешения телефонов бывает 16:9, учитывая что сферху место будет занимать полоска браузера (которая отображает адрес страницы и т.п.), то будет почти 2:1. Допустим мы определились с разрешением текстур для игры и делаем их под разрешение 1024:768 — это будет нашим максимальным отображаемым разрешением, а минимальную необходимую область определили разрешением в 800:400.
Я специально подготовил прототип игровой сцены с разметкой этих областей:
Т.е. игра на разных устройствах должна выглядить следующим образом:
Я думаю смысл SafeArea понятен — эта область должна всегда быть на экране и если экран позволяет, то показывается MaximumArea.
Теперь рассмотрим случаи, которые описаны в коде под буквами A, B, C, D.
А вот живая демонстрация данного скейлинга, откройте и изменяйте размеры окна браузера чтобы понаблюдать динамку масштабирования, а так же можно открыть на любом устройстве. Вот видео данной демонстрации:
Так же у меня здесь (в шаблоне) частично прикручена функция отображения иконки «разверните устройство», если на мобильных устройствах повернулись в портретный режим. Это можно видеть на видео и на мобильных устройствах. Но она пока что работает только для горизонтальных ориентаций. И это уже другая тема, которую я раскрою в последующей статье, когда допишу опциональный функционал для обоих ориентаций.