从零开始做一个SLG游戏(四):UI系统之主界面搭建

作者:观复 2019-10-12
地图部分也已经算是可以告一段落了,也需要仔细考虑下接下来该做哪个系统。地图部分可以算是六边形地图的SLG游戏最主要的一部分,所以先做了下练练手。

接下来的工作更多的需要从项目的全局角度来考虑该怎么做。深思熟虑后,觉得现在比较合适的一个入手点是UI部分,利用UI部分可以将整个游戏的流程搭建起来。

先简单整理了一下,最终结果主要有哪些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这个脚本挂上去。

如图:


然后再右键UIRoot,选择Create Empty,新建一个空物体,命名为Normal UI,作为所有主界面的根目录。

而后将它复制一下,将新的重命名为Keep Above 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类型更为方便一些。然后将两个物体拖到上去。


接下来定义一个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上:




做完后,在将刚刚做好的UI放在Asset/Resources/UIPrefabs目录下(没有这个目录就新建一个),如下图所示:


同时再新建一个脚本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已经被加载了。

加载第一个页面

接下来我们要制作第二个UI来测试这个页面切换功能。

我们翻到前面的UI表,可以得知,第二个UI是游戏设置UI,所以我们建第二个UI,名字命名为GameSettingUI,并在左上角加上一个回退按钮,如图所示:

游戏设置UI

新建一个GameSettingUI:BaseUI脚本,然后将该UI拖到存放StartUI的预设体的那个目录内:

  1. public class GameSettingUI : BaseUI {
  2.        
  3. }
复制代码


接下来要做的就是给两个按钮实现切换功能。但在做这个之前,我们需要在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的按钮:


先将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也如法炮制:

修改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

最新评论
暂无评论
参与评论

商务合作 查看更多

编辑推荐 查看更多