前言

​ 拿到ET框架后发现原生的UI框架非常简陋,所以萌生了基于ET去设计UI框架的想法,后续也确实实现了UI的sortingOrder分级、UICamera管理以及生命生命周期的新增、UI栈引入等等([ET6.0框架初步()]),但是近期在查看ET大佬所写框架时发现自己还是小巫见大巫了,因此还是想先去将比较成熟的EGUI导入项目中使用并且记录下方法、思路,同时对EGUI框架加上一些自己的理解。

​ 同时也放上框架作者字母哥的B站视频链接:

1
【ET框架】01-EUI介绍与UI控件获取--字母哥Binaray:https://www.bilibili.com/video/BV12F411e7bP?spm_id_from=333.999.0.0

项目导入

项目框架主要区分为五个部分:

  • ModelView/GameLogic中对于UI表现层数据的存储,抛弃了传统ET自带Demo中传统UIName+Component的思路,将其进一步细化为UIName+ViewComponent中UIBehaviour的两部分以及UIItemBehaviour,同时将ViewComponent作为组件加入到UI对应的Entity实体中。
    • UIBehaviour
      • CommonUI,表示通用的子UI组件,其能嵌套在不同的UIPrefab中
      • ViewComponent,表示主界面展示元素的数据组件,内部存有打上标记的UnityEngine.UI组件
    • UIItemBehaviour
      • 滑动列表数据处理
    • UI Entity实体
      • 其可通过View获取到对应UI的ViewComponent,同时也可用于存储UI中其他逻辑数据,相当于进一步把UI层View与Model区分开来
  • HotfixView中对于UI表现层的逻辑处理,对于每个UI来说具有一个用于实现具体UI函数的System.cs以及实现UI生命周期的接口的Handler.cs
  • ModelView/Module中存有对于UI中的基础数据模块,包括用于表示UI类型的AUIEventAttribute、供逻辑层handler实现的IAUIEventHandler接口、红点处理的数据层等等,这部分会在之后细讲。
  • HotfixView/Module中对于ModelView层Module数据的具体逻辑实现,同时也包括最上层UI管理器组件UIComponent的逻辑代码实现(ShowWindow、HideWindow等)
  • Editor/UI中的UI代码生成拓展,可以对已有的Prefab通过右键生成对应的UI代码

在分析好基本的UI关联代码结构之后,我们就可以去将关联文件导入到我们自己的项目中。

下面我们来进行测试:

  1. 在Bundles/UI中创建Common、Dlg、Item目录分别对应三种不同类型的UI

  2. 创建一个DlgLogin的UIPrefab,在View需要获取的GO中打上标签

    image-20211116181241991

  3. 修改AssetBundle信息,这个后续会用Editor右键来方便处理。
    image-20211116182957134

  4. 在WindowID枚举类中新增对应WindowID_{UIName}的枚举
    image-20211116183159931

  5. 通过UIComponent.Instance.ShowWindow(WindowID windowId)进行展示同步显示,ShowWindowAsync(WindowId windowId)进行异步显示。

Tips:如果对应的UIPrefab中有Canvas组件,其Render Mode需要先设置为World Space

框架理解

整体架构

​ EGUI的框架本身比起GF的通用性更面向于实际逻辑的开发,其根据实际窗口使用类型已经分为了不同的UIGroup,包括Normal、Fixed、PopUp、Other和Invalid,并根据不同类型在UI生命周期中进行了相应的适配。

​ UI窗口类型:

image-20211119131302026

​ 本文的框架图和结构是结合了自身的修改以及拓展后的版本

EGUI.drawio_1

ET的基础思路是用组件代替继承,分发代替多态,因此在EGUI的设计中没有Entity类间的继承关系,所有的UI类都是继承自Entity,通过Component的拆装实现类似于OOP中的继承效果。同时ET中的所有Component默认在池中获取,方便管理内存。

查询接口

  • IsWindowVisible(),判断Window是否处于显示状态
  • GetDlgLogic(),获取UIBaseWindow中对应类型数据

操作接口

  • showWindow(),同步展示窗口
  • showWindowAsync(),异步展示窗口
  • closeWindow(),关闭窗口
  • HideWindow(),隐藏窗口
  • HideLastWindow(),隐藏popUp窗口类型的最后一个UI
  • CloseLastWindow(),关闭popUp窗口类型的最后一个UI
  • HideAndShowWindowStack(),将当前UI窗口隐藏并压入栈中,展示新的窗口,在新窗口隐藏或关闭时重新展示
  • HideAllShownWindows(),隐藏所有正在展示的窗口
  • CloseAllWindow(),清空所有UI
  • RemoveAllStackNode(),清空UI栈中所有结点(栈已经拓展为链表)
  • RemoveParentStackUI(),递归清除该UI的所有父亲UI,相当于从栈UI窗口(如角色信息、背包等)中回到主界面
  • CancelAllLoadingUI(),清除所有加载中的UI,和GF相同,EGUI中的UI无法在加载时就取消,只能在加载完成后清除

生命周期

​ 将上面提到的IAUIEventHandler中各个接口函数的过程进行具体解析。

  • OnInitWindowCoreData():在UIComponent中的show()生命周期中的ReadyToShowBaseWindow()以及ShowAsync()生命周期的showBaseWindowAsync()调用,在过程中若未加载对应的UIData资源则调用该函数
  • OnInitComponent():过程同上,若过程中UIBaseWindow未进行加载或者未进行UITransform的preload操作则调用该函数
  • OnRegisterComponent():过程同上,若过程中UIBaseWindow未进行加载或者未进行UITransform的preload操作则调用该函数
  • OnShowWindow():在UIComponent中的show()和showAsync()生命周期中RealShowWindow()中调用,即在窗口展示时进行调用
  • OnHideWindow():在UIComponent中的hide()和CheckDirectlyHide()调用,同时也在UIComponent中HideAllShownWindow()调用,即在窗口隐藏/关闭时调用
  • BeforeUnload():在UIComponent中的CloseWindow()以及UIComponent组件被移除时的Destroy()回调中调用,即在窗口关闭/被销毁前调用

UI窗口打开的生命周期:

EGUI_OpenUI_.drawio

UI隐藏时的生命周期:

EGUI_CloseUI_.drawio

自身拓展

  • 将原本框架中的UI栈由Stack数据类型改为链表类型,方便后续可删操作

  • 新增操作接口image-20211119160218585

  • WindowCoreData中新增各种状态参数,方便获取状态
    image-20211119160343165

  • UIComponentSystem的Update()中增加对删除加载中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
    private static void HandleCancelLoadingUI(this UIComponent self)
    {
    //MARKER:控制Loading中UI取消
    if (self.LoadingWindowsCancelCache != null && self.LoadingWindowsCancelCache.Count > 0)
    {
    foreach (var windowID in self.LoadingWindowsCancelCache)
    {
    UIBaseWindow window = self.GetUIBaseWindow(windowID);
    if (window != null&&window.WindowData.isLoaded)
    {
    self.CloseWindow(windowID);
    self.ReadyToRemove.Add(windowID);
    }
    }

    if (self.ReadyToRemove.Count > 0)
    {
    foreach (var id in self.ReadyToRemove)
    {
    self.LoadingWindowsCancelCache.Remove(id);
    }
    self.ReadyToRemove.Clear();
    }
    }
    }
  • 更改部分生命周期代码,提供可将WindowID生成在任意WindowType的接口,如ShowWindowInWindowType(),ShowWindowInWindowTypeAsync()

  • 新增Editor中对于快捷赋值ABName和清空ABName的小轮子

    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
    [MenuItem("Assets/AssetBundle/NameUIPrefab")]
    public static void NameAllUIPrefab()
    {
    string suffix = ".unity3d";
    UnityEngine.Object[] selectAsset = Selection.GetFiltered<UnityEngine.Object>(SelectionMode.DeepAssets);
    for (int i = 0; i < selectAsset.Length; i++)
    {
    string prefabName = AssetDatabase.GetAssetPath(selectAsset[i]);
    //MARKER:判断是否是.prefab
    if (prefabName.EndsWith(".prefab"))
    {
    Debug.Log(prefabName);
    AssetImporter importer=AssetImporter.GetAtPath(prefabName);
    importer.assetBundleName = selectAsset[i].name.ToLower() + suffix;
    }

    }
    AssetDatabase.Refresh();
    AssetDatabase.RemoveUnusedAssetBundleNames();
    }

    [MenuItem("Assets/AssetBundle/ClearABName")]
    public static void ClearABName()
    {
    UnityEngine.Object[] selectAsset = Selection.GetFiltered<UnityEngine.Object>(SelectionMode.DeepAssets);
    for (int i = 0; i < selectAsset.Length; i++)
    {
    string prefabName = AssetDatabase.GetAssetPath(selectAsset[i]);
    AssetImporter importer=AssetImporter.GetAtPath(prefabName);
    importer.assetBundleName = string.Empty;
    Debug.Log(prefabName);
    }
    AssetDatabase.Refresh();
    AssetDatabase.RemoveUnusedAssetBundleNames();
    }
  • 11/23更新:生命周期中新增OnStart,OnResume两个周期

    • OnStart 在第一次show且在OnShow前调用
    • OnResume在非第一次show且在OnShow前调用
  • 11/23更新:将EGUI的code生成中Behaviour部分(View)的生成抽离出来,单独作为一个右键扩展,方便更改UIPrefab内部GO后的代码再生成。

  • 11/28更新:更改子UI代码生成函数,适配ET6.0新版,在一个ViewComponent中可以有多个同类型的子UI

    使用步骤:

    • 先设计子UI的UIPrefab,并右键点击SpawnEGUICode
      image-20211128143508070

    • 在需要添加这个子UI的Dlg中,通过”子UI名_实际名称”的方法做标识,并右键Dlg点击SpawnEGUICode生成对应的EGUI代码

      DlgTest的测试例子:
      image-20211128143933465

    • 若后续有对这个Dlg的View进行更改,则只需要重新右键Dlg点击SpawnEGUIBehaviourCode即可
      image-20211128144215486