前言
曾经的老大说过,游戏开发70%跟UI打交道,貌似也是,UI是游戏开发里面的大头,这么说一个不错的UI框架就比较重要,UI界面会比较多,大项目大几十个系统界面,管理不当会造成比较大的麻烦。之前NGUI项目界面层级通过Depth管理界面的显示层级,Depth越大就显示在前面,后来UGUI通过设置SetSiblingIndex,值越大越显示在前面。有时候我们有需求某些界面固定显示在最上面,例如引导界面,有的显示在中间,有的显示在后面,GFFrameWork设置了三层显示层级,加载的界面由开发者自行执行放在哪一层上,然后每一层的界面进行设置显示前后顺序。
说明
本UI结构基于UGUI,后续也考虑对接FairyGUI,一款非常强大的UI编辑器。
UI显示结构
UI界面在Hierarchy中的显示
分成三层显示,每一层的就Order in Layer不同,数值越大的越显示在前面,动态加载的UI根据需要显示在具体的哪一层。
UIManager和窗口
窗口基类 Window_Base
主要就是窗口的管理(初始化、打开、关闭、消息管理),加载对应的界面预设,子窗口管理(添加、打开、关闭)
UIManager设计
主要是UI的加载(同步、异步)和管理(、显示、卸载、获取、关闭、获取UI状态),发送消息
UI导航
我们如何打开一个UI界面呢?这里采用类似iOS开发中界面跳转,导航跳转。
我们每创建一个需要跳转显示的界面,我们要创建两个脚本,一个UI的View界面脚本,一个导航脚本:
跳转界面
1 | ScreenViewManager.Inst.MainLayer.BeginNavTo("main"); |
参数是导航脚本的特性的值
导航跳转原理
上面的截图会发现导航脚本类都继承自IScreenView接口,这个用于导航管理器搜集所有导航组件用的。
接口里面重要的是BeginInit和BeginExit方法,是导航跳转到这个界面和离开这个界面触发的事件方法。所有这些导航跳转脚本都归ScreenViewLayer管理,
这个其实就是导航组件Manager,既然是Manager,那肯定是有所有导航组件的管理器的,
当调用了跳转方法的时候会将这个界面的导航组件添加进这个navViews
1 | /// <summary> |
这样以便向前,向后跳转。也可以导航到具体的界面,这个也有点跟Cocos的UI管理类似。
根据导航组件加载对应的UI
根据特性标记搜集导航组件,当调用ScreenViewManager.Inst.MainLayer.BeginNavTo(“main”)的时候,导航器会找到对应UI的导航组件然后调用他的BeinInit()方法,在这个方法里面我们去加载想要加载的UI。
以上就是UI导航的原理。
UI自动获取属性
由于C#代码热更,所以不能将C#代码直接挂在Prefab上,我们加载出来的Prefab然后动态给他挂在代码组件,UI组件脚本原来的定义一个组件变量,我们就要在Start或者Awake里面取Find然后GetComponent,如果变量很多就会显得这样的代码很多,感觉很冗余,这里采用自定义特性去找对应的UI上的组件节点。
1 | public class TransformPath : Attribute |
原理
在界面加载的时候,UITools会调用AutoSetTransformPath方法找到这个界面上特性绑定的节点1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37static public void AutoSetTransformPath(WindowBase win)
{
var vt = win.GetType();
var fields = vt.GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
var vTransform = win.Transform;
foreach (var f in fields)
{
if (f.FieldType.IsSubclassOf(checkType) == false)
{
continue;
}
//1.自动获取节点
var _attrs = f.GetCustomAttributes(typeof(TransformPath), false); //as Attribute[];
if (_attrs != null && _attrs.Length > 0)
{
var attr = _attrs.ToList().Find((a) => a is TransformPath) as TransformPath;
if (attr == null) continue;
//获取节点,并且获取组件
var trans = vTransform.Find(attr.Path);
if (trans == null)
{
Debugger.LogError(string.Format("自动设置节点失败:{0} - {1}", vt.FullName, attr.Path));
}
var com = trans.GetComponent(f.FieldType);
if (com == null)
{
Debugger.LogError(string.Format("节点没有对应组件:type【{0}】 - {1}", f.FieldType, attr.Path));
}
//设置属性
f.SetValue(win, com);
//Debug.LogFormat("字段{0}获取到setTransform ,path:{1}" , f.Name , attr.Path);
}
}
#endregion
}
UI之间消息传递
UI窗口之间交互通讯都通过发送消息来实现,这样可以减少UI模块之间的耦合,消息是这样的一个结构:
消息发送
里面主要是一个字典结构,如何发送消息如下:1
2
3var data = WindowData.Create("AddDial"); //创建一个消息结构,参数是消息名,用于监听,内部会创建一个消息字典
data.AddData("key", tempDialData); //添加消息键值对
UIManager.Inst.SendMessage((int)WinEnum.Win_Menu, data); //通过UI接口发送消息,第一个参数是接受界面ID
消息接受
在接受消息界面添加消息监听,第一个参数是监听的消息名
获取消息内容
消息发送原理
或许就有人疑惑,为啥注册这个监听,通过UImanager的发送消息就能收到消息回调呢?
每一个界面都继承自Window_Base,WindowsBase里面有一个消息回调表1
protected Dictionary<string, Action<WindowData>> callbackMap;
还有消息注册监听方法,也就是我们消息接受调用的监听方法1
2
3
4protected void RegisterAction(string name, Action<WindowData> callback)
{
callbackMap[name] = callback;
}
还有发送消息的方法1
2
3
4
5
6
7
8
9public void SendMessage(WindowData data)
{
Action<WindowData> action = null;
callbackMap.TryGetValue(data.Name, out action);
if (action != null)
{
action(data);
}
}
然后我们在看看UIManager的发送消息,Manager里面缓存了所有的界面,然后根据想要发送消息的界面ID找到对应的界面调用上面的界面的SendMessage方法,这样就明白消息发送的原理了。1
2
3
4
5
6
7
8
9
10
11
12
13
14public void SendMessage(int Index, WindowData data)
{
var uiIndex = Index.GetHashCode();
if (windowMap.ContainsKey(uiIndex))
{
var ui = windowMap[uiIndex];
if (ui.IsLoad)
{
ui.SendMessage(data);
return;
}
}
}
GFFramework地址
https://github.com/dingxiaowei/GFFrameWork