對於移動端的輕量級 HTML5 互動小遊戲(簡稱為 H5 輕互動),如果從螢幕呈現模式來劃分的話,可以歸類為:豎屏式和橫屏式。
HTML5互動小遊戲案例截圖
平常我們做過的需求裡,主要是以豎屏式為主,而橫屏式較少。對於豎屏式場景來說,大家的經驗會比較豐富,因此,此次主要式探討下橫屏式場景下的一些需要注意的點,特別是怎樣去做橫屏適配。
對於 H5 輕互動遊戲來說,要實現橫屏的話,主要是解決兩點:
1.無論使用者手持方向如何,都需要保證螢幕橫向顯示。
2.由於螢幕解析度的多樣化,因此就算是橫屏下也是需要進行橫屏適配,保證畫面在所有解析度下都能夠合理適配。
下面,我們針對這兩點分別闡述如何解決。
強制橫屏顯示
頁面內容顯示方向可分為豎排方向和橫排方向,如下圖所示。
頁面內容顯示方式:豎向排版和橫向排版
對於豎屏式 H5 輕互動來說,頁面會被期望保持豎排方向顯示。而如果頁面出現橫排方向顯示的情況,開發者往往會選擇利用提示蒙層來進行友好提示,讓使用者自主保持豎屏體驗,如下圖所示。
提示蒙層提醒使用者保持豎屏體驗
同樣地,在橫屏式 H5 輕互動遊戲中可以採取相同的措施進行簡單處理,在頁面內容按豎排方向顯示時,開發者進行對使用者提示其保持橫屏體驗。
但是,這對使用者體驗並不友好,因為這對於那些習慣於開啟鎖定為豎排方向功能(如下圖所示)的 iOS 平臺使用者,或者是關閉螢幕旋轉功能(如下圖所示)的 Android 平臺使用者來說,他們需要多一個處理步驟——先關閉豎排方向鎖定或是開啟螢幕旋轉,然後再橫向手持裝置。
豎排方向鎖定功能(iOS)與螢幕旋轉(Android)功能
因此,更好的做法是強制橫屏顯示,對螢幕 resize 事件進行監聽,當判斷為豎屏時將整個根容器進行逆時針 CSS3 旋轉 90 度即可,程式碼如下所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// 利用 CSS3 旋轉 對根容器逆時針旋轉 90 度 var detectOrient = function() { var width = document.documentElement.clientWidth, height = document.documentElement.clientHeight, $wrapper = document.getElementById("J_wrapper"), style = ""; if( width >= height ){ // 橫屏 style += "width:" + width + "px;"; // 注意旋轉後的寬高切換 style += "height:" + height + "px;"; style += "-webkit-transform: rotate(0); transform: rotate(0);"; style += "-webkit-transform-origin: 0 0;"; style += "transform-origin: 0 0;"; } else{ // 豎屏 style += "width:" + height + "px;"; style += "height:" + width + "px;"; style += "-webkit-transform: rotate(90deg); transform: rotate(90deg);"; // 注意旋轉中點的處理 style += "-webkit-transform-origin: " + width / 2 + "px " + width / 2 + "px;"; style += "transform-origin: " + width / 2 + "px " + width / 2 + "px;"; } $wrapper.style.cssText = style; } window.onresize = detectOrient; detectOrient(); |
但是!這裡有坑:如果你是採用 CreateJS 框架進行開發,那麼就不能通過 CSS3 途徑對包含 Canvas 的根容器進行旋轉處理,因為旋轉後會導致 Canvas 內的舞臺元素的事件響應位置錯亂。
解決辦法是,換成利用 CreateJS 框架內的 Stage 的 rotation
屬性對整個舞臺旋轉處理,程式碼如下:
1 2 3 4 5 6 7 8 9 10 |
if(self.isPortrait) { // 豎屏 // 舞臺旋轉 self.stage.x = self.canvasHeight; // 注意:x偏移相當於旋轉中點處理,更簡單 self.stage.rotation = 90; // more... }else { // 橫屏 self.stage.x = 0; self.stage.rotation = 0; // more... } |
橫屏適配處理
面對移動端多解析度繁複冗雜的情況,我們對於一般情況下(也就是常見的豎屏式)頁面適配處理可以說是爛熟於心,但是切換到橫屏式場景下,同樣的頁面適配方法可以直接應用嗎?會不會有什麼問題呢?
下面筆者分別從 DOM 和 Canvas 兩方面去著手闡述如何做橫屏適配處理。
解決 DOM 的橫屏適配問題
在移動端,常見的移動端適配方案是 REM 方案,而為了減少 JS 與 CSS 的耦合,筆者團隊開發頁面時採用的是 VW + REM 方案。(想要了解該方案的同學可詳細閱讀《利用視口單位實現適配佈局》)。
因為頁面適配的場景往往是豎屏式的,因此 VW + REM 方案表現得十分完美。但是遇上橫屏式,它的缺點就暴露了出來。
現行的 vw 單位適配方案帶來的問題
如上圖所示,由於響應斷點的限制最大寬度處理,會導致頁面兩側留白,當然這可以通過去掉最大寬度限制來解決。而真正的缺點在於,由於 vw 單位的特性,適配換算大小是根據螢幕寬度而言的,因此螢幕寬度越大導致容器、文字會越大,還可能導致 DOM 元素超出螢幕外,且文字過大並不是我們所想要的使用者體驗。
那麼,換成 px 單位的固定佈局如何?
但 px 單位的固定佈局只適合於部分場景,對於需要內容全屏覆蓋的場景(如下圖所示),就可能存在這樣的不理想的使用者體驗:絕對定位的元素之間空隙過大,導致佈局不美觀,又或者空隙過小,導致元素疊放被遮擋。
px單位固定佈局適配方案帶來的問題
我們瞭解到,vw 單位的特點是適配換算大小時是根據螢幕寬度而定的,那麼在強制橫屏顯示時,我們就可以同理轉換為螢幕高度來而定,也就是 vw 單位替換成 vh 單位。
這樣進一步改良之後就會得到滿意的適配效果,如下圖所示。
更好的適配解決方案—— vw、vh 單位搭配
具體實現可參考如下 SCSS 程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 |
$vw_base: 375; $vw_fontsize: 20; html { font-size: 20px; //不支援vw單位時,回退到px單位 font-size: ($vw_fontsize / $vw_base) * 100vw; } @media screen and (orientation: landscape) { html { font-size: 20px; font-size: ($vw_fontsize / $vw_base) * 100vh; } } |
解決 Canvas 的橫屏適配問題
解決 Canvas 的橫屏適配問題,目前在實際應用中有兩種主流的方案:
- 通過做兩套Canvas的方案。
- 採用縮放的手段進行適配的方案。
兩套 Canvas 的方案的做法是,頁面包含兩個 Canvas 分別用於橫豎屏時的相應顯示,但是它們的資料是打通的。但是,該方案難免會有侷限性,比較適合遊戲邏輯資料處理簡單、且舞臺元素少且居中的場景;
而縮放適配方案做法是,採用的最為常見的縮放手段——利用 CSS3 Transform 的 scale
屬性,達到“一種設計尺寸適配多種解析度螢幕”的目的。
採用了不同適配方案的案例
在市面上的一些成熟的主流 HTML5 遊戲引擎,例如 Cocos2D、Laya、Egret 等等,它們本身就整合了橫屏適配的方案。如果你有去了解過,可以發現它們普遍都是採用縮放的理念進行適配。
但是,對於我們常用的 CreateJS、PixiJS 框架來說,它們並沒有配套的現成的橫屏適配解決方案可以被採用的,尤其是我們如果採用原生 Javascript 去開發一個橫屏遊戲的時候。
因此,下面我們來研究下如何解決 Canvas 橫屏適配問題。
注意:下面文中示例程式碼都是在 CreateJS 框架的基礎上進行編寫的。
選用合適的縮放模式
橫屏適配的核心是縮放,通過 scale
屬性等手法將Canvas縮放至適合螢幕視窗大小。類似於 background-size
屬性的表現,縮放適配也可以有很多種模式,或有裁剪或無裁剪,或根據長邊縮放或根據短邊縮放等等。根據一些常見的實際應用場景,有比較常用的五種縮放模式:Contain、Cover、Fill、Fixed-Width、Fixed-Height。根據遊戲的不同的實際場景需求,我們可以選其中一種縮放模式進行適配。
下面,我們逐一解釋以上五種縮放模式的定義、實現與其適用的場景。
a. Contain模式
Canvas可以類比為一張圖,而圖片的適配,我們可以聯想到經常用以適配背景圖片的屬性 background-size
,其屬性值包括 contain
、cover
。
藉助 contain
的概念,我們把縮放的其中一種模式稱為 Contain 模式。因為在這種模式下,舞臺內容(gameArea)會保持寬高比進行縮放適配瀏覽器可視視窗(window),縮放至其能顯示完整的舞臺內容。
根據下圖推導,我們可以得出在這種縮放模式下的縮放比例(scaleRadio),為瀏覽器可視視窗與遊戲內容的寬度比或高度比之間較小者。
Contain 模式下的縮放比例推導圖
根據推導結論,簡單程式碼實現如下:
1 2 3 4 5 6 7 |
// Contain模式核心原理函式 CONTAIN: function(){ var self = this; self.radioX = self.radioY = Math.min((self.winWidth / self.designWidth) , (self.winHeight / self.designHeight)); self.canvasWidth = self.designWidth; self.canvasHeight = self.designHeight; } |
可以看出,在 Contain 模式下,如果舞臺內容寬高比與瀏覽器可視視窗的寬高比不相等時,舞臺內容並沒有填滿整個瀏覽器可視視窗,此時就會出現上下或左右兩側會存在留空部分。
對於這種 Contain 模式,會比較適合舞臺背景為純色或者是漸變型別的H5輕互動,舞臺內容與視窗的緊鄰處得以自然過渡銜接,不會突兀。
b. Cover模式
同樣地,藉助 cover
的概念把其中一種模式稱為 Cover 模式。在這種模式下,舞臺內容(gameArea)會保持寬高比進行縮放適配瀏覽器可視視窗(window),縮放至舞臺內容填滿視窗。
根據下圖推導,我們可以得出在這種縮放模式下的縮放比例(scaleRadio),為瀏覽器可視視窗與遊戲內容的寬度比或高度比之間較大者。
Cover 模式下的縮放比例推導圖
根據推導結論,簡單程式碼實現如下:
1 2 3 4 5 6 7 |
// Cover模式核心原理函式 COVER: function(){ var self = this; self.radioX = self.radioY = Math.max((self.winWidth / self.designWidth) , (self.winHeight / self.designHeight)); self.canvasWidth = self.designWidth; self.canvasHeight = self.designHeight; } |
在 Cover 模式下,如果舞臺內容寬高比與瀏覽器可視視窗的寬高比不相等時,由於舞臺內容需要填滿整個瀏覽器可視視窗,此時就會出現上下或者左右兩側被裁剪的情況。
那麼,如果能保證遊戲場景內的重點顯示內容全部顯示,被裁剪內容無關緊要時,那麼這種 H5 輕互動型別就可以考慮採用 Cover 模式。
怎麼做到保證想要重點顯示的內容可以不被裁剪呢?這時要談到一個“安全區域”的概念,指的是絕對不會被裁剪的內容區域,它應該是由最小的螢幕可視視窗(目前應該是 iPhone 4 )與最大的螢幕可視視窗(目前應該是 iPhone 7 Plus)疊加後得出的重疊區域,如下圖所示。
“安全區域”即為紅色虛線框內部分
開發者應該在設計階段與設計師、產品等相關人員進行溝通,告知其不想被裁剪的內容都應該在“安全區域”進行設計佈局。
c. Fill模式
Fill 模式,可以類比為 backgrouns-size: 100% 100%
的表現,在這種模式下,不會保持寬高比,舞臺內容(gameArea)的寬高分別按照舞臺內容與瀏覽器可視視窗(window)的寬度比與高度比進行縮放,縮放至舞臺內容拉伸鋪滿視窗。
根據下圖推導,我們可以得出在這種縮放模式下的縮放比例(scaleRadio),為對於遊戲內容的寬應用其與可視視窗的寬度比,而遊戲內容的高應用其與可視視窗的高度比。
Fill 模式下的縮放比例推導圖
根據推導結論,簡單程式碼實現如下:
1 2 3 4 5 6 7 8 |
// Fill模式核心原理函式 FILL: function(){ var self = this; self.radioX = (self.winWidth / self.stageWidth); self.radioY = (self.winHeight / self.stageHeight); self.canvasWidth = self.designWidth; self.canvasHeight = self.designHeight; } |
這種模式下既不會留空,也不會被裁剪,但是在舞臺內容寬高比與瀏覽器可視視窗的寬高比不相等時,顯示的內容會有一定程度的拉伸形變。
這種暴力的處理方式雖然免去了留空和裁剪的煩惱,但是會存在拉伸形變,這就得看是否能夠被接受了。
d. Fixed-Width模式
區別於影象,Canvas 是可以進行動態繪製大小的。所以,我們可以考慮根據螢幕視窗大小變化來動態繪製 Canvas。
從保持舞臺橫向內容不變的角度考慮,我們提出這樣的模式:舞臺內容(gameArea)等比進行縮放至與瀏覽器可視視窗的一致的寬度大小,而舞臺的高度(Canvas高度)進行重新繪製其高度為瀏覽器可視視窗的高度,稱之為 Fixed-Width 模式。
根據下圖推導,我們可以得出在這種縮放模式下的縮放比例(scaleRadio),為瀏覽器可視視窗與遊戲內容的寬度比。
Fixed-Width 模式下的縮放比例推導圖
根據推導結論,簡單程式碼實現如下:
1 2 3 4 5 6 7 |
// Fixed-Width模式核心原理函式 FIXED_WIDTH: function(){ var self = this; self.radioX = self.radioY = self.winWidth / self.designWidth; self.canvasWidth = self.designWidth; self.canvasHeight = self.winHeight / self.radioY; } |
在 Fixed-Width 模式下,無論在什麼解析度下,舞臺橫向內容保持不變,而縱向高度則會動態裁補,這就會比較適用於那些場戲場景可以縱向擴充的 H5 輕互動型別。
e. Fixed-Height模式
說完 Fixed-Width 模式,換個角度考慮便得出 Fixed-Height 模式,舞臺內容(gameArea)等比進行縮放至與瀏覽器可視視窗的一致的高度大小,而舞臺的寬度(Canvas寬度)進行重新繪製其寬度為瀏覽器可視視窗的寬度。
根據下圖推導,我們可以得出在這種縮放模式下的縮放比例(scaleRadio),為瀏覽器可視視窗與遊戲內容的高度比。
Fixed-Height 模式下的縮放比例推導圖
根據推導結論,簡單程式碼實現如下:
1 2 3 4 5 6 7 |
// Fixed-Height模式核心原理函式 FIXED_HEIGHT: function(){ var self = this; self.radioX = self.radioY= self.winHeight / self.designHeight; self.canvasWidth = self.winWidth / self.radioX; self.canvasHeight = self.designHeight; } |
與 Fixed-Width 模式相反,Fixed-Height 模式下,舞臺縱向內容保持不變,而橫向寬度則會動態裁補。對於這種模式的應用場景應該會比較廣泛,譬如常見的跑酷遊戲型別H5輕互動。
加入重定位和重繪製策略
綜合以上五種縮放模式,我們可以看到對於 Cover、Fixed-Width、Fixed-Height 模式而言,有存在被裁剪的可能性。特別是 Fixed-Height 模式,對於橫屏遊戲來說這是比較常用的模式,但是在螢幕較小的時候難免會被裁剪,而且我們是不希望貼邊元素被裁剪掉的,譬如位於右上角的音樂圖示。而對於 Fixed-Width、Fixed—Height 模式,它們還存在舞臺區域需要補充繪製的情況,因此對某些舞臺元素來說需要重新設定其渲染大小。
所以,除了基本的縮放適配模式實現之外,為了解決貼邊元素不被裁剪以及對一些舞臺元素重繪製的需求,我們還需要加入兩個策略:重定位和重繪製。
a. 重定位
貼邊元素重定位策略的實現原理很簡單,對需要重新定位的元素物件額外設定 top
、left
、right
、bottom
的自定義屬性(當然你可以命名為其他屬性名),這樣我們就可以在適配的時候根據這些自定義屬性以及實際顯示的 Canvas 大小進行重新計算位置。
為了保證效能,下面是策略裡需要注意的地方:
- 在舞臺裡,並不是所有遊戲元素都是需要被重定位的,因此我們只需要建立一個陣列記錄需要被重定位的元素。
- 適當控制重定位次數,我們不需要在每一幀 tick 繪製的時候都進行重定位,只需要在 Canvas 大小改變的時候進行處理。
以下是重定位策略相關的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// halfCutHeight、halfCutWidth是根據適配後的實際Canvas大小計算出來的相對距離 _setSize: function(){ // ... if(self.isPortrait) { // ... self.halfCutWidth = (self.canvasWidth * self.radioY - this.winWidth ) / 2 / self.radioY; self.halfCutHeight = (self.canvasHeight * self.radioX - this.winHeight) / 2 / self.radioX; }else { // ... self.halfCutWidth = (self.canvasWidth * self.radioX - this.winWidth ) / 2 / self.radioX; self.halfCutHeight = (self.canvasHeight * self.radioY - this.winHeight) / 2 / self.radioY; } // ... }, // 貼邊元素重定位核心處理函式 _adjustPosition: function(item){ var self = this; item && self.adjustPositionArr.push(item); self.adjustPositionArr.map(function(item, index, arr){ (typeof item.top == "number") && (item.y = item.top + self.halfCutHeight >= 0 ? self.halfCutHeight : 0); (typeof item.left == "number") && (item.x = item.left + self.halfCutWidth >= 0 ? self.halfCutWidth : 0); (typeof item.bottom == "number") && (item.y = self.canvasHeight - item.getBounds().height - item.bottom + self.halfCutHeight >= 0 ? self.halfCutHeight : 0); (typeof item.right == "number") && (item.x = self.canvasWidth - item.getBounds().width - item.right - self.halfCutWidth); }); }, // 暴露方法:提供給開發者記錄需要重定位的貼邊元素 adjustPosition: function(item){ var self = this; self._adjustPosition(item); } |
b. 重繪製
對於一些以舞臺區域(gameArea)作為其大小設定的參考標準的元素,在適配時遇到需要補全繪製區域時,舞臺區域大小發生變化,相應地,該元素就需要進行重新繪製,這就是重繪製策略的存在意義。
同樣地,為了保證效能,重繪製策略也是同樣需要保證:
- 建立對應的陣列記錄全顯圖形物件。
- 不在每一幀 tick 時進行重繪製,只在適配的時候重繪製。
以下是重繪製策略的相關程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 全顯圖形重繪製核心處理函式 _adjustFullSize: function(item){ var self = this; item && self.adjustFullSizeArr.push(item); self.adjustFullSizeArr.map(function(item, index, arr){ item.drawRect(0, 0, self.canvasWidth, self.canvasHeight); }); }, // 暴露方法:提供給開發者記錄需要重繪製的全顯圖形 adjustPosition: function(item){ var self = this; self._adjustPosition(item); } |
至此,Canvas 橫屏適配問題才得以完全解決。
這部分內容篇幅較長,筆者簡單總結下,一個簡單的解決 Canvas 橫屏適配問題的方案至少需要包括兩點實現:
- 選用合適的縮放模式。
方案內建五種縮放模式,在實際應用中根據場景不同而採用不同的縮放進行適配。 - 加入重定位和重繪製策略。
為了保證貼邊元素不被裁剪以及舞臺元素動態渲染大小以適應舞臺區域的動態變化。
最終的整體效果可前往體驗地址進行體驗,體驗時可點選文字元素進行切換模式。另外,整體的實現方案是基於 CreateJS 框架進行實現的,文中的實現方案的程式碼會託管筆者github上。
後話
本文主要的核心在於探討橫屏遊戲中的處理點與解決方案,因此如果實現程式碼方面有任何錯漏之處,請大膽地提出糾正吧!又或者讀者們有更好的見解之處,也歡迎留言分享噢。
參考資料
《如何打造一個高效適配的H5》
《Cocos2d-JS的螢幕適配方案》
《Cocos2d-JS 多解析度適配方案》
《Cocos2d-JS 對齊策略》
《Laya引擎-自動橫屏適配》
《Phaser-scaleManager物件》
《How to create mobile games for different screen sizes and resolutions》
《Egret-螢幕適配策略》