目前我们的游戏暂停界面并不是弹窗的形式,而是覆盖掉了之前的GameUI,这是不合理的。

在这里我们引入UI分层的概念,定义好每个UI的层级关系,让每种UI更合理地显示在屏幕上。

UILayer枚举

Enums中定义三个UI层级

1
2
3
4
5
6
7
public enum UILayer
{
BASE_UI,
MIDDLE_UI,
TOP_UI,
COUNT
}
  • 每个UI层级内的UI界面互相覆盖
  • 不同UI层级的UI界面可以同时显示

这里只定义了三个层级,一般够用了。

UILayerMgr

我们定义好三个层级,需要在游戏启动时根据三个层级创建UI父物体,然后新建的UI根据自己的层级自动挂载到对应的UI父物体上。

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
37
38
39
40
41
42
43
44
45
using System.Collections.Generic;
using UnityEngine;

public class UILayerMgr : NormalSingleton<UILayerMgr>,IInit
{
private Dictionary<UILayer, RectTransform> _uiParentDic;
public void Init()
{
_uiParentDic = new();
var canvas = GameObject.FindObjectOfType<Canvas>();
if(canvas == null )
{
Debug.LogError("当前场景中未发现Canvas");
return;
}
for (UILayer i = UILayer.BASE_UI; i < UILayer.COUNT; i++)
{
GameObject child = new(i.ToString());
child.transform.SetParent(canvas.transform);
var rect = child.AddComponent<RectTransform>();
rect.anchoredPosition = Vector2.zero;
_uiParentDic.Add(i, rect);
}
}

public void SetParent(string path, Transform trans)
{
var layer = GetLayer(path);
var parent = _uiParentDic[layer];
trans.SetParent(parent);
}
public UILayer GetLayer(string path)
{
if( UILayerConfig.Layers.TryGetValue(path, out UILayer layer) )
{
return layer;
}
else
{
Debug.LogError("当前prefab未指定层级配置:" + path);
return UILayer.BASE_UI;
}

}
}

UIMgr

修改UIMgr的UI显示逻辑

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
37
38
39
40
41
42
43
/// <summary>
/// 显示界面
/// </summary>
/// <param name="path">界面路径</param>
public IView Show(string path)
{
if(uiStack.Count > 0)
{
string pre = uiStack.Peek();
if (GetLayer(pre) >= GetLayer(path))//+++
{
HideAll(viewsDic[pre]);
}
}
//...
}
private UILayer GetLayer(string path)
{
return UILayerMgr.Instance.GetLayer(path);
}
private IView InitView(string path)
{
if (viewsDic.ContainsKey(path))
{
return viewsDic[path];
}
else
{
GameObject go = LoadMgr.Instance.LoadPrefabAndInstantiate(path,Canvas.transform);
if (go != null)
{
InitLayer(path, go.transform);//+++
AddTypeComponent(go, path);
//...
}
else
Debug.LogError("没有找到对应的Prefab:" + path); return null;
}
}
private void InitLayer(string path, Transform viewTrans)//+++
{
UILayerMgr.Instance.SetParent(path, viewTrans);
}

注意,在实际的UI层级管理中,关闭掉一个MIDDLE_UI,一定会连带关闭掉它之前的BASE_UI,在这里并没有实现。在Show方法中,我们只判断了if (GetLayer(pre) >= GetLayer(path)),然后关闭掉了之前的UI。所以在设计层面,我们打开Middle UI时一定不能在Middle UI中打开一个Base UI。

UILayerConfig

每个UI的Prefab都需要指定自己所属的层级,我们把它写在一个Config当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System.Collections.Generic;

public class UILayerConfig
{
public static readonly Dictionary<string, UILayer> Layers = new()
{
{Pathes.START_VIEW, UILayer.BASE_UI },
{Pathes.SELECT_HERO_VIEW, UILayer.BASE_UI },
{Pathes.STRENGTHEN_VIEW, UILayer.BASE_UI },
{Pathes.LOADING_VIEW, UILayer.BASE_UI },
{Pathes.GAMEUI_VIEW, UILayer.BASE_UI },
{Pathes.LEVELS_VIEW, UILayer.BASE_UI },
{Pathes.PAUSE_VIEW, UILayer.MIDDLE_UI },
{Pathes.DIALOG_VIEW, UILayer.TOP_UI }
};
}

LifeCycleAddConfig

在生命周期初始化部分添加UILayerMgr

1
2
3
4
5
6
7
8
private void Add()
{
//优先级0
Objects.Add(UILayerMgr.Instance);//+++
Objects.Add(ConfigMgr.Instance);
Objects.Add(new InitCustomAttributes());
//...
}

剔除LoadingUI

当前游戏从主界面进入游戏界面时,我们的UI入栈顺序为:

…LEVELS——>LOADING——>GAMEUI。

所以当我们想从GameUI界面返回主界面时,会回退到LOADING界面,这是不合理的。我们应该让Loading界面不参与入栈。

修改UIMgr

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
private readonly HashSet<string> _skipViews = new()//+++
{
Pathes.LOADING_VIEW
};
public IView Show(string path)
{
if(uiStack.Count > 0)
{
//...
}
IView view = InitView(path);
if (view != null)
{
ShowAll(view);
if(!_skipViews.Contains(path))//+++
uiStack.Push(path);
viewsDic[path] = view;
}
return view;
}
/// <summary>
/// 单独隐藏不被UIStack管理的UI
/// </summary>
/// <param name="name">UI路径</param>
public void Hide(string name)
{
HideAll(viewsDic[name]);
}

修改Show方法,让UIMgr显示UI时跳过skipViews的UI,不让它加入uiStack。

添加Hide方法,Loading界面没有在栈中,所以不能调用Back方法,必须主动调用UIMgrHide方法才能让自己关闭。

修改LoadingController

修改UpdateFun方法,添加跳转目标场景的逻辑判断,来决定下一个要显示的UI是什么。然后主动调用一下UIMgr.Hide方法

1
2
3
4
5
6
7
8
9
10
11
12
public override void UpdateFun()
{
base.UpdateFun();
if(SceneMgr.Instance.Process() == 1f)
{
if (GameStateModel.Instance.TargetScene == SceneName.Game)
UIMgr.Instance.Show(Pathes.GAMEUI_VIEW);
else if (GameStateModel.Instance.TargetScene == SceneName.StartScene)
UIMgr.Instance.Back();
UIMgr.Instance.Hide(Pathes.LOADING_VIEW);
}
}