弹出层子系统

说明

弹出层,用于消息提示和广播通知;在游戏中,我们获得一件新道具或者是等级提升等等都需要使用弹出层来进行信息展示。除了信息的展示,例如我们的所有弹窗(背包、战利品框、确认框等…)也都属于弹出层的业务范围。

效果展示

因为也是属于UI界面相关的内容,所以在ui系统下划分出弹出层子系统PopWindowManager

1
2
3
4
5
6
7
8
public class UISystem : ISystem
{
private Dictionary<string, UIPanelBase> _panelDict;

......

public PopWindowManager PopWindowManager { get; private set; }
}

弹窗初始化

考虑在UiSystem初始化时一并对PopWindowManager进行初始化,在初始化时我们需要传入:资源加载器、弹窗根节点、mask(可选)

1
2
3
4
5
6
7
8
9
10
11
12
13
/// <summary>
/// 初始化 弹出层
/// </summary>
/// <param name="resLoader">资源加载器</param>
/// <param name="root">根节点</param>
public PopWindowManager(IResLoader resLoader, Transform root, GameObject windowMask)
{
_resLoader = resLoader;
Debug.Log("PopWindowSystem".Color(FastyColor.GreenB) + "|OK");
this.root = root;
this.windowMask = windowMask;
this.windowMask.SetActive(false);
}

弹窗结构设计

对于弹窗来说,这里我们拆分为两个部分来设计:

PopwindowAbs + PopWindow

PopWindwManager 管理PopwindowAbs

  1. PopWindowAbs 是所有弹窗的基类对象,所有的自定义弹窗都继承自它:其中和uipanel一样提供 OnInit、 OnEnter和OnExit 方法。
  2. PopWindow 是挂在mono上的组件,主要提供弹窗的动画效果支持。
  3. PopWindwManager 则是直接对 所有的PopWindowAbs 进行管理,提供PushPopWindow方法来显示一个弹窗,如果弹窗不存在会自动调用资源管理器创建。
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
46
47
48
49
50
51
52
53
54
55
/// <summary>
/// 弹窗抽象类
/// </summary>
[RequireComponent(typeof(PopWindow))]
[RequireComponent(typeof(CanvasGroup))]
[RequireComponent(typeof(WindowDragger))]
public abstract class PopWindowAbs : MonoBehaviour
{
public PopWindowAnimType PopWindowAnimType = PopWindowAnimType.FadeIn;
public Ease EaseType = Ease.OutBack;

private CompositeDisposable mDispose = new CompositeDisposable();
public PopWindow PopWindow { get; private set; }

[HideInInspector] public bool masked;

public bool Enable => PopWindow.Enable;

public string PopWindowName { get; set; }

private void Awake()
{
}

public virtual void OnInit()
{
PopWindow = GetComponent<PopWindow>();
PopWindow.AnimType = PopWindowAnimType;
PopWindow.EaseType = EaseType;


var closeBtn = transform.FindComponent_DFS<Button>("closeBtn");
if (closeBtn != null)
{
closeBtn.onClick.AddListener(OnExit);
}

PopWindow.OnInit();
}


public virtual void OnEnter()
{
PopWindow.OnEnter();
GameCore.GetSystem<EventCenter>().DispatchEvent(new OnPopWindowEnter() { popWindowName = PopWindowName });
}

public virtual void OnExit()
{
GameCore.GetSystem<EventCenter>().DispatchEvent(new OnPopWindowExit() { popWindowName = PopWindowName });

PopWindow.OnExit();
mDispose.Clear();
}
}

弹窗拖拽

给弹窗增加以下脚本即可:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
using System;
using UnityEngine;
using UnityEngine.EventSystems;

namespace FFW.BaseSystems.UiKit.PopWindowKit
{
public class WindowDragger : UIBehaviour, IBeginDragHandler,IEndDragHandler, IDragHandler,IPointerClickHandler
{
[System.Runtime.InteropServices.DllImport("user32.dll")] //引入dll
public static extern int SetCursorPos(int x, int y);

public static Action<int> BindCameraEvent;

[Header("RESOURCES")]
public RectTransform dragArea;
public RectTransform dragObject;

[Header("SETTINGS")]
public bool topOnClick = true;

private Vector2 originalLocalPointerPosition;
private Vector3 originalPanelLocalPosition;

private Vector3 MouseLocalPosition;

public new void Start()
{
if(dragArea == null)
{
try
{
var canvas = (Canvas)GameObject.FindObjectsOfType(typeof(Canvas))[0];
dragArea = canvas.GetComponent<RectTransform>();
}

catch
{
//Debug.LogError("Movable Window - Drag Area has not been assigned.");
}
}
}

private RectTransform DragObjectInternal
{
get
{
if (dragObject == null)
return (transform as RectTransform);
else
return dragObject;
}
}

private RectTransform DragAreaInternal
{
get
{
if (dragArea == null)
{
RectTransform canvas = transform as RectTransform;
while (canvas.parent != null && canvas.parent is RectTransform)
{
canvas = canvas.parent as RectTransform;
}
return canvas;
}
else
return dragArea;
}
}

public void OnBeginDrag(PointerEventData data)
{
BindCameraEvent?.Invoke(-1);

originalPanelLocalPosition = DragObjectInternal.localPosition;

RectTransformUtility.ScreenPointToLocalPointInRectangle(DragAreaInternal, data.position, data.pressEventCamera, out originalLocalPointerPosition);


//RectTransformUtility.ScreenPointToLocalPointInRectangle(dragObject.transform as RectTransform, Input.mousePosition, data.pressEventCamera, out _pos);


if (topOnClick == true && dragObject)
gameObject.transform.SetAsLastSibling();
}

public void OnDrag(PointerEventData data)
{
Vector2 localPointerPosition;
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(DragAreaInternal, data.position, data.pressEventCamera, out localPointerPosition))
{
Vector3 offsetToOriginal = localPointerPosition - originalLocalPointerPosition;
DragObjectInternal.localPosition = originalPanelLocalPosition + offsetToOriginal;
}

ClampToArea();
}

public void OnEndDrag(PointerEventData eventData)
{
BindCameraEvent?.Invoke(0);

// gameObject.transform.SetAsLastSibling();

}


private void ClampToArea()
{
Vector3 pos = DragObjectInternal.localPosition;

Vector3 minPosition = DragAreaInternal.rect.min - DragObjectInternal.rect.min;
Vector3 maxPosition = DragAreaInternal.rect.max - DragObjectInternal.rect.max;

pos.x = Mathf.Clamp(DragObjectInternal.localPosition.x, minPosition.x, maxPosition.x);
pos.y = Mathf.Clamp(DragObjectInternal.localPosition.y, minPosition.y, maxPosition.y);

DragObjectInternal.localPosition = pos;
}

public void OnPointerClick(PointerEventData eventData)
{
gameObject.transform.SetAsLastSibling();
}
}
}

关于弹窗Mask

对于像是退出游戏之类的最高优先级的弹窗;在该弹窗弹出时只能操作该弹窗;故而我们需要Mask遮罩。

思考:一开始我的想法是直接将mask作为uiSystem里面的内置功能,这样的话在弹出时根据参数来选择性的开启;但是如果存在多个弹窗、不同的叠加层级的情况下;就不是很好判断mask的状态还是需要记录所有弹窗的置顶信息。

处理:面对这样的情况,考虑之后还是由弹窗本身自己控制mask;如果需要mask,那么就创建一个全屏 的弹窗,然后在OnEnter和OnExit中手动控制即可。

Toast的说明

PopWindow是比较重型的系统,支持完全自定义和扩展的;用到的每个弹窗都必须继承基类,重写代码;考虑有啥可能只是想简单的显示点什么;故而引入了Toast的概念。

Toast效果

初始化centerTip:

1
2
3
4
5
6
7
8
9
10
11
12
13
var tip = Resources.Load<GameObject>("FastyTool/centerTip");
var tipGo = GameObject.Instantiate(tip);
this.GetSystem<UISystem>().InitTip(tipGo.GetComponent<CenterTip>(), new Vector2(0, 200));

public void InitTip(CenterTip tip, Vector2 position)
{
_centerTip = tip;
_centerTip.transform.SetParent(_layer2Root[UILayer.Top]);
_centerTip.transform.AsRect().anchoredPosition = position;
_centerTip.canvasGroup.alpha = 0;
_centerTip.canvasGroup.blocksRaycasts = false;
_centerTip.canvasGroup.interactable = false;
}

Toast示例:

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
public class CenterTip : MonoBehaviour
{
[SerializeField] private TextMeshProUGUI messageText;

[SerializeField] private Image iconImage;

[SerializeField] public CanvasGroup canvasGroup;

protected float fadeDuration = 0.5f;


public virtual void Show(string message, float duration = 2f, Sprite icon = null)
{
iconImage.gameObject.SetActive(icon != null);
iconImage.sprite = icon;
messageText.text = message;
StopAllCoroutines();
StartCoroutine(ShowAnimation(duration));
}

protected virtual IEnumerator ShowAnimation(float duration = 2f)
{
DOTween.To(x => canvasGroup.alpha = x, canvasGroup.alpha, 1, fadeDuration);
yield return new WaitForSeconds(fadeDuration);
yield return new WaitForSeconds(duration);
DOTween.To(x => canvasGroup.alpha = x, canvasGroup.alpha, 0, fadeDuration);
}
}

思考

关于toast

起初toast是想制作的复杂、功能更多一些的,例如:按优先级、队列、最大显示数量、动画等;但写了一版觉得似乎也用不到这些功能;如果没有需求,去创建需求也没必要。而且要想复用,实现框架式的感觉没想到什么好办法;现在CenterTip,支持从中间弹出、支持替换预制体;感觉也足够了。

至于上面提到的这些,后面有空再填坑吧。…