这个UI模块不好评价,其结构和唐老师的小框架里的UI模块大差不差,只不过多了一些功能罢了。
BaseUIPanel
UI基类, 亮点是有一个UIType属性。
private UIType _currentUIType = new UIType();
internal UIType CurrentUIType
{
get => _currentUIType;
set => _currentUIType = value;
}
其他方法比较基础, 属于是常见设计
- Display() 显示面板
- Hiding() 隐藏面板
- Redisplay() 刷新面板
- ClearEvent() 清空UI事件
- DestoryPanel() 删除面板
值得注意的是显示和隐藏面板方法里额外的处理,这个处理和UIType有关
//页面显示
public virtual void Display()
{
gameObject.SetActive(true);
if (_currentUIType.UIPanelType == UIPanelType.PopUp)
{
//创建蒙版
}
}
//页面隐藏
public virtual void Hiding()
{
gameObject.SetActive(false);
if (_currentUIType.UIPanelType == UIPanelType.PopUp)
{
//蒙版关闭
UIMaskManager.GetInstance().CancelMaskPanel();
}
}
另外值得注意的一点是这个UI框架没有常用的UI生命周期设计 。
例如 :
- OnShow
- OnHide
- OnInit
- OnDestroy
- OnUpdate
- OnPause
…
挺怪的。我一直以为UI生命周期是UI框架的标配来着。
UIType
UIType就是用来判断显示和隐藏的时候如何用UI蒙版进行处理的。
public class UIType : MonoBehaviour
{
public UIPanelType UIPanelType = UIPanelType.Normal;
public UIPanelShowMode UIPanelShowMode = UIPanelShowMode.Normal;
public UIPanelTransparencyType UIPanelTransparencyType = UIPanelTransparencyType.Transparent;
}
//UI面板的类型
public enum UIPanelType
{
//普通
Normal,
//弹出面板
PopUp,
}
//UI面板的显示类型
public enum UIPanelShowMode
{
//普通
Normal,
//隐藏其他
HideOther,
}
//UI面板的透明类型
public enum UIPanelTransparencyType
{
//完全透明,不能穿透
Transparent,
//半透明,不能穿透
Tanslucent,
//低透明度,不能穿透
LowTransparency,
//可以穿透
Panetrate,
}
UIMaskManager
这是一个UI蒙版管理器,就是用来和UIType配合,进行对应的处理的。
核心方法如下 :
public void SetMaskPanel(GameObject displayPanel, UIPanelTransparencyType transparencyType = UIPanelTransparencyType.Transparent )
{
Color newColor = Color.black;
switch (transparencyType)
{
case UIPanelTransparencyType.Transparent:
_MaskPanel.SetActive(true);
newColor = new Color(255/255F, 255/255F, 255/255F, 0F/255F);
_MaskPanel.GetComponent<Image>().color = newColor;
break;
case UIPanelTransparencyType.Tanslucent:
_MaskPanel.SetActive(true);
newColor = new Color(220 / 255F, 220 / 255F, 220 / 255F, 50F / 255F);
_MaskPanel.GetComponent<Image>().color = newColor;
break;
case UIPanelTransparencyType.LowTransparency:
_MaskPanel.SetActive(true);
newColor = new Color(50 / 255F, 50 / 255F, 50 / 255F, 200F / 255F);
_MaskPanel.GetComponent<Image>().color = newColor;
break;
case UIPanelTransparencyType.Panetrate:
_MaskPanel.SetActive(false);
break;
default:
break;
}
_MaskPanel.transform.SetAsLastSibling();
displayPanel.transform.SetAsLastSibling();
}
说实话作为每一个面板每次显示隐藏都会调用到的方法,能抠搜点性能的地方还是要抠搜一点的。
- Image组件可以Awake的时候获取一次然后保存引用就好,没必要每次调用都Get一遍。
- Color也是,需要用到的Color可以提前在方法外面定义好,定义成常量来使用的。
- SetActive可以改成切换Layer(设立一个HideLayer),切换Layer比SetActive消耗少。
UIManager
很明显这会是一个单例类。
为什么我会说这个UI模块很像唐老师的小框架的UI模块,其实大部分都是这个UIManager既视感太重了。
嘛,让我们慢慢鉴赏。
核心字典容器
// 保存所有的UI面板
private Dictionary<string, BaseUIPanel> _dicAllUIPanel;
// 当前显示的UI面板
private Dictionary<string, BaseUIPanel> _dicCurrentShowUIPanel;
// 要移除的UI面板
private Dictionary<string, BaseUIPanel> _dicRemoveUIPanel;
唐老师的UI小框架的核心是一个PanenlDic,这里分成了三个。
其目的应该是为了缓存UI,自动延迟卸载功能,但是这个功能实现不算太好,为什么呢?
来看看Update吧。
void Update()
{
_timer += Time.deltaTime;
if (_timer > 60)
{
_timer = 0;
if (_dicRemoveUIPanel.Count == 0) return;
foreach (KeyValuePair<string, BaseUIPanel> item in _dicRemoveUIPanel.ToList())
{
BaseUIPanel baseUIPanel = null;
_dicAllUIPanel.TryGetValue(item.Key, out baseUIPanel);
if (baseUIPanel != null)
{
_dicAllUIPanel.Remove(item.Key);
baseUIPanel.ClearEvent();
baseUIPanel.DestoryPanel();
}
}
_dicRemoveUIPanel.Clear();
}
}
这里是有一个timer进行计数,每隔1分钟清理了一次dicRemoveUIPanel里的面板。
我说的实现的不太好,其实是说这个实现有点不优雅。
假如上一帧有一个面板进入移除字典,下一帧移除字典就累计满1分钟清空了,这个作为缓存字典缓存了个寂寞。
其实更好的实现方式是用一个类对UIPanel进行包装, 如下。
public class RemoveItem
{
float _timer;
BaseUIPanel _uiPanel;
}
然后每次Update对这个RemoveItem里的timer进行累加,满足一分钟后再释放。
那除了这个缓存功能这三个字典还有其他功能吗?
没有,就是为了缓存功能这点醋包了这个饺子,但是这个醋也不香啊。
层级功能
对应唐老师UI小框架里的E_UI_LayerLayer
// 根节点
private Transform _rootCanvasTrans = null;
// 三个子层级
private Transform _normalNodeTrans = null;
private Transform _popUpNodeTrans = null;
private Transform _uiScriptsNodeTrans = null;
然后是初始化, 值得注意的是这里把CanvasRoot的路径挪到外面定义了
//加载Canvas的Prefab,初始化各个node
void InitUIRes()
{
GameObject canvasRes = Resources.Load<GameObject>(SysDefine.CanvasPrefabPath);
GameObject canvasGO = null;
if (canvasRes != null)
{
canvasGO = Instantiate(canvasRes);
}
else
{
Debug.LogError("Canvas路径错误");
return;
}
_rootCanvasTrans = canvasGO.transform;
_normalNodeTrans = _rootCanvasTrans.Find(SysDefine.CanvasNormalNodeName);
_popUpNodeTrans = _rootCanvasTrans.Find(SysDefine.CanvasPopUpNodeName);
_uiScriptsNodeTrans = _rootCanvasTrans.Find(SysDefine.CanvasScriptsNodeName);
transform.SetParent(_uiScriptsNodeTrans, false);
}
不仅如此, Panel名也定义了对应的常量。
也算是一点小优化吧。
public class SysDefine
{
public const string CSVUIPrefabPath = @"UIData\UIPanelPrefabPath";
public const string CanvasPrefabPath = @"UIPrefabs\Canvas";
public const string CanvasNormalNodeName = "Normal";
public const string CanvasPopUpNodeName = "PopUp";
public const string CanvasScriptsNodeName = "Scripts";
public const string CanvasMaskNodeName = "_UIMaskPanel";
public const string MainPanel = "MainPanel";
public const string OptionsPanel = "OptionsPanel";
public const string ProducersPanel = "ProducersPanel";
public const string SelectLevelPanel = "SelectLevelPanel";
public const string CyclopPanel = "CyclopPanel";
public const string WarPreparePanel = "WarPreparePanel";
public const string LoadingPanel = "LoadingPanel";
public const string CombatPanel = "CombatPanel";
}
路径加载
这个更厉害,我愿意称之为脱裤子放屁。
// UI面板的prefabs路径
private Dictionary<string, string> _dicPanelResPath;
然后这个路径数据要在awake初始化,通过下面的方法。
//从配置表中读取UI面板的路径
void InitUIPanelPathData()
{
if (_dicPanelResPath != null)
{
UnityHelper.LoadUIPrefabPath(_dicPanelResPath);
}
}
//从csv中读取UIPrefab的路径信息
public static void LoadUIPrefabPath(Dictionary<string, string> dicUIPrefabPath)
{
TextAsset binAsset = Resources.Load<TextAsset>(SysDefine.CSVUIPrefabPath);
string text = binAsset.text;
string[] lineArray = text.Split('\n');
for (int i = 1; i < lineArray.Length - 1; i++)
{
string[] rowArray = lineArray[i].Split(',');
dicUIPrefabPath.Add(rowArray[0], rowArray[1]);
}
}
下面就是路径文件信息
咋一看挺不错,通过配表获得对应的路径,好高大上。
仔细一看,这是相对路径,而且统一放在了Resource的UIPrefab文件夹里。
加载也是通过Resource加载的。下面是UIManager里关于面板加载的代码。
private BaseUIPanel LoadUIPanel(string uiPanelName)
{
string strUIPanelPath = "";
GameObject uiPrefabGo = null;
BaseUIPanel baseUIPanel = null;
_dicPanelResPath.TryGetValue(uiPanelName, out strUIPanelPath);
if (string.IsNullOrEmpty(strUIPanelPath) == false)
{
GameObject uiPrefabRes = Resources.Load<GameObject>(strUIPanelPath.Trim());
uiPrefabGo = Instantiate(uiPrefabRes);
}
if (uiPrefabGo != null)
{
baseUIPanel = uiPrefabGo.GetComponent<BaseUIPanel>();
switch (baseUIPanel.CurrentUIType.UIPanelType)
{
case UIPanelType.Normal:
uiPrefabGo.transform.SetParent(_normalNodeTrans,false);
break;
case UIPanelType.PopUp:
uiPrefabGo.transform.SetParent(_popUpNodeTrans, false);
break;
default:
break;
}
uiPrefabGo.SetActive(false);
_dicAllUIPanel.Add(uiPanelName, baseUIPanel);
return baseUIPanel;
}
Debug.LogError("加载UI面板出现未知错误");
return null;
}
而且教程里这里还出现了Bug,原因是通过读表获得的加载路径前后有空格。
经过排查之后发现还需要用Trim()方法去除前后空格才能获得正确路径进而通过Resource进行加载。
这还不如像唐老师小框架一样,直接拼接字符串呢?都是统一命名,都放在同一位置!
都是Resouce加载,搞得这么花里胡哨的干啥啊!
UI业务层
我们来看看UI业务层吧。
这里贴一个代码比较短的Panel,节省点理智消耗。
public class MainPanel : BaseUIPanel
{
public Button levelSelectButton;
public Button optionsButton;
public Button producersButton;
public Button quitButton;
private void Awake()
{
base.CurrentUIType.UIPanelType = UIPanelType.Normal;
base.CurrentUIType.UIPanelShowMode = UIPanelShowMode.Normal;
base.CurrentUIType.UIPanelTransparencyType = UIPanelTransparencyType.Transparent;
levelSelectButton.onClick.RemoveAllListeners();
optionsButton.onClick.RemoveAllListeners();
producersButton.onClick.RemoveAllListeners();
quitButton.onClick.RemoveAllListeners();
levelSelectButton.onClick.AddListener(OnLevelSelectButtonClick);
optionsButton.onClick.AddListener(OnOptionButtonClick);
producersButton.onClick.AddListener(OnProducersButtonClick);
quitButton.onClick.AddListener(OnQuitButtonClick);
}
void OnLevelSelectButtonClick()
{
//MainPanel关闭
UIManager.GetInstance().CloseOrReturnUIPanel(SysDefine.MainPanel);
//加载选关的场景
LevelLoad.Instance.LoadScene(0, false);
}
void OnOptionButtonClick()
{
UIManager.GetInstance().ShowUIPanel(SysDefine.OptionsPanel);
}
void OnProducersButtonClick()
{
UIManager.GetInstance().ShowUIPanel(SysDefine.ProducersPanel);
}
void OnQuitButtonClick()
{
Application.Quit();
}
}
可以看到这里的写法和唐老师的小框架没有什么不同, 除了在Awake里需要对UIType进行赋值,这是强制性的。
然后控件的获取也没有像唐老师的小框架一样进行一些小优化。
闲话
随便说俩点,我能想到的小的优化点吧。
小优化
这UIType手动赋值实在不优雅,完全可以通过特性来解决这个问题。
定制一个UI特性用来标注UI面板类型, UI显示类型,UI透明类型。
然后在面板加载的时候通过反射获得特性信息对UIType进行赋值即可。
小优化2
或者更进一步,使用代码生成, 把UIType配置在表格里。
既然面板配了表,那么就不能让这个表格像上面一样变成脱裤子放屁的笑话。
完全可以通过表格获得面板名字,然后自动生成代码。
当然要避免代码覆盖,要加限定条件,仅限还没有自动生成过面板的Panel,可以自动生成面板类代码。
这个还可以更进一步,通过表格里的路径读取Prefab,然后遍历Prefab里的UI控件,自动生成UI控件代码。
这样这个表格就还算有点有点用处。