UGUI-創(chuàng)建屏幕過渡
在多個(gè) UI 屏幕之間進(jìn)行過渡的需求相當(dāng)普遍。在本頁面中,我們將探索一種使用動(dòng)畫和狀態(tài)機(jī)來創(chuàng)建和管理這些過渡以便驅(qū)動(dòng)和控制每個(gè)屏幕的簡單方法。
概述
大體思路是每個(gè)屏幕都有一個(gè)動(dòng)畫控制器以及兩個(gè)狀態(tài)(Open 和 Closed)和一個(gè)布爾參數(shù)?(Open)。要在屏幕之間過渡,只需關(guān)閉當(dāng)前打開的屏幕并打開所需的屏幕。為了簡化這一過程,我們將創(chuàng)建一個(gè)小型的 ScreenManager 類,稍后用于跟蹤并處理所有已打開的屏幕。觸發(fā)過渡的按鈕只需要讓 ScreenManager 打開所需的屏幕。
關(guān)于導(dǎo)航的注意事項(xiàng)
如果打算支持通過控制器/鍵盤對(duì) UI 元素進(jìn)行導(dǎo)航,必須注意幾點(diǎn)。必須避免可選元素超出屏幕,因?yàn)檫@樣會(huì)讓玩家選擇屏幕外的元素,為此我們可以停用所有屏幕外層級(jí)視圖。我們還需要確保在顯示新屏幕時(shí)將其中的某個(gè)元素設(shè)置為選中狀態(tài),否則玩家將無法導(dǎo)航到新屏幕。我們將在下面的 ScreenManager 類中處理所有這些問題。
設(shè)置動(dòng)畫控制器
讓我們來看看進(jìn)行屏幕過渡時(shí)所需的最常見和最小低限度的動(dòng)畫控制器設(shè)置??刂破餍枰粋€(gè)布爾參數(shù) (Open) 和兩個(gè)狀態(tài)(Open 和 Closed),每個(gè)狀態(tài)應(yīng)該有一段只有一個(gè)關(guān)鍵幀的動(dòng)畫,這樣就能讓狀態(tài)機(jī)為我們執(zhí)行過渡混合。


現(xiàn)在我們需要在兩個(gè)狀態(tài)之間創(chuàng)建過渡,讓我們從 Open 到 Closed 的過渡開始,首先正確設(shè)置條件,當(dāng)參數(shù) Open 設(shè)置為 false 時(shí),我們希望從 Open 變?yōu)?Closed。接著,我們創(chuàng)建從 Closed 到 Open 的過渡,并將條件設(shè)置為:當(dāng)參數(shù) Open 為 true 時(shí),狀態(tài)從 Closed 變?yōu)?Open。


管理屏幕
進(jìn)行以上所有設(shè)置后,唯一缺少的是我們需要在要過渡到的屏幕的動(dòng)畫器上將參數(shù) Open 設(shè)置為 true,并在當(dāng)前打開的屏幕的動(dòng)畫器上將 Open 設(shè)置為 false。為此,我們將創(chuàng)建一個(gè)小腳本:
using UnityEngine;?
using UnityEngine.UI;?
using UnityEngine.EventSystems;?
using System.Collections;?
using System.Collections.Generic;?
public class ScreenManager : MonoBehaviour { ? ?
//在場景開始時(shí)自動(dòng)打開的屏幕 ? ?
public Animator initiallyOpen; ? ?
//當(dāng)前打開的屏幕 ? ?
private Animator m_Open; ? ?
//我們用來控制過渡的參數(shù)的哈希值。 ? ?
private int m_OpenParameterId; ? ?
//在我們打開當(dāng)前屏幕之前選擇的游戲?qū)ο蟆? ? ?
//在關(guān)閉屏幕時(shí)使用,因此我們可以返回將其打開的按鈕。 ? ?
private GameObject m_PreviouslySelected; ? ?
//我們?cè)跈z查時(shí)需要比對(duì)的動(dòng)畫器狀態(tài)和過渡名稱。 ? ?
const string k_OpenTransitionName ="Open"; ? ?
const string k_ClosedStateName = "Closed"; ? ?
public void OnEnable() ? ?{ ? ? ? ?
//我們將哈希緩存到 "Open" 參數(shù),因此可提供給 Animator.SetBool。 ? ? ? ?m_OpenParameterId = Animator.StringToHash (k_OpenTransitionName);?
//現(xiàn)在打開初始屏幕(如果已設(shè)置)。 ? ? ? ?
if (initiallyOpen == null) ? ? ? ? ? ?return; ? ? ? ?
OpenPanel(initiallyOpen); ? ?
} ? ?
//關(guān)閉當(dāng)前打開的面板并打開提供的面板。 ? ?
//還負(fù)責(zé)處理導(dǎo)航,設(shè)置新的選定元素。 ? ?
public void OpenPanel (Animator anim) ? ?{ ? ? ? ?
if (m_Open == anim) ? ? ? ? ? ?return; ? ? ? ?
//激活新的屏幕層級(jí)視圖,以便對(duì)其進(jìn)行動(dòng)畫化。 ? ? ? ?
anim.gameObject.SetActive(true); ? ? ??
?//保存當(dāng)前選定的用于打開此屏幕的按鈕。(CloseCurrent 會(huì)對(duì)其進(jìn)行修改) ? ? ? ?
var newPreviouslySelected = EventSystem.current.currentSelectedGameObject; ? ? ? ?
//將屏幕移到前面。 ? ? ? ?
anim.transform.SetAsLastSibling(); ? ? ? ?
CloseCurrent(); ? ? ? ?
m_PreviouslySelected = newPreviouslySelected; ? ? ? ?
//將新屏幕設(shè)置為打開的屏幕。 ? ? ? ?
m_Open = anim; ? ? ??
//啟動(dòng)打開動(dòng)畫 ? ? ? ?
m_Open.SetBool(m_OpenParameterId, true); ? ? ??
?//將新屏幕中的一個(gè)元素設(shè)置為新的選定元素。 ? ? ? ?
GameObject go = FindFirstEnabledSelectable(anim.gameObject); ? ? ??
?SetSelected(go); ? ?
} ? ?
//查找提供的層級(jí)視圖中的第一個(gè)可選元素。 ? ?
static GameObject FindFirstEnabledSelectable (GameObject gameObject){ ? ? ? ?GameObject go = null; ? ? ? ?
var selectables = gameObject.GetComponentsInChildren<Selectable> (true); ? ? ? ?
foreach (var selectable in selectables) { ? ? ? ? ? ?
if (selectable.IsActive () && selectable.IsInteractable ()) { ? ? ? ? ? ? ? ?
go = selectable.gameObject; ? ? ? ? ? ? ? ?
break; ? ? ? ? ??
?} ? ? ??
?} ? ? ? ?
return go; ? ?
} ? ?
//關(guān)閉當(dāng)前打開的屏幕 ? ?
//還負(fù)責(zé)處理導(dǎo)航。 ? ?
//將選定元素還原為打開當(dāng)前屏幕之前使用的可選元素。 ? ?
public void CloseCurrent() ? ?{ ? ? ? ?
if (m_Open == null) ? ? ? ? ? ?
return; ? ? ??
?//啟動(dòng)關(guān)閉動(dòng)畫。 ? ? ? ?
m_Open.SetBool(m_OpenParameterId, false); ? ? ? ?
//將選定元素還原為打開當(dāng)前屏幕之前使用的可選元素。
?SetSelected(m_PreviouslySelected); ? ? ??
?//關(guān)閉動(dòng)畫結(jié)束時(shí)啟動(dòng)協(xié)程以禁用該層級(jí)視圖。?
?StartCoroutine(DisablePanelDeleyed(m_Open)); ? ? ? ?
//無打開屏幕。 ? ? ? ?
m_Open = null; ? ?
} ? ?
//協(xié)程將檢測關(guān)閉動(dòng)畫何時(shí)結(jié)束,并會(huì)停用 ? ?
//層級(jí)視圖。 ? ?
IEnumerator DisablePanelDeleyed(Animator anim)?{ ? ? ? ?
bool closedStateReached = false; ? ? ? ?
bool wantToClose = true; ? ? ? ?
while (!closedStateReached && wantToClose){
if (!anim.IsInTransition(0)) ? ? ? ? ? ? ??
?closedStateReached = anim.GetCurrentAnimatorStateInfo(0).IsName(k_ClosedStateName);
? wantToClose = !anim.GetBool(m_OpenParameterId); ? ? ? ? ? ?
yield return new WaitForEndOfFrame(); ? ? ? ?
} ? ? ? ?
if (wantToClose) ? ? ? ? ? ?
anim.gameObject.SetActive(false); ? ?
} ? ?
//選定提供的游戲?qū)ο? ? ?
//當(dāng)使用鼠標(biāo)/觸摸時(shí),我們實(shí)際上想要將其設(shè)置為先前選定的對(duì)象并且 ? ?
//現(xiàn)在不將任何對(duì)象設(shè)置為選定狀態(tài)。 ? ?
private void SetSelected(GameObject go) ? ?{ ? ? ? ?
//選擇游戲?qū)ο蟆? ? ? ? ?
EventSystem.current.SetSelectedGameObject(go); ? ? ? ?
//如果我們現(xiàn)在正在使用鍵盤,那么我們便完成了所有設(shè)置。 ? ? ? ?
var standaloneInputModule = EventSystem.current.currentInputModule as StandaloneInputModule; ? ? ? ?
if (standaloneInputModule != null) ? ? ? ? ? ?
return; ? ? ? ?
//由于我們使用的是指針設(shè)備,因此我們不希望選擇任何內(nèi)容。 ? ? ? ?
//但是如果用戶切換到鍵盤,我們希望從提供的游戲?qū)ο箝_始導(dǎo)航。 ? ? ? ?
//所以,此處我們將當(dāng)前的選定項(xiàng)設(shè)置為 null,因此提供的游戲?qū)ο蟪蔀?EventSystem 中的最后選定項(xiàng)。 ? ? ? ?
EventSystem.current.SetSelectedGameObject(null); ? ?
} }
讓我們連接此腳本,為此需要?jiǎng)?chuàng)建新的游戲?qū)ο?,我們可以將其重命名為“ScreenManager”之類的名稱,并將上面的組件添加到該游戲?qū)ο?。您可以為其指定初始屏幕,此屏幕將在場景啟?dòng)時(shí)打開。
現(xiàn)在,最后的工作是讓?UI 按鈕生效。選擇應(yīng)觸發(fā)屏幕過渡的按鈕,并在 Inspector 的?On Click ()?列表下添加一個(gè)新操作。將剛創(chuàng)建的 ScreenManager 游戲?qū)ο笸系?ObjectField,在下拉選單中選擇?ScreenManager > OpenPanel (Animator),然后將用戶點(diǎn)擊按鈕時(shí)要打開的面板拖放到 ObjectField。

注意
這種方法只要求每個(gè)屏幕有一個(gè)動(dòng)畫控制器 (AnimatorController) 以及一個(gè) Open 參數(shù)和一個(gè) Closed 狀態(tài)即可發(fā)揮作用(無論屏幕或狀態(tài)機(jī)的構(gòu)造如何)。這種方法也適用于嵌套的屏幕,這意味著每個(gè)嵌套級(jí)別只需要一個(gè) ScreenManager。
我們上面設(shè)置的狀態(tài)機(jī)的默認(rèn)狀態(tài)為 Closed,因此使用此控制器的所有屏幕在開始時(shí)均為關(guān)閉狀態(tài)。ScreenManager 提供了一個(gè) initiallyOpen 屬性,因此可以指定首先顯示的屏幕。