iphoneX和安卓全面屏手機的出現增加了游戲分辨率適配的難度
以橫版游戲為例,一般采用16:9的設計分辨率,匹配iphone5~iphone8以及幾乎全部安卓手機
對於iphone4/4s(3:2)和ipad(4:3)這種更方的分辨率,只需要使用FIXED_WIDTH模式,再處理頂部和底部多出來的顯示區域即可
常用的做法是把背景圖做大,頂部底部UI使用widget貼canvas邊
然而iphoneX和全面屏手機的分辨率都大於等於2:1,更加狹長的顯示區域
繼續使用FIXED_WIDTH模式會使界面元素進一步放大
橫向看來元素放大與屏幕長度增加成比例,與設計分辨率效果一致
縱向看來元素放大而屏幕高度沒變,因此必然會十分擁擠
因此項目中采取了兩種新方案處理iphoneX和全面屏的顯示
1.判斷屏幕高寬比,在大於16:9的設備上切換為SHOW_ALL模式
2.判斷屏幕高寬比,在大於16:9的設備上切換為FIXED_HEIGHT模式
下面結合源碼談一下這兩種模式的區別
轉載注明http://www.cnblogs.com/billyrun/articles/8082415.html
0.常用的幾個size比較

首先需要明確,編輯器/游戲場景中的canvas和頁面dom里的canvas實際上不是同一個東西
cc.director.getScene().children[0] == cc.game.canvas ❌結果是false
二者指的都是canvas,但前者場景canvas是引擎中的node節點,后者domCanvas是頁面dom節點
場景中的size
首先說最常用的cc.director.getWinSize()
cc.director.getWinSize() == 場景canvas.getContentSize() ✅結果是true
這個值是相對設計分辨率而言的相對設計大小
比如設計分辨率1136*768的場景,在任何分辨率16:9的瀏覽器內,這個winsize都是1136*768,盡管瀏覽器窗口實際大小各不相同
換句話說,winsize其實只關心高寬比例,最大限度讓游戲內坐標系與設備窗口無關
假設我用ipad全屏進入這個場景,相應的winsize會變成1136*852,因為定寬的情況下canvas縱向撐滿了屏幕
dom中的size

其次是cc.game.frame , cc.game.container , cc.game.canvas,這三個屬性都是dom節點,對應上圖1,2,3
cc.game.frame是整個頁面,如圖是被嵌套在iframe中的這個游戲子頁面
CCView中計算頁面大小/是否轉屏都是由cc.game.frame.clientHeight/clientWidth獲取頁面大小
(window.innerWidth/innerHeight其實與之對應是相同的)
cc.view._frameSize/cc.view.getFrameSize()記錄的就是這個大小
cc.game.container是包裝游戲的容器
轉屏時候該容器會改變css強轉90度
cc.game.canvas是游戲內容渲染容器
SHOW_ALL模式下可能會比頁面窗口小
定寬定高等其余情況,canvas會與頁面窗口一樣大
1.場景設置與顯示模式的對應關系
當勾選場景Canvas定高或定寬時即使用相應顯示模式
同時勾選定高和定寬,對應SHOW_ALL模式,顯示全部游戲內容,屏幕不足處留黑邊
同時不選定高和定寬,對應NO_BORDER模式,無黑邊,游戲內容可能會顯示不全
CCCanvas.applySettings: function () { var ResolutionPolicy = cc.ResolutionPolicy; var policy; if (this.fitHeight && this.fitWidth) { policy = ResolutionPolicy.SHOW_ALL; } else if (!this.fitHeight && !this.fitWidth) { policy = ResolutionPolicy.NO_BORDER; } else if (this.fitWidth) { policy = ResolutionPolicy.FIXED_WIDTH; } else { // fitHeight policy = ResolutionPolicy.FIXED_HEIGHT; } var designRes = this._designResolution; if (CC_EDITOR) { cc.engine.setDesignResolutionSize(designRes.width, designRes.height); } else { cc.view.setDesignResolutionSize(designRes.width, designRes.height, policy); } }
2.頁面適配流程
在游戲初始化的過程中會依次執行以下代碼
CCView構造函數中首先初始化瀏覽器信息,獲取頁面尺寸(或來自window.innerHeight/innerWidth,不同瀏覽器有別)
__BrowserGetter.init(this);
然后初始化frameSize,得到"外框"區域大小
_t._initFrameSize();
此后系統會設置設計分辨率
首先設置顯示模式setResolutionPolicy
此后是policy.apply(十分重要)
最后會保存winSize(相當於cc.director.setWinSize)並設置visibleRect
setDesignResolutionSize: function (width, height, resolutionPolicy) { // Defensive code ...... this.setResolutionPolicy(resolutionPolicy); var policy = this._resolutionPolicy; ...... this._originalDesignResolutionSize.width = this._designResolutionSize.width = width; this._originalDesignResolutionSize.height = this._designResolutionSize.height = height; var result = policy.apply(this, this._designResolutionSize); ...... // reset director's member variables to fit visible rect var director = cc.director; director._winSizeInPoints.width = this._designResolutionSize.width; director._winSizeInPoints.height = this._designResolutionSize.height; policy.postApply(this); cc.winSize.width = director._winSizeInPoints.width; cc.winSize.height = director._winSizeInPoints.height; ...... cc.visibleRect && cc.visibleRect.init(this._visibleRect); },
setResolutionPolicy: function (resolutionPolicy) { var _t = this; if (resolutionPolicy instanceof cc.ResolutionPolicy) { _t._resolutionPolicy = resolutionPolicy; } // Ensure compatibility with JSB else { var _locPolicy = cc.ResolutionPolicy; if(resolutionPolicy === _locPolicy.EXACT_FIT) _t._resolutionPolicy = _t._rpExactFit; if(resolutionPolicy === _locPolicy.SHOW_ALL) _t._resolutionPolicy = _t._rpShowAll; if(resolutionPolicy === _locPolicy.NO_BORDER) _t._resolutionPolicy = _t._rpNoBorder; if(resolutionPolicy === _locPolicy.FIXED_HEIGHT) _t._resolutionPolicy = _t._rpFixedHeight; if(resolutionPolicy === _locPolicy.FIXED_WIDTH) _t._resolutionPolicy = _t._rpFixedWidth; } },
對於不同適配策略
policy.apply使用不同邏輯
cc.ResolutionPolicy構造函數接收兩個參數分別對應_containerStrategy與_contentStrategy
_t._rpExactFit = new cc.ResolutionPolicy(_strategyer.EQUAL_TO_FRAME, _strategy.EXACT_FIT); _t._rpShowAll = new cc.ResolutionPolicy(_strategyer.PROPORTION_TO_FRAME, _strategy.SHOW_ALL); _t._rpNoBorder = new cc.ResolutionPolicy(_strategyer.EQUAL_TO_FRAME, _strategy.NO_BORDER); _t._rpFixedHeight = new cc.ResolutionPolicy(_strategyer.EQUAL_TO_FRAME, _strategy.FIXED_HEIGHT); _t._rpFixedWidth = new cc.ResolutionPolicy(_strategyer.EQUAL_TO_FRAME, _strategy.FIXED_WIDTH);
這里首先關心的是_containerStrategy
可以發現只有showAll對應PROPORTION_TO_FRAME
其余的模式都對應EQUAL_TO_FRAME
接下來具體來看兩者的區別
var EqualToFrame = cc.ContainerStrategy.extend({ apply: function (view) { var frameH = view._frameSize.height, containerStyle = cc.container.style; this._setupContainer(view, view._frameSize.width, view._frameSize.height); // Setup container's margin and padding if (view._isRotated) { containerStyle.margin = '0 0 0 ' + frameH + 'px'; } else { containerStyle.margin = '0px'; } containerStyle.padding = "0px"; } }); /** * @class ProportionalToFrame * @extends ContainerStrategy */ var ProportionalToFrame = cc.ContainerStrategy.extend({ apply: function (view, designedResolution) { var frameW = view._frameSize.width, frameH = view._frameSize.height, containerStyle = cc.container.style, designW = designedResolution.width, designH = designedResolution.height, scaleX = frameW / designW, scaleY = frameH / designH, containerW, containerH; scaleX < scaleY ? (containerW = frameW, containerH = designH * scaleX) : (containerW = designW * scaleY, containerH = frameH); // Adjust container size with integer value var offx = Math.round((frameW - containerW) / 2); var offy = Math.round((frameH - containerH) / 2); containerW = frameW - 2 * offx; containerH = frameH - 2 * offy; this._setupContainer(view, containerW, containerH); if (!CC_EDITOR) { // Setup container's margin and padding if (view._isRotated) { containerStyle.margin = '0 0 0 ' + frameH + 'px'; } else { containerStyle.margin = '0px'; } containerStyle.paddingLeft = offx + "px"; containerStyle.paddingRight = offx + "px"; containerStyle.paddingTop = offy + "px"; containerStyle.paddingBottom = offy + "px"; } } });
_setupContainer: function (view, w, h) { var locCanvas = cc.game.canvas, locContainer = cc.game.container; if (cc.sys.platform !== cc.sys.WECHAT_GAME) { if (cc.sys.os === cc.sys.OS_ANDROID) { document.body.style.width = (view._isRotated ? h : w) + 'px'; document.body.style.height = (view._isRotated ? w : h) + 'px'; } // Setup style locContainer.style.width = locCanvas.style.width = w + 'px'; locContainer.style.height = locCanvas.style.height = h + 'px'; } // Setup pixel ratio for retina display var devicePixelRatio = view._devicePixelRatio = 1; if (view.isRetinaEnabled()) devicePixelRatio = view._devicePixelRatio = Math.min(2, window.devicePixelRatio || 1); // Setup canvas locCanvas.width = w * devicePixelRatio; locCanvas.height = h * devicePixelRatio; cc._renderContext.resetCache && cc._renderContext.resetCache(); },
EqualToFrame中直接使用frameSize設置container
this._setupContainer(view, view._frameSize.width, view._frameSize.height);
ProportionalToFrame經過計算得到縮小后的size設置container
在_setupContainer中設置了game.container和game.canvas的dom元素size
這里把游戲邏輯與H5頁面元素結合到了一起
再回顧上文所述可以發現
只有SHOW_ALL一種模式下canvas不一定鋪滿屏幕(寬高比不一定同frame),其余模式canvas均鋪滿了屏幕(寬高比同frame)
而我們常用的WinSize始終等於場景的CanvasSize
3.總結
因此,回到最初的兩種解決方案
1.判斷屏幕高寬比,在大於16:9的設備上切換為SHOW_ALL模式
游戲內容16:9居中顯示,兩側留黑邊
游戲內坐標完全同設計分辨率
黑邊區域並不在游戲渲染范圍以內
想要裝飾黑邊需要使用dom元素
2.判斷屏幕高寬比,在大於16:9的設備上切換為FIXED_HEIGHT模式
游戲內容16:9居中顯示(widget組件靠邊的除外,按frame靠邊)
若無widget左右貼邊且背景圖剛好是設計分辨率大小,兩側會留黑邊
黑邊區域也包含在渲染區域內,如果游戲內背景圖夠大可以顯示
canvas變長變扁,坐標系會發生左右偏移
顯然方案1比較簡單
需要特別指出的時,按分辨率切換渲染模式時不能使用16:9作為閾值
因為1136/640!=16/9,只是近似相等!
ps.
在使用dom元素裝飾黑邊時,safari瀏覽器在移動設備轉屏時與chrome會有較大差異
以下圖為例
當設備翻轉時,若不改變設置邊框css,很可能出現這種情況
而safari瀏覽器中,右側未能顯示在屏幕中的邊框,也會計算頁面大小
即改變
window.innerWidth/innerHeight
cc.game.frame.clientWidth/clientHeight
(chrome瀏覽器貌似不會變!)
這個值的改變直接導致w/h是否大於1的改變,因此會使引擎cc.view._isRotated計算錯誤!
對於橫版游戲
原本橫屏時引擎設置cc.game.container.style.transform = 'rotate(0deg)'
豎屏時cc.game.container.style.transform = 'rotate(90deg)' , 即通過css強行將游戲轉向為橫屏
當以上cc.view._isRotated計算錯誤時可想而知 游戲的轉向也會出錯
解決方案(從簡便到徹底)
1.監聽orientationchange事件,在豎屏時不顯示邊框(display:none) , 這樣邊框即使位置不對也不會改變頁面大小
2.將邊框加入cc.game.container而不是加在body上,讓邊框跟着一起轉
3.徹底棄用SHOW_ALL+邊框的適配方式,改做定寬轉定高,在定高顯示方案中選用更寬的底圖,並手動處理一些橫向坐標偏移
