|
地图部分也已经算是可以告一段落了,也需要仔细考虑下接下来该做哪个系统。地图部分可以算是六边形地图的SLG游戏最主要的一部分,所以先做了下练练手。
接下来的工作更多的需要从项目的全局角度来考虑该怎么做。深思熟虑后,觉得现在比较合适的一个入手点是UI部分,利用UI部分可以将整个游戏的流程搭建起来。
先简单整理了一下,最终结果主要有哪些UI:
我们可以看到,这个游戏的UI还是很简单的,UI的层次也只有两层。
于是,可以这样规划:
将游戏UI简单分为两类:主界面,界面上的弹窗。
主界面:主界面暂时就是图片中第二层那四个:启动界面、开局初始条件设置界面、国家选择界面、游戏内进行界面。
这几个界面之间是互斥的,即游戏进行时只可能打开其中一个界面。也可以近似的看做游戏的不同状态阶段。
界面上的弹窗:这类界面其实就是点了界面上的按钮后,弹出的一些小窗口,这些窗口可能会覆盖掉整个屏幕(比如科技界面),但实际上并没有作场景切换。
这一类界面之间并不互斥,比如我打开了科技界面后,选择一个科技升级,肯定还是需要一个确认的弹窗。不过,虽然界面之间并不互斥,但同一个界面是不能同时存在两个的。所以我们可以和主界面一样,只实例化一个界面,在不同的场合显示不同的信息来反复使用。
首先,建立一个UI管理类,同时也作为所有UI的根目录:
- public class UIRoot : MonoBehaviour {
- }
复制代码
然后再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脚本中,新建两个参数:
- public class UIRoot : MonoBehaviour {
- public Transform NormalUI;
- public Transform KeepAboveUI;
- }
复制代码
因为这两个物体主要用于存放窗口,所以存放为transform类型更为方便一些。然后将两个物体拖到上去。
接下来定义一个UI的基类,用于一些UI的基本操作。
- public abstract class BaseUI : MonoBehaviour
- {
- public void OpenUI()
- {
- gameObject.SetActive(true);
- }
- public void CloseUI()
- {
- gameObject.SetActive(false);
- }
- }
复制代码
BaseUI是一个抽象类,抽象类是无法实例化的,即我们不能把BaseUI挂到一个gameobject上面去,但是一个继承了BaseUI的类,是可以被挂到一个gameobject上去的。
同样,BaseUI虽然不可以被实例化,但我们可以把BaseUI的一个子类,存放到BaseUI类型的变量上去。
而后,定义一个枚举,用于保存UI的类型,比如:启动界面、开局初始条件设置界面、国家选择界面、游戏主界面等。
好了,开始做第一个UI启动界面,命名为StartUI。
现在UIType中添加一个对应的枚举,并且新建一个继承自BaseUI的StartUI脚本:
- public enum UIType
- {
- StartUI,
- }
- public class StartUI : BaseUI {
-
- }
复制代码
然后在NormalUI这个物体上新建一个UI,叫StartUI,搭一个简单的UI出来,并且把前面的StartUI脚本拖到该UI上:
做完后,在将刚刚做好的UI放在Asset/Resources/UIPrefabs目录下(没有这个目录就新建一个),如下图所示:
同时再新建一个脚本UIConfig,用于存放一些UI的配置:
- public class UIConfig
- {
- public static Dictionary<UIType, string> UIPath = new Dictionary<UIType, string>
- {
- { UIType.StartUI,"UIPrefabs/StartUI"},
- };
- }
复制代码
在这个脚本中,我暂时只建了一个字典用于记录UI的存放地址。一会用Resources.Load()函数可以读取Resources文件夹里面的内容。
接下来要做的就是UI切换功能了,做之前需要先封装一下添加子物体的函数:
- public class UITool{
- public static void AddChild(Transform child, Transform parent)
- {
- child.SetParent(parent.transform);
- child.localPosition = Vector3.zero;
- child.localScale = Vector3.one;
- child.localEulerAngles = Vector3.zero;
- }
- }
复制代码
这个函数的功能是将一个子物体放在想要添加的父物体上。
而后就是UI的切换功能了:
- public class UIRoot : MonoBehaviour {
- public Transform NormalUI;
- public Transform KeepAboveUI;
- private Dictionary<UIType, BaseUI> NormalUIDic = new Dictionary<UIType, BaseUI>();
- private BaseUI CurrentUI;
- public void OpenNormalUI(UIType uiType)
- {
- if (CurrentUI != null)
- {
- CurrentUI.CloseUI();
- }
- if (NormalUIDic.ContainsKey(uiType))
- {
- CurrentUI = NormalUIDic[uiType];
- }
- else
- {
- BaseUI theUI = Instantiate(Resources.Load<BaseUI>(UIConfig.UIPath[uiType])) as BaseUI;
- UITool.AddChild(theUI.transform, NormalUI);
- NormalUIDic.Add(uiType, theUI);
- CurrentUI = theUI;
- }
- CurrentUI.OpenUI();
- }
- }
复制代码
我新建了一个字典NormalUIDic用于存放所有已经开启过的UI,同时用CurrentUI用于存放当前正在使用的UI。
因为只要游戏开启,则必定存在一个normalUI,所以只有OpenNormalUI,每次开启UI之前,都要把上一个UI关闭。而下面则是一个以前是否开启过该UI的函数,如果开启过,则直接从字典中找到那个UI,并开启,如果以前没有开启过,则加载该UI,并将该UI加入字典。
接下来就是测试一下该功能是否可用,在UIRoot的Start函数中,加入一行代码:
- public class UIRoot : MonoBehaviour {
- ……
- private void Start()
- {
- OpenNormalUI(UIType.StartUI);
- }
- ……
- }
- }
复制代码
点击运行,我们会发现第一个UI已经被加载了。
加载第一个页面
接下来我们要制作第二个UI来测试这个页面切换功能。
我们翻到前面的UI表,可以得知,第二个UI是游戏设置UI,所以我们建第二个UI,名字命名为GameSettingUI,并在左上角加上一个回退按钮,如图所示:
游戏设置UI
新建一个GameSettingUI:BaseUI脚本,然后将该UI拖到存放StartUI的预设体的那个目录内:
- public class GameSettingUI : BaseUI {
-
- }
复制代码
接下来要做的就是给两个按钮实现切换功能。但在做这个之前,我们需要在UITool内封装一个新的工具性的函数:
- public class UITool{
- ……
- public static GameObject FindChildByName(GameObject parent, string childName)
- {
- if (parent.name == childName)
- {
- return parent;
- }
- if (parent.transform.childCount < 1)
- {
- return null;
- }
- GameObject obj = null;
- for (int i = 0; i < parent.transform.childCount; i++)
- {
- GameObject go = parent.transform.GetChild(i).gameObject;
- obj = FindChildByName(go, childName);
- if (obj != null)
- {
- break;
- }
- }
- return obj;
- }
- }
复制代码
这个函数的作用是搜索某个物体的子物体,并找到第一个名字为childName的子物体。我们需要用这个函数来找到某个UI下的子按钮。
之后是将UIRoot这个脚本改成一个单例:
- public class UIRoot : MonoBehaviour {
- public static UIRoot Instance;
- ……
- private void Awake()
- {
- Instance = this;
- }
- ……
- }
复制代码
这样,我们就可以通过UIRoot.Instance来操作UI。
接下来是实现StartUI的按钮:
先将StartUI预制体上的Button按钮重命名为StartGame。而后在StartUI的脚本上,添加如下函数:
- using UnityEngine.UI;
- public class StartUI : BaseUI {
- private Button btn_Start;
- private void Awake()
- {
- btn_Start = UITool.FindChildByName(gameObject, "StartGame").GetComponent<Button>();
- btn_Start.onClick.AddListener(StartGame);
- }
- private void StartGame()
- {
- UIRoot.Instance.OpenNormalUI(UIType.GameSettingUI);
- }
- }
复制代码
其实UGUI本身的编辑器功能可以直接让该按钮能够调用某个脚本的某个函数,但是通过编辑器有时候可能会不小心点错,从而出现bug,尤其是在多人合作的项目中,很容易出现这种问题。所以很多人会习惯用代码来实现这一功能。
给GameSettingUI也如法炮制:
修改Button名字
- using UnityEngine.UI;
- public class GameSettingUI : BaseUI {
- private Button backToStartUI;
- private void Awake()
- {
- backToStartUI = UITool.FindChildByName(gameObject, "BackToStartUI").GetComponent<Button>();
- backToStartUI.onClick.AddListener(StartGame);
- }
- public void StartGame()
- {
- UIRoot.Instance.OpenNormalUI(UIType.StartUI);
- }
- }
复制代码
接下来运行测试一下,就会发现我们可以在两个页面之间切换了。
相关阅读:
从零开始做一个SLG游戏(一):六边形网格
从零开始做一个SLG游戏(二):用mesh实现简单的地形
从零开始做一个SLG游戏(三):用unity绘制图形
作者:观复
专栏地址:https://zhuanlan.zhihu.com/p/48934135
|
|