不管 UI 做得多漂亮、多華麗,反饋、排版、效果等這些細工做得多好,如果整體架構、動線流程出錯了,做了那麼多細工的辛苦就會白費掉,因為,會讓玩家迷路的 UI 動線,使用起來就是會不舒服;即使使用 UI 的動線設計都沒問題,但沒有規劃一致性的流程管理,在製作及維護上也容易造成一些困擾。
舉例來說,大部份的 UI 畫面都會有個「返回」按鈕可以回到前一個畫面,當這個 UI 畫面是從 A 畫面進來時,應該可以返回到 A 畫面,從 B 畫面進來時,應該可以返回到 B 畫面,就像我們平常使用網頁瀏覽器一樣,不管目前的頁面是從哪裡進來的,按下「上一頁」就是會回到進來目前網頁之前的那個網頁,UI 上的「返回」按鈕也是一樣的功能,只是,這個返回是個按鈕,它會被我們設置執行某個 Component 的某個功能,透過這個功能來使它開啟某個 UI 畫面,所以,我們很直覺的就會有點像寫死的程式一樣,A 畫面進入 B 畫面,所以點擊 B 畫面的返回按鈕就是開啟 A 畫面,如果,C 畫面也可以進入 B 畫面呢?那麼就做個暫存紀錄讓 B 畫面判斷是從 A 還是 C 畫面進入的,或者進入 B 畫面的同時,通知 B 畫面說是從哪裡進來的,此時,第一個問題來了,每當多出一個會進入到 B 畫面的畫面,B 畫面的返回按鈕功能就要進行修改來達到正確的判斷;第二個問題,假設 A、B 畫面都可以進入 C 畫面,C 畫面可以進入 A、D 畫面,A、D 畫面都可以進入 B 畫面... 等等較深入的交叉動線,新增一個 UI 畫面或者更動到某個 UI 畫面流程,就幾乎全部畫面的返回鍵都要進行修改,這種工程實在太浩大了,牽一髮而動全身的維護狀況是最容易產生未知 Bug 的。
有鑑於此,設計的 UI 流程管理機制至少要滿足兩個條件:
- 玩家使用時不會迷路。
- 不管未來變更多少畫面,都不需要修改或維護返回按鈕。
流程上的概念沒問題了,另外就是一些其他要注意的部分:
- 每個 UI 畫面間的進場和退場動態,
- 進退場畫面進行中時,UI 事件不能有作用。
- 退場 UI 畫面即使遊戲畫面上看不到,也不應該讓它持續執行。
UI 畫布的基本結構
先決定我們要使用怎麼樣的畫面比例、解析度來編排製作 UI,通常會跟 Game view 設置好要預覽的畫面相同。
建立 UI 的主畫布(Canvas),並在其 Canvas Scaler 設置好 Ui Scale Mode 以及 Reference Resolution,這主要是為了使將來 UI 面臨不同畫面比例的裝置時,可以有基本的自適應調整,詳情可以參考另一篇文章「Unity:UGUI 應對各種螢幕自動調整大小及位置」
設置 Ui Scale Mode 與 Reference Resolution |
使用 Image 製作透明遮擋層 Modal 物件 |
製作動畫檔
製作 UI 畫面的進場、退場動畫檔分別命名為 Open、Closed,最簡單的就是淡入、淡出或者是放大、縮小的方式,當然,也可以設計更豐富的動畫效果,最重要的是,進、退場動畫檔播放期間要記得開啟前面製作的「透明遮擋層」。
動畫播放期間開啟「透明遮擋層」 |
設置動畫控制
當我們為 UI 畫面製作好進場、退場的動畫檔之後,開啟 Animator view,可以發現 Unity 已經自動幫我們在 Animator 建立兩個與動畫檔相同名稱的動畫狀態(State)。
預設,Unity 會自動將 AnimationClip 放到 Animator 做為 state |
在 Animator 的 Parameters 建立一個 Trigger |
設置 Transition 裡的幾個相關欄位 |
設置好正確的時間值 |
取消 Loop Time |
動畫狀態腳本
這部分,最主要的是要讓 UI 畫面退場之後,也能同時將它的 GameObject 整個關閉掉,以往,我們是在 GameObject 掛上我們自已寫的 Script,並在製作動畫檔時,在 Animation view 加入 Event,自從 Unity 5.0 發佈後,就不再需要這麼麻煩,我們可以直接點選 Animator view 裡的任意狀態,並在 Inspector view 中點擊 Add Behaviour 按鈕,建立 Animator 狀態專用的 Script,如此就可以直接透過撰寫這個 Script 的內容來對 Animator 裡的狀態針對狀態開始、進行中、結束.. 等等的時機進行其他操作,因此,我們要讓 UI 畫面退場之後關閉本身 GameObject,只需要為 Closed 建立 StateMachineBehaviour 並在 OnStateExit 加入簡短的一行程式碼即可。
為 Animator 的 State 加入 Behaviour |
using UnityEngine;
public class UIStateClosed : StateMachineBehaviour {
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
animator.gameObject.SetActive(false);
}
}
public class UIStateClosed : StateMachineBehaviour {
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) {
animator.gameObject.SetActive(false);
}
}
UI 流程管理腳本
接下來算是重頭戲了,做完前面的工作,剩下的就是靠這個 Script 去做主要的管理,而這個 Script 主要做哪些工作呢?我們將其列出如下:
1. 建立一個列表來紀錄 UI 畫面歷程,用來將曾經進入的 UI 畫面依序記錄下來,以方便依序返回。
2. 先將第一個開啟的 UI 畫面記錄到歷程中,當歷程只剩下一筆時,就不能再返回。
3. 不可以進入與目前 UI 畫面相同的畫面。
4. 即將要進入或返回的目標畫面必須移到最上層。
5. 往前進入的目標畫面必須記錄到歷程中。
6. 返回時,必須要將目前畫面從歷程紀錄中移除。
另外,之前的文章「Unity:認識 Tag 與 Layer 的差異與應用」曾提到過程式碼中應該避免寫死的字串值,所以,這個 Script 因為會使用到 Animator 的 Parameters 中設定的名稱來通知 Animator 轉換到 Closed 狀態,所以也需要宣告一個 public 欄位來設置該名稱。
有了以上的工作需求,我們就可以撰寫出如下的程式碼:
using UnityEngine;
using System.Collections.Generic;
public class UIManager : MonoBehaviour {
public GameObject startScreen;
public string outTrigger;
private List<GameObject> screenHistory;
void Awake(){
this.screenHistory = new List<GameObject>{this.startScreen};
}
public void ToScreen(GameObject target){
GameObject current = this.screenHistory[this.screenHistory.Count - 1];
if(target == null || target == current) return;
this.PlayScreen(current , target , false , this.screenHistory.Count);
this.screenHistory.Add(target);
}
public void GoBack(){
if(this.screenHistory.Count > 1){
int currentIndex = this.screenHistory.Count - 1;
this.PlayScreen(this.screenHistory[currentIndex] , this.screenHistory[currentIndex - 1] , true , currentIndex - 2);
this.screenHistory.RemoveAt(currentIndex);
}
}
private void PlayScreen(GameObject current , GameObject target , bool isBack , int order){
current.GetComponent<Animator>().SetTrigger(this.outTrigger);
if(isBack){
current.GetComponent<Canvas>().sortingOrder = order;
}else{
current.GetComponent<Canvas>().sortingOrder = order - 1;
target.GetComponent<Canvas>().sortingOrder = order;
}
target.SetActive(true);
}
}
using System.Collections.Generic;
public class UIManager : MonoBehaviour {
public GameObject startScreen;
public string outTrigger;
private List<GameObject> screenHistory;
void Awake(){
this.screenHistory = new List<GameObject>{this.startScreen};
}
public void ToScreen(GameObject target){
GameObject current = this.screenHistory[this.screenHistory.Count - 1];
if(target == null || target == current) return;
this.PlayScreen(current , target , false , this.screenHistory.Count);
this.screenHistory.Add(target);
}
public void GoBack(){
if(this.screenHistory.Count > 1){
int currentIndex = this.screenHistory.Count - 1;
this.PlayScreen(this.screenHistory[currentIndex] , this.screenHistory[currentIndex - 1] , true , currentIndex - 2);
this.screenHistory.RemoveAt(currentIndex);
}
}
private void PlayScreen(GameObject current , GameObject target , bool isBack , int order){
current.GetComponent<Animator>().SetTrigger(this.outTrigger);
if(isBack){
current.GetComponent<Canvas>().sortingOrder = order;
}else{
current.GetComponent<Canvas>().sortingOrder = order - 1;
target.GetComponent<Canvas>().sortingOrder = order;
}
target.SetActive(true);
}
}
套用
完成以上的工作以及程式撰寫之後,只要建立一個空的 GameObject 並將 UIManager script 掛上去,在 Start Screen 欄位放入第一個要顯示的 UI 畫面 GameObject,在 Out Trigger 填寫跟 Animator 設置的 Trigger 參數相同的名稱。
設置 Start Screen 及 Out Trigger 欄位 |
前往下個 UI 畫面,選擇 ToScreen |
設置好下個 UI 畫面的 GameObject |
返回上個 UI 畫面,選擇 GoBack |
以上,這個簡單的 UI 流程管理機制算是完成了,之後,我們如果對於 UI 執行轉換動作的邏輯要進行變更(例如,目標畫面的排序規則),則只需要修改 UIManager 裡面的 PlayScreen;如果,要換成別的進場、退場動畫效果,也只需要替換掉 Animator 裡面各狀態的 motion;如果做了多種版本的 UIManager 並存於同一個專案或場景中,也可直接變更 UI 裡 Button 的 On Click 設置,整個機制相當的彈性,但是,玩家操作上卻不至於混亂,在後續維護上也將會更為輕鬆。
最後,提供範例專案檔以供參考:下載
P.S. 目前使用 Unity 版本為 5.1.0f3。