2014年5月6日 星期二

Unity:應對各種螢幕比例自動調整畫面縮放及位置


以前曾經發佈過一篇文章『Unity 自動調整 GUI 縮放比例及位置』說明如何應對各種不同解析度畫面,以佔滿版面的方式自動縮放調整 GUI 位置及大小,但這種方式有很大的缺點就是 GUI 遇到比例不同的畫面時,會被拉扯變形,使得圓形變成橢圓形這類的問題發生,而且,現今多數 Unity 使用者可能已不使用 Unity 內建的 GUI class 來製作 UI,而改使用 NGUI、DF-GUI 等其他工具來製作,或是利用自製平面 Mesh 配合 GUI class 來使 GUI 與場景內的其他物件互動更靈活,同時,在 Unity 4.3 發佈了 Unity Native 2D Tools 之後, Unity 在 2D 應用上更為方便及普遍,這也代表著處理 2D 畫面的縮放及位置調整將變得更為重要,所以本篇文章將同時說明 Camera 及 Unity 內建 GUI 在維持原比例以及不維持原比例時的自動縮放及位置調整。

鏡頭 Camera 數值的換算調整

在不使用 Unity 內建 GUI 的情況下,無論是使用 NGUI、DF-GUI 等工具製作 UI,或是透過 Unity Native 2D Tools 製作 2D 遊戲及介面,甚至是使用其他 2D 遊戲開發插件來開發 2D 遊戲,為了這些 2D 畫面的呈現,都一定會需要將 Camera 的 Projection 調整為 Orthographic,此時 Camera 在 Inspector view 的 Size 欄位將代表遊戲畫面上能看到的範圍大小,而此時的畫面解析度得出的長寬比(aspect ratio)也將影響到畫面長度與高度的可視範圍,如果遊戲在實機安裝上與製作時預設的長寬比不同,就很容易發生畫面邊緣被切掉或是露出過多的情形。

開發時,預設的長寬比
在 Scene view 看到 Camera 的可視範圍與預期畫面大小剛剛好
在不同的畫面比例,左右兩邊被裁切掉了
在 Scene view 可以看到 Camera 可是範圍左右兩邊縮短了

為了避免這種情形發生,我們可以有以下兩種選擇來解決:

1. 使遊戲畫面直接佔滿整個螢幕畫面:

這是比較簡單的處理方式,缺點是遊戲畫面可能會被拉長或縮短而使得遊戲物件在螢幕上看起來有些變形,例如圓形變成橢圓形,但不用擔心,這個圓形自體旋轉時並不會看起來像橄欖球那樣旋轉。做法相當簡單,就是在執行遊戲時重設 Camera 的 aspect,使這個數值與預期的畫面解析度的 aspect 值相同即可,而這個值則是從畫面解析度的寬除以高來求得的。

程式碼如下:
//定義遊戲開發時所使用的基礎解析度寬高
public float baseWidth = 1024;
public float baseHeight = 768;

void Awake(){
    camera.aspect = this.baseWidth / this.baseHeight;
}

縮放變形佔滿整個畫面,左右兩邊沒有被切掉
在 Scene view 可以看出 Camera 可視範圍不變,但右下角的 Preview 已經改變

2. 使遊戲畫面維持原來比例進行縮放:

這是比較複雜的換算方式,如果遊戲畫面高度超出螢幕畫面高度,必須等比縮小使得遊戲畫面高度與螢幕畫面高度相同,反之,當遊戲畫面寬度超出螢幕畫面寬度,就必須等比縮小使得遊戲畫面寬度與螢幕畫面寬度相同。
首先,要來解說一些原理,我們可以從官方文件 Camera.orthographicSize 得知 orthographicSize 是檢視空間(viewing volume)的垂直大小的一半,所以,以我們在開發遊戲時設定畫面解析度為 1024*768 這樣的長寬比為例,如果當時將 orthographicSize 設定為 5,那麼檢視空間的垂直大小就是 5 * 2 = 10,我們假設檢視空間垂直大小需要乘以一個轉換因子(factor)之後會得到我們期望的畫面解析度垂直大小 768,那麼可以簡單的用這樣的算式求得轉換因子的值...

10 * factor = 768
=> factor = 786 / 10
=> factor = 76.8

求得轉換因子之後,我們希望將遊戲畫面維持原本比例縮放至高度為 1024 的畫面,就可以用這樣的算式反算求出我們期望的檢視空間垂直大小。

viewVolumeVertical * 76.8 = 1024
=> viewVolumeVertical = 1024 / 76.8
=> viewVolumeVertical = 13.33333333333333...

如此,我們就知道應該將 Camera.orthographicSize 設定為多少了。

newOrthographicSize = viewVolumeVertical / 2 = 6.66666666666666…

先整理一下算式:

newOrthographicSize = viewVolumeVertical / 2
=> newOrthographicSize = (targetHeight / factor) / 2
=> newOrthographicSize = (targetHeight / (baseHeight / (baseOrthographicSize * 2))) / 2

viewVolumeVertical: 檢視空間的垂直大小
targetHeight: 目標畫面解析度的高度
baseHeight: 開發時定義的基礎解析度高度
baseOrthographicSize: 開發時定義的 orthographicSize

看起來似乎已經達到我們的目的,但這是假定只有高度改變的情況,實作上寬度也需要進行調整,所以還需要算出寬度變為原來的幾倍,並讓它乘上 factor,而得到以下這個新的算式:

newOrthographicSize = (targetHeight / (factor * (targetWidth / baseWidth))) / 2
=> newOrthographicSize = (targetHeight / (baseHeight / (baseOrthographicSize * 2) * (targetWidth / baseWidth))) / 2

targetWidth: 目標畫面解析度的寬度
baseWidth: 開發時定義的基礎解析度寬度

將這個複雜的算式簡化後,可以得到一個有趣且可讀性較高的算式:

newOrthographicSize = targetHeight / targetWidth * baseWidth / baseHeight * baseOrthographicSize;

這樣看起來,算式似乎已經完成了,但還有一點要注意的是 orthographocSize 為檢視空間垂直大小的一半,所以當計算結果 newOrthographicSize 比 baseOrthographicSize 小的話,畫面上下邊將會發生被切掉的情形,所以實作時要注意。

總結以上的理論,只需要一個簡單的 Script,將它掛到需要進行處理的鏡頭上,就可以讓遊戲畫面在各種不同尺寸的螢幕上自動縮放到適中且維持原來的長寬比例,即使是橫向的遊戲畫面硬要放到縱向的手機螢幕上,仍然不用擔心畫面邊緣被切掉。

程式碼如下:
public float baseWidth = 1024;
public float baseHeight = 768;
public float baseOrthographicSize = 5;

void Awake(){

    float newOrthographicSize = (float)Screen.height / (float)Screen.width * this.baseWidth / this.baseHeight * this.baseOrthographicSize;
    camera.orthographicSize = Mathf.Max(newOrthographicSize , this.baseOrthographicSize);
}

等比縮放畫面內容,使左右兩邊不被切掉
從 Scene view 看到 Camera 的可視範圍包含整個畫面內容



Unity 內建 GUI class (OnGUI) 的數值調整

如果是使用 Unity 內建的 GUI class 來製作 UI,也就是直接在 OnGUI() 裡面直接撰寫程式碼的那些 GUI 元素,由於它算是另一個獨立的坐標空間算法,所以即使如前面提到的方式對鏡頭做調整,也無法使 GUI 跟著自動縮放以及調整,因此,針對這部分則需要有另一套解決方案。
開發時,設定的長寬比,GUI 都在正確的指定位置
畫面比例改變,即使鏡頭已自動調整,但 GUI 並不會因此調整

在以前的那篇文章『Unity 自動調整 GUI 縮放比例及位置』所提到的兩種做法,其實終歸是利用改變 GUI.matrix 的內容去達到目的,而使得 GUI 可以變形縮放適合不同長寬比例的畫面,所以在這邊我們要先了解到 GUI.matrix 裡面的幾個元素值所代表的功用:

m00: x 軸的縮放比例
m03: 實際畫面上,GUI的 x 軸起始點為從左邊算來第幾個像素(pixel)
m11: y 軸的縮放比例
m13: 實際畫面上,GUI的 y 軸起始點為從上方算來第幾個像素(pixel)
m33: x 軸及 y 軸等比縮放比例的反比,也就是說,數值變大為等比縮小,數值變小則等比放大。

1. 使 GUI 直接依照螢幕比例改變而跟著自動調整變形:

這種做法相當簡單,只需要依照長度和寬度個別的縮放倍數去重新指定 GUI.matrix 中的值即可,我們只需要為 GUI 做這種方式的調整即可讓我們設計的 GUI 總是在對的位置,且不需要個別的調整 GUI 的圖或文字的大小及位置。

實作程式碼:
public float baseWidth = 1024;
public float baseHeight = 768;

private float m00;
private float m11;

void Awake(){

    this.m00 = (float)Screen.width / this.baseWidth;
    this.m11 = (float)Screen.height / this.baseHeight;
}

void OnGUI(){

    Matrix4x4 _matrix = GUI.matrix;
    _matrix.m00 = this.m00;
    _matrix.m11 = this.m11;
    GUI.matrix = _matrix;

    //... Your GUI ...
}

隨著畫面比例改變,GUI 變形縮放佔滿整個版面,位置也正確了

2. 使 GUI 等比縮放在螢幕顯示範圍內

前面的做法雖然簡單,但需要留意的是,當設計的 GUI 有旋轉的行為時,看起來就會有點怪異,例如,GUI 上有個圓形的圖需要旋轉,將會變得像是個橢圓形如同橄欖球那樣的旋轉,甚至會破壞掉原本設計的畫面;所以我們除了需要 GUI 能夠依照顯示螢幕的不同而自動縮放之外,還需要使它能等比例縮放並維持以螢幕正中央為基準的位置上。
在此,還是一樣來講一下原理,首先,我們必須先知道原本開發時的畫面以及實際顯示時的畫面的長寬比(aspect),並以這兩個長寬比來決定應該使用寬度還是高度來計算縮放比例,如果實際顯示時的畫面長寬比(aspect)比較大,代表畫面是上下縮窄(直向變橫向),那麼我們可以將實際顯示畫面的高度除以原本開發時定義的畫面高度來計算出高度的縮放倍數作為運算因子(factor);反之,如果實際顯示畫面的長寬比(aspect)比較小,代表畫面左右被縮窄(橫向變直向),那麼我們就需要將實際顯示畫面的寬度除以原本開發時定義的畫面寬度來計算寬度的縮放倍數作為運算因子(factor);這麼做的目的主要是為了避免縮放後發生畫面縱向或橫向的兩邊被裁切到。

factor = targetAspect > baseAspect ? targetHeight / baseHeight : targetWidth / baseWidth

targetAspect: 實際顯示畫面的長寬比
targetWidth: 實際顯示畫面的解析度寬度
targetHeight: 實際顯示畫面的解析度高度
baseAspect: 開發時定義的基礎解析度長寬比
baseWidth: 開發時定義的基礎解析度寬度
baseHeight: 開發時定義的基礎解析度高度

接下來,由於 GUI.matrix.m33 是等比縮放比例的反比,所以可以這樣指定新的值給它。

m33 = 1 / factor

為了使畫面置中,如果 targetAspect 比較大,表示畫面變寬了,所以我們需要改變 GUI 的 x 軸起始點。

m03 = (targetWidth - baseWidth * factor) / 2 * m33

反之,如果 targetAspect 比較小,代表畫面變長了,所以我們需要改變 GUI 的 y 軸的起始點。

m13 = (targetHeight - baseHeight * factor) / 2 * m33

如此,就能達到 GUI 等比縮放到剛好符合畫面大小,且剛好位於整個顯示畫面以中間為基準的適當位置。

實作程式碼:
public float baseWidth = 1024;
public float baseHeight = 768;

private float baseAspect;
private float targetAspect;
private float m03;
private float m13;
private float m33;

void Awake(){

    float targetWidth = (float)Screen.width;
    float targetHeight = (float)Screen.height;

    this.baseAspect = this.baseWidth / this.baseHeight;
    this.targetAspect = targetWidth / targetHeight;

    float factor = this.targetAspect > this.baseAspect ? targetHeight / this.baseHeight : targetWidth / this.baseWidth;

    this.m33 = 1 / factor;
    this.m03 = (targetWidth - this.baseWidth * factor) / 2 * this.m33;
    this.m13 = (targetHeight - this.baseHeight * factor) / 2 * this.m33;
}

void OnGUI(){

    Matrix4x4 _matrix = GUI.matrix;

    _matrix.m33 = this.m33;

    if(this.targetAspect > this.baseAspect) _matrix.m03 = this.m03;
    else _matrix.m13 = this.m13;

    GUI.matrix = _matrix;

    //... Your GUI ...
}

畫面比例改變,GUI 等比縮放到適中大小,並使其置中

以上,解決了 Camera 的 Orthographic 及 GUI 在不同畫面解析度時的等比例及非等比例自動縮放調整的需求,也許你還使用到了 3D Text (Text Mesh),由於它也是存在於 3D 空間中的 Mesh 物件,所以透過 Camera 的 orthgraphicSize 的調整,它所呈現的就跟其它遊戲中物件一樣,也會跟著自動縮放調整在畫面上呈現適當的大小及位置;比較特別的是 GUIText 及 GUITexture 這兩個老舊的 GUI 物件,它們的坐標定位方式較特別,但實際應用時的使用範圍並不廣泛,所以在此並沒有特別去求它們的換算方式。

最後,畫面的自動縮放調整處理好了之後,如果選擇的是等比例縮放的方式,只要實機上的螢幕比例與開發時制定的比例不同,就會有左右或上下的兩邊會出現較多的露出,如果是開發 2D 遊戲,而版面背景也是與制定的畫面大小剛好一樣大的話,那麼只需要將 Camera 的 Background 設定為黑色就好;但通常為了保險起見,除了 UI 之外,並不會做的那麼剛好,而是會為場景配置得比要呈現的畫面範圍多出很多,同時,也會在畫面可視範圍外放置一些準備中的物件,所以,通常在需要限制畫面顯示範圍的情況下,我們必須在場景最靠近鏡頭的位置,依照想要呈現的畫面外圍再多擺放一些黑色的平面物件作為遮擋,如此,整個應對螢幕比例不同的工程就完成了,至於 GUI 的部分,通常不會在原本的可視範圍外放置多餘的 GUI 來增加 Draw Calls 數量,因此,不需要再做其他處理。

在很寬比例的螢幕上,等比縮放到適中大小及位置
即使是橫向畫面遊戲,在縱向螢幕上顯示,畫面比例及位置也都是正確的

目前的 Unity 版本為 4.3.4,但由於本文提到的 Camera orthgraphicSize 及 GUI.matrix 並不是新功能,理論上可適用於 3.x 及 4.x,以上程式碼使用的程式語言為 C#,但省略了 class 聲明以及 using 的部分,如果要直接複製使用,請記得補上,以免發生錯誤。

【2015/10/27 補充】
範例下載 http://1drv.ms/1OT1UOc

如果,你喜歡這個文章,請幫忙介紹給你的朋友,也別忘了來粉絲專頁按個讚,謝謝!
facebook: https://www.facebook.com/RandomExp
Google+: https://plus.google.com/118024819171916511609/
YouTube: https://www.youtube.com/channel/UCV6EVZj_vILRuD8gdrZBekQ