地图部分也已经算是可以告一段落了,也需要仔细考虑下接下来该做哪个系统。地图部分可以算是六边形地图的SLG游戏最主要的一部分,所以先做了下练练手。
接下来的工作更多的需要从项目的全局角度来考虑该怎么做。深思熟虑后,觉得现在比较合适的一个入手点是UI部分,利用UI部分可以将整个游戏的流程搭建起来。
先简单整理了一下,最终结果主要有哪些UI:
我们可以看到,这个游戏的UI还是很简单的,UI的层次也只有两层。
于是,可以这样规划:
将游戏UI简单分为两类:主界面,界面上的弹窗。
主界面:主界面暂时就是图片中第二层那四个:启动界面、开局初始条件设置界面、国家选择界面、游戏内进行界面。
这几个界面之间是互斥的,即游戏进行时只可能打开其中一个界面。也可以近似的看做游戏的不同状态阶段。
界面上的弹窗:这类界面其实就是点了界面上的按钮后,弹出的一些小窗口,这些窗口可能会覆盖掉整个屏幕(比如科技界面),但实际上并没有作场景切换。
这一类界面之间并不互斥,比如我打开了科技界面后,选择一个科技升级,肯定还是需要一个确认的弹窗。不过,虽然界面之间并不互斥,但同一个界面是不能同时存在两个的。所以我们可以和主界面一样,只实例化一个界面,在不同的场合显示不同的信息来反复使用。
首先,建立一个UI管理类,同时也作为所有UI的根目录:
1. public class UIRoot : MonoBehaviour {
2.
3. }
复制代码
然后再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.
8. public void CloseUI()
9. {
10. gameObject.SetActive(false);
11. }
12. }
复制代码
BaseUI是一个抽象类,抽象类是无法实例化的,即我们不能把BaseUI挂到一个gameobject上面去,但是一个继承了BaseUI的类,是可以被挂到一个gameobject上去的。
同样,BaseUI虽然不可以被实例化,但我们可以把BaseUI的一个子类,存放到BaseUI类型的变量上去。
而后,定义一个枚举,用于保存UI的类型,比如:启动界面、开局初始条件设置界面、国家选择界面、游戏主界面等。
1. public enum UIType
2. {
3.
4. }
复制代码
好了,开始做第一个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.
8. }
复制代码
在这个脚本中,我暂时只建了一个字典用于记录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.
5. private Dictionary<UIType, BaseUI> NormalUIDic = new Dictionary<UIType, BaseUI>();
6. private BaseUI CurrentUI;
7.
8.
9. public void OpenNormalUI(UIType uiType)
10. {
11. if (CurrentUI != null)
12. {
13. CurrentUI.CloseUI();
14. }
15. if (NormalUIDic.ContainsKey(uiType))
16. {
17. CurrentUI = NormalUIDic[uiType];
18. }
19. else
20. {
21. BaseUI theUI = Instantiate(Resources.Load<BaseUI>(UIConfig.UIPath[uiType])) as BaseUI;
22. UITool.AddChild(theUI.transform, NormalUI);
23. NormalUIDic.Add(uiType, theUI);
24. CurrentUI = theUI;
25. }
26. CurrentUI.OpenUI();
27. }
28.
29. }
复制代码
我新建了一个字典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. }
10.
11. }
复制代码
点击运行,我们会发现第一个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.
26. }
复制代码
这个函数的作用是搜索某个物体的子物体,并找到第一个名字为childName的子物体。我们需要用这个函数来找到某个UI下的子按钮。
之后是将UIRoot这个脚本改成一个单例:
1. public class UIRoot : MonoBehaviour {
2. public static UIRoot Instance;
3.
4. ……
5. private void Awake()
6. {
7. Instance = this;
8. }
9. ……
10.
11. }
复制代码
这样,我们就可以通过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.
10.
11. private void StartGame()
12. {
13. UIRoot.Instance.OpenNormalUI(UIType.GameSettingUI);
14. }
15. }
复制代码
其实UGUI本身的编辑器功能可以直接让该按钮能够调用某个脚本的某个函数,但是通过编辑器有时候可能会不小心点错,从而出现bug,尤其是在多人合作的项目中,很容易出现这种问题。所以很多人会习惯用代码来实现这一功能。
给GameSettingUI也如法炮制:
修改Button名字
1. using UnityEngine.UI;
2.
3. public class GameSettingUI : BaseUI {
4. private Button backToStartUI;
5. private void Awake()
6. {
7. backToStartUI = UITool.FindChildByName(gameObject, "BackToStartUI").GetComponent<Button>();
8. backToStartUI.onClick.AddListener(StartGame);
9. }
10.
11.
12. public void StartGame()
13. {
14. UIRoot.Instance.OpenNormalUI(UIType.StartUI);
15. }
16. }
复制代码
接下来运行测试一下,就会发现我们可以在两个页面之间切换了。