從零開始做一個SLG遊戲(四):UI系統之主介面搭建

遊資網發表於2019-10-12
地圖部分也已經算是可以告一段落了,也需要仔細考慮下接下來該做哪個系統。地圖部分可以算是六邊形地圖的SLG遊戲最主要的一部分,所以先做了下練練手。

接下來的工作更多的需要從專案的全域性角度來考慮該怎麼做。深思熟慮後,覺得現在比較合適的一個入手點是UI部分,利用UI部分可以將整個遊戲的流程搭建起來。

先簡單整理了一下,最終結果主要有哪些UI:

從零開始做一個SLG遊戲(四):UI系統之主介面搭建

我們可以看到,這個遊戲的UI還是很簡單的,UI的層次也只有兩層。

於是,可以這樣規劃:

將遊戲UI簡單分為兩類:主介面,介面上的彈窗。

主介面:主介面暫時就是圖片中第二層那四個:啟動介面、開局初始條件設定介面、國家選擇介面、遊戲內進行介面。

這幾個介面之間是互斥的,即遊戲進行時只可能開啟其中一個介面。也可以近似的看做遊戲的不同狀態階段。

介面上的彈窗:這類介面其實就是點了介面上的按鈕後,彈出的一些小視窗,這些視窗可能會覆蓋掉整個螢幕(比如科技介面),但實際上並沒有作場景切換。

這一類介面之間並不互斥,比如我開啟了科技介面後,選擇一個科技升級,肯定還是需要一個確認的彈窗。不過,雖然介面之間並不互斥,但同一個介面是不能同時存在兩個的。所以我們可以和主介面一樣,只例項化一個介面,在不同的場合顯示不同的資訊來反覆使用。

首先,建立一個UI管理類,同時也作為所有UI的根目錄:

  1. public class UIRoot : MonoBehaviour {

  2. }
複製程式碼

然後再Unity的Hierarchy介面上,右鍵UI/Canvas新建一個幕布,這時候會自動新增一個Canvas和一個EventSystem,將EventSystem拖到Canvas下作為一個子物體(也可以不拖,這麼做純粹是為了把UI相關的東西都放到一個gameobject下)。而後將Canvas重新命名為UIRoot,然後把UIRoot這個指令碼掛上去。

如圖:

從零開始做一個SLG遊戲(四):UI系統之主介面搭建

然後再右鍵UIRoot,選擇Create Empty,新建一個空物體,命名為Normal UI,作為所有主介面的根目錄。

而後將它複製一下,將新的重新命名為Keep Above UI,作為所有介面彈窗的根目錄。

從零開始做一個SLG遊戲(四):UI系統之主介面搭建

如圖所示,只要在這個介面上使得Keep Above UI位於Normal UI的下方,在遊戲顯示中,就會優先顯示Keep Above UI這個物體上的介面。因為在ugui中,下方的物體會被優先顯示。

而後我們在UIRoot指令碼中,新建兩個引數:

  1. public class UIRoot : MonoBehaviour {
  2.         public Transform NormalUI;
  3.         public Transform KeepAboveUI;
  4. }
複製程式碼

因為這兩個物體主要用於存放視窗,所以存放為transform型別更為方便一些。然後將兩個物體拖到上去。

從零開始做一個SLG遊戲(四):UI系統之主介面搭建

接下來定義一個UI的基類,用於一些UI的基本操作。

  1. public abstract class BaseUI : MonoBehaviour
  2. {
  3.         public void OpenUI()
  4.         {
  5.                 gameObject.SetActive(true);
  6.         }

  7.         public void CloseUI()
  8.         {
  9.                 gameObject.SetActive(false);
  10.         }
  11. }
複製程式碼

BaseUI是一個抽象類,抽象類是無法例項化的,即我們不能把BaseUI掛到一個gameobject上面去,但是一個繼承了BaseUI的類,是可以被掛到一個gameobject上去的。

同樣,BaseUI雖然不可以被例項化,但我們可以把BaseUI的一個子類,存放到BaseUI型別的變數上去。

而後,定義一個列舉,用於儲存UI的型別,比如:啟動介面、開局初始條件設定介面、國家選擇介面、遊戲主介面等。

  1. public enum UIType
  2. {

  3. }
複製程式碼

好了,開始做第一個UI啟動介面,命名為StartUI。

現在UIType中新增一個對應的列舉,並且新建一個繼承自BaseUI的StartUI指令碼:

  1. public enum UIType
  2. {
  3.         StartUI,
  4. }
  5. public class StartUI : BaseUI {
  6.        
  7. }
複製程式碼

然後在NormalUI這個物體上新建一個UI,叫StartUI,搭一個簡單的UI出來,並且把前面的StartUI指令碼拖到該UI上:

從零開始做一個SLG遊戲(四):UI系統之主介面搭建

從零開始做一個SLG遊戲(四):UI系統之主介面搭建

從零開始做一個SLG遊戲(四):UI系統之主介面搭建

做完後,在將剛剛做好的UI放在Asset/Resources/UIPrefabs目錄下(沒有這個目錄就新建一個),如下圖所示:

從零開始做一個SLG遊戲(四):UI系統之主介面搭建

同時再新建一個指令碼UIConfig,用於存放一些UI的配置:

  1. public class UIConfig
  2. {
  3.         public static Dictionary<UIType, string> UIPath = new Dictionary<UIType, string>
  4.         {
  5.                 { UIType.StartUI,"UIPrefabs/StartUI"},
  6.         };

  7. }
複製程式碼

在這個指令碼中,我暫時只建了一個字典用於記錄UI的存放地址。一會用Resources.Load()函式可以讀取Resources資料夾裡面的內容。

接下來要做的就是UI切換功能了,做之前需要先封裝一下新增子物體的函式:

  1. public class UITool{
  2.         public static void AddChild(Transform child, Transform parent)
  3.         {
  4.                 child.SetParent(parent.transform);
  5.                 child.localPosition = Vector3.zero;
  6.                 child.localScale = Vector3.one;
  7.                 child.localEulerAngles = Vector3.zero;
  8.         }
  9. }
複製程式碼

這個函式的功能是將一個子物體放在想要新增的父物體上。

而後就是UI的切換功能了:

  1. public class UIRoot : MonoBehaviour {
  2.         public Transform NormalUI;
  3.         public Transform KeepAboveUI;

  4.         private Dictionary<UIType, BaseUI> NormalUIDic = new Dictionary<UIType, BaseUI>();
  5.         private BaseUI CurrentUI;


  6.         public void OpenNormalUI(UIType uiType)
  7.         {
  8.                 if (CurrentUI != null)
  9.                 {
  10.                         CurrentUI.CloseUI();
  11.                 }
  12.                 if (NormalUIDic.ContainsKey(uiType))
  13.                 {
  14.                         CurrentUI = NormalUIDic[uiType];
  15.                 }
  16.                 else
  17.                 {
  18.                         BaseUI theUI = Instantiate(Resources.Load<BaseUI>(UIConfig.UIPath[uiType])) as BaseUI;
  19.                         UITool.AddChild(theUI.transform, NormalUI);
  20.                         NormalUIDic.Add(uiType, theUI);
  21.                         CurrentUI = theUI;
  22.                 }
  23.                 CurrentUI.OpenUI();
  24.         }

  25. }
複製程式碼

我新建了一個字典NormalUIDic用於存放所有已經開啟過的UI,同時用CurrentUI用於存放當前正在使用的UI。

因為只要遊戲開啟,則必定存在一個normalUI,所以只有OpenNormalUI,每次開啟UI之前,都要把上一個UI關閉。而下面則是一個以前是否開啟過該UI的函式,如果開啟過,則直接從字典中找到那個UI,並開啟,如果以前沒有開啟過,則載入該UI,並將該UI加入字典。

接下來就是測試一下該功能是否可用,在UIRoot的Start函式中,加入一行程式碼:

  1. public class UIRoot : MonoBehaviour {
  2.         ……
  3.         private void Start()
  4.         {
  5.                 OpenNormalUI(UIType.StartUI);
  6.         }

  7.         ……
  8.         }

  9. }
複製程式碼


點選執行,我們會發現第一個UI已經被載入了。

從零開始做一個SLG遊戲(四):UI系統之主介面搭建
載入第一個頁面

接下來我們要製作第二個UI來測試這個頁面切換功能。

我們翻到前面的UI表,可以得知,第二個UI是遊戲設定UI,所以我們建第二個UI,名字命名為GameSettingUI,並在左上角加上一個回退按鈕,如圖所示:

從零開始做一個SLG遊戲(四):UI系統之主介面搭建
遊戲設定UI

新建一個GameSettingUI:BaseUI指令碼,然後將該UI拖到存放StartUI的預設體的那個目錄內:

  1. public class GameSettingUI : BaseUI {
  2.        
  3. }
複製程式碼

從零開始做一個SLG遊戲(四):UI系統之主介面搭建

接下來要做的就是給兩個按鈕實現切換功能。但在做這個之前,我們需要在UITool內封裝一個新的工具性的函式:

  1. public class UITool{
  2.         ……
  3.         public static GameObject FindChildByName(GameObject parent, string childName)
  4.         {
  5.                 if (parent.name == childName)
  6.                 {
  7.                         return parent;
  8.                 }
  9.                 if (parent.transform.childCount < 1)
  10.                 {
  11.                         return null;
  12.                 }
  13.                 GameObject obj = null;
  14.                 for (int i = 0; i < parent.transform.childCount; i++)
  15.                 {
  16.                         GameObject go = parent.transform.GetChild(i).gameObject;
  17.                         obj = FindChildByName(go, childName);
  18.                         if (obj != null)
  19.                         {
  20.                                 break;
  21.                         }
  22.                 }
  23.                 return obj;
  24.         }

  25. }
複製程式碼

這個函式的作用是搜尋某個物體的子物體,並找到第一個名字為childName的子物體。我們需要用這個函式來找到某個UI下的子按鈕。

之後是將UIRoot這個指令碼改成一個單例:

  1. public class UIRoot : MonoBehaviour {
  2.         public static UIRoot Instance;

  3. ……
  4.         private void Awake()
  5.         {
  6.                 Instance = this;
  7.         }
  8. ……

  9. }
複製程式碼

這樣,我們就可以通過UIRoot.Instance來操作UI。

接下來是實現StartUI的按鈕:

從零開始做一個SLG遊戲(四):UI系統之主介面搭建

先將StartUI預製體上的Button按鈕重新命名為StartGame。而後在StartUI的指令碼上,新增如下函式:

  1. using UnityEngine.UI;
  2. public class StartUI : BaseUI {
  3.         private Button btn_Start;
  4.         private void Awake()
  5.         {
  6.                 btn_Start = UITool.FindChildByName(gameObject, "StartGame").GetComponent<Button>();
  7.                 btn_Start.onClick.AddListener(StartGame);
  8.         }


  9.         private void StartGame()
  10.         {
  11.                 UIRoot.Instance.OpenNormalUI(UIType.GameSettingUI);
  12.         }
  13. }
複製程式碼

其實UGUI本身的編輯器功能可以直接讓該按鈕能夠呼叫某個指令碼的某個函式,但是通過編輯器有時候可能會不小心點錯,從而出現bug,尤其是在多人合作的專案中,很容易出現這種問題。所以很多人會習慣用程式碼來實現這一功能。

給GameSettingUI也如法炮製:

從零開始做一個SLG遊戲(四):UI系統之主介面搭建
修改Button名字

  1. using UnityEngine.UI;

  2. public class GameSettingUI : BaseUI {
  3.         private Button backToStartUI;
  4.         private void Awake()
  5.         {
  6.                 backToStartUI = UITool.FindChildByName(gameObject, "BackToStartUI").GetComponent<Button>();
  7.                 backToStartUI.onClick.AddListener(StartGame);
  8.         }


  9.         public void StartGame()
  10.         {
  11.                 UIRoot.Instance.OpenNormalUI(UIType.StartUI);
  12.         }
  13. }
複製程式碼


接下來執行測試一下,就會發現我們可以在兩個頁面之間切換了。

相關閱讀:

從零開始做一個SLG遊戲(一):六邊形網格
從零開始做一個SLG遊戲(二):用mesh實現簡單的地形
從零開始做一個SLG遊戲(三):用unity繪製圖形

作者:觀復
專欄地址:https://zhuanlan.zhihu.com/p/48934135

相關文章