• Unity UI 框架相关的一些思考


     开源地址: 

    GitHub - NRatel/NRFramework.UI: 基于 Unity UGUI 的 UI 开发框架基于 Unity UGUI 的 UI 开发框架. Contribute to NRatel/NRFramework.UI development by creating an account on GitHub.icon-default.png?t=N7T8https://github.com/NRatel/NRFramework.UI

    简介:

    Unity UI 框架_NRatel的博客-CSDN博客Unity UI 框架组件化、树状聚合设计提供面板创建/销毁/显隐藏接口、显示状态维护提供控件创建/动态逻辑绑定/销毁接口提供元素半自动收集、代码自动生成层级管理焦点管理内置自动添加背景,及背景点击响应逻辑内置自动播放打开/关闭动画、动画状态维护内置返回键回退逻辑其他(自定义组件、屏幕适配、多语言支持等)https://blog.csdn.net/NRatel/article/details/127902181

    1、UI 类应该使用一个 Monobehaviour 子类进行逻辑控制,还是用纯 C# 类?

    这两者的区别是:

    前者创建:先创建 GameObjet 再给他挂一个控制它的脚本;
    后者创建:先创建一个纯 C# 对象, 再在合适的时机创建 GameObject 交给它管理。

    前者:
    ⑴、优点:符合Unity原本的 “go-comp” 思路。
    ⑵、缺点:逻辑脚本和预设资源绑在一起(至少不能以传统方式热更)。

    后者:
    ⑴、优缺点:从类图上看,更符合控制引用关系、更便于代码设计;但需要自己维护 “引用 go”、“”“解除引用 go”、“销毁 go” 的逻辑;
    ⑵、优点:可为 go 挂一个通用的需操作元素收集组件,与逻辑类分开,并可随意组合复用。

    2、为什么要组件式、树状聚合设计?

    我之前的一篇文章里说过,游戏界面内容应该面向对象:每个界面作为一个游戏对象,界面中,较为独立的部分/重复出现的部分,可以单作为一个游戏对象,逐层嵌套,形成父子关系。 聚合/组合关系:1~1或1~N。好处是:

    ⑴、可以针对各层对象写逻辑,使逻辑各归其所,适合大规模建设。

    ⑵、对象可以复用。界面干净整洁、统一性好,不会出现有两处相同的东西,却长得不太一样

    ⑶、极大加快UI开发效率,设计界面、拼界面、写界面逻辑都是对 “对象化的零件” 进行拼凑组合。

    比如:商店界面:商店界面 -> 货架 -> 货架的层 -> 货架层上的物体。
    比如:编队界面:编队界面 -> 队伍槽位 -> 队伍中的英雄。
    比如:英雄培养界面:英雄培养界面 -> 消耗材料槽(放材料图标和拥有/需求数量) -> 材料图标。
    但是,这对策划(UE设计)和美术(UI效果图设计)的规划能力要求大幅增高了。
    另外,也带给程序一些困惑,主要表现为:

    ⑴、若设计中各处针对性强 ,则难以复用。

    ⑵、设计复用需求不明确时,难以判断是否应按提出组件的方式来做。
    (只出了UE,未出UI效果图就让程序先做功能,常常出现UE相似但最后UI不同的情况,导致返工)。

    ⑶、设计的需求调整可能不易,因为文件数变多了。
    (只在本界面内重复出现的对象的逻辑类,可以定义在该界面的逻辑文件内,以减少文件数)

    ⑷、当有很多相似组件出现时,对组件的命名成了一个问题。

    整体来说,利远大于弊。

    3、接口支持度和易用性的思考

    接口支持度和易用性,看似多少有点冲突。

    首先,在一个项目里,面对需求,框架不应该出现 “不能支持”的情况,这就可能产生很多参数和细碎接口。

    一个做事的上层方法,应该让用户自己决定怎么组合调用底层接口,而不是只提供自己臆想的 “某一种组合方式” 限制用户。

    但是,如果一件事大部分情况下都是以 “某一种组合方式” 去做,每次都让用户组合,又未免过于麻烦。

    以下几种手段可能解决这个问题:

    ⑴、将一些接口的参数改为默认形参 或 重载接口(UI框架中CreatePanel、CreateWidget等接口)

    ⑵、为一组细碎的接口提供默认的组合接口。

    ⑶、父类中提供接口的默认实现,但可在子类中重写(UI框架中播放打开/关闭Panel动画接口)。

    4、操作/生命周期顺序的思考

    ⑴、父子类创建销毁的接口/生命周期顺序?

    创建时先父后子;销毁时先子后父。

    ⑵、上下两层层界面的焦点焦点变化,获得焦点时,应该先触发谁的 OnFocus,丢失焦点时,应该先触发谁的 OnLostFocus?

    出现时是先出现下层界面后出现上层界面;消失时是先消失上层界面后消失下层界面。

    ⑶、聚合/依赖关系,销毁的时候先解除引用还是先销毁自己?

    创建:创建自身、建立引用;那么移除应该相反,即:解除引用、销毁自身。

    ⑷、对外提供接口的完成回调 和 对内完成事件的调用顺序?

    对外提供接口的完成回调,应该晚于内部完成事件,即:先处理完内部,再处理外部。

    5、复杂黑盒接口的异常处理思考

    一个复杂的黑盒接口,传入非法参数时,让它报错还是返回 null?

    尤其是封装成了 dll,别人不能查看源码时,无论报错还是光返回 null 都会让使用者感到疑惑。

    仔细想想,异常,通常应该在外层调用处进行处理,那就应该把错误的情况列表并传出来。

    可以为其定义一系列错误码,然后返回,若方法原来有返回值,改成 out 传出。如下:

    1. static public class FindCompErrorCode
    2. {
    3. //UIView中
    4. public const int OK = 0;
    5. public const int ERROR_CAST_TYPE = 1001; //错误的组件转换类型
    6. public const int COMP_DEFINE_IS_NULL_OR_EMPTY = 1002; //compDefine为null或""
    7. public const int NOT_EXIST_THIS_COMPONENT = 1003; //View中不存在此组件定义
    8. public const int NOT_EXIST_ANY_CHILD_WIDGET = 1004; //View中不存在任何子Widget(不存在此Widget)
    9. public const int WIDGETS_ID_IS_NULL_OR_EMPTY = 1005; //widgetIds为null或""
    10. public const int NOT_EXIST_THIS_CHILD_WIDGET = 1006; //View不存在此Widget
    11. //UIRoot
    12. public const int PANEL_ID_IS_NULL_OR_EMPTY = 1007; //panelId为null或""
    13. public const int NOT_EXIST_THIS_PANEL = 1008; //Root中不存在此Panel
    14. //UIManager中
    15. public const int NOT_EXIST_THIS_ROOT = 1009; //UIManager中不存在此Root
    16. public const int VIEW_PATH_IS_NULL_OR_EMPTY = 1010; //viewPath为null或""
    17. public const int VIEW_PATH_IS_TOO_SHORT = 1011; //ViewPath应该至少包含一个rootId和一个panelId
    18. }
    19. public int FindComponentByPath<T>(string path, string compDefine, out T comp) where T : Component {}

    6、界面显示及状态相关问题的思考

    基本流程:
    ⑴、界面创建后,播放打开动画(若有)。
    ⑵、界面初始化时,注入或获取 Data 完成显示。
    ⑶、界面刷新时,注入或获取 Data 完成显示。
    ⑷、界面关闭时,播放关闭动画(若有)。
    注意:
    ⑴、动画播放是异步的。动画一般都是创建时挂到预设上的,只操作预设初始节点,不依赖数据。
    ⑵、获取Data可能是异步的(现请求)。
    ⑶、某些组件的显示可能是异步的(如:为了优化脏标记异步更新)。
    外部需求:
    ⑵、跳转连续打开多个界面时,不关心动画,但依赖数据(由数据决定是否可以依次打开,直至目标界面)。
    ⑶、功能解锁、红点、引导等上层系统需要能随时获取界面当前状态(如引导,要等界面完全准备好后才能执行)。
    ---------------------------------
    其他问题:
    ⑴、异步请求数据,应放在Create前还是Init中?
         建议后者,后者可以利用自身界面阻挡操作。但注意,必须处理好“创建后~初始化完成前”的显示。
    ⑵、界面显示和动画状态如何维护? 
         ①、只维护自身状态,不考虑子Widget。
         ②、但在外部读取时可以考虑计入自身及所有子Widget的状态(结合实际需求)。
         ③、初始化/刷新方法 完全由用户自定义(可能不是最终想法),如果是同步的,默认标记为Idle;如果是异步的,可以应该在初始化/刷新开始时将显示状态改为Initing/Refreshing,并在完成时将显示状态标记为Idle。
         ④、在Panel创建时默认调起打开动画,播放完成时将动画状态改为Idle。
         ⑤、在Panel关闭时调起关闭动画,播放完成时将动画状态改为Closed。  
    ⑶、是否将界面状态暴露到 Inspector中,便于调试?
         不确定是否有必要,待定。

    7、维护焦点变化的一些思考

    ⑴、维护焦点变化,起到什么作用? 

    界面失去焦点时,可选择性地“挂起”(暂停内部耗时Update类操作),并在重新获得焦点时恢复,以此优化。另外,还可在获得焦点时做一些事件触发,比如拍脸弹窗等。

    ⑵、关闭界面时是否触发 OnFoucusChanged(false)?

    否,焦点在打开/关闭界面之后统一计算的。
    无法触发已关闭界面的 OnFoucusChanged(false)。
    这意味着,OnFoucusChanged 是不完全对称的。
    如果在其中做了一些创建操作(尽量不要这样做),可能需要在 OnClosing 中善后清理。

    ⑶、界面的打开/关闭动画对焦点变化有什么影响?

    界面在上层创建时,获得焦点应该是敏感的,即:只要创建就可能立刻获得焦点,此时下层界面丢失焦点也是立刻的。

    界面在上层销毁时,下层界面获得焦点应该是迟钝的,即:要等到上层界面完全销毁,下层界面才能够获得焦点。

    8、维护通用背景的一些思考

    ⑴、通用背景是否是单例的?

    是,一个够用。可以根据当前状态,移动背景到它应该去的界面。

    ⑵、通用背景是否总是出现在主要获得焦点的界面上?

    不是,有些界面需要背景,但却不抢夺焦点(System类型)。

    ⑶、界面动画不应对预设根节点进行操作(缩放、旋转、位移)

    因为背景是添加到预设根节点下的第一个物体,如果操作根节点,就会带着背景一起移动。

    也是完全可以避免的:

    ①、PanelType 为 Underlay 的 界面,打开动画一般是 “子元素逐渐加入”(不会操作根节点)、“翻篇进入”(应该加入额外一个动画根节点)。

    ②、PanelType 为 Window 的 界面,打开动画一般是 “缩放、淡入淡出、飞入飞出等(应该加入额外一个动画根节点)”。

    9、组件收集的一些实现问题解决和思考

    ⑴、每次支持新组件,都要改哪些地方?

    ①、支持原组件使用的图标, 
        修改 UIEditorUtility.GetIconByType 方法。
    ②、支持新组件脚本生成 生命周期、事件修改
        适当修改 UIView 或 为其增加 partial class,添加 “含事件组件” 的事件绑定、解除绑定和生命周期方法。
        适当修改 UIViewBehaviourEditor 的 canBindEventCompSet,增加 “含事件组件” 的组件名。
        适当修改 UIEditorUtility.kUITemporaryCode,增加事件生命周期方法。
    ③、支持组件推测。
        适当修改 SetAsUIOpElement。

    ⑵、TMP 的组件图标获取问题

    ①、TMP 是怎么做到脚本图标自定义的?

    只要将图标资源按照其命名规则放在 Gizmos 目录中即可
    可在 TMP 包的以下目录中找到:Packages/TextMeshPro/Edutor Resources/Gizmos/

    ②、那我要怎么才能加载?

    访问包内资源的官方文档:https://docs.unity3d.com/cn/2020.3/Manual/upm-assets.html
    (注意,路径中的包名不带版本号、空格、“-”都要保留)

    1. (Texture2D)AssetDatabase.LoadAssetAtPath("Packages/com.unity.textmeshpro/Editor Resources/Gizmos/TMP - Input Field Icon.psd", typeof(Texture2D));
    2. //实测,这样也可:
    3. (Texture2D)EditorGUIUtility.Load("Packages/com.unity.textmeshpro/Editor Resources/Gizmos/TMP - Input Field Icon.psd")

    ⑵、在以下三种情况中,如何在 OnInspector 中准确获得当前关联预设的资源路径?

    1点击预设时,2双击预设并选择预设时、3预设拖入Hierarchy时

    解决:
    双击预设并选择预设时、预设拖入Hierarchy时的 "Select" 是怎么做到的呢?
    在Unity源码中全局搜索源码 "Select" ,找到 GameObjectInspector 了解具体情况。

    PrefabAssetType singlePrefabType = PrefabUtility.GetPrefabAssetType(target);
    PrefabInstanceStatus singleInstanceStatus = PrefabUtility.GetPrefabInstanceStatus(target);

    在三种情境下测试(1点击预设时,2双击预设并选择预设时、3预设拖入Hierarchy时)
    在其 OnInspectorGUI 中输出:
    PrefabAssetType singlePrefabType = PrefabUtility.GetPrefabAssetType(target);
    PrefabInstanceStatus singleInstanceStatus = PrefabUtility.GetPrefabInstanceStatus(target);
    Debug.Log("singlePrefabType: " + singlePrefabType);
    Debug.Log("singleInstanceStatus: " + singleInstanceStatus);
    输出结果如下:
    1、singlePrefabType: Regular;  singleInstanceStatus: NotAPrefab
    2、singlePrefabType: NotAPrefab;  singleInstanceStatus: NotAPrefab
    3、singlePrefabType: Regular;  singleInstanceStatus: Connected

    发现一个方法,但是 internal 方法,不能用。。
    GameObject prefabGo = PrefabUtility.GetOriginalSourceOrVariantRoot(targets[i]);   

    但又发现一个调用了它的 public 方法:
    GetPrefabAssetPathOfNearestInstanceRoot

    在三种情境下测试(1点击预设时,2双击预设并选择预设时、3预设拖入Hierarchy时)
    在其 OnInspectorGUI 中输出:
    string prefabAssetPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(target);            
    Debug.Log("prefabAssetPath: " + prefabAssetPath);
    1、3,可以获得预设路径,2不行(输出为"")

    全局搜索 "Canvas (Environment)" 找线索,发现以下调用堆栈:

    PrefabStageUtility.GetOrCreateCanvasGameObject,
    PrefabStageUtility.HandleUIReparentingIfNeeded
    PrefabStageUtility.HandleReparentingIfNeeded
    PrefabStage.LoadStage
    PrefabStage.OpenStage
    StageNavigationManager.SwitchToStage
    PrefabStageUtility.OpenPrefabMode
    PrefabStageUtility.OpenPrefab

    可知:
    点击预设 Inspector 右上角的 "Open"、鼠标右键点击预设再点击"Open"、双击预设等,
    都是调用的 PrefabStageUtility.OpenPrefab

    而我要的答案就是:
    获取当前预设操作的Stage:PrefabStageUtility.GetCurrentPrefabStage() 或 PrefabStageUtility.GetPrefabStage(GameObject gameObject)
    然后取其预设资源路径:pfabStage.prefabAssetPath(已弃用)或 prefabStage.assetPath(改为使用它)

    ⑶、如何在生成脚本后默认定位脚本位置

    EditorGUIUtility.PingObject(AssetDatabase.LoadAssetAtPath(scriptAssetPath));

    ⑷、为 操作元素Add(Set)、Delete(Remove) 增加快捷键

    Alt+1:Add(Set)

    Alt+2:Drelete(Remove)

  • 相关阅读:
    Word控件Spire.Doc 【文本】教程(11) ;如何将文本分成两列并在它们之间添加行
    PHP:错误
    Spring+CXF restful开发WebService
    物联网AI MicroPython传感器学习 之 MQ136硫化氢传感器
    水果店圈子:水果店开业的小活动方案怎么做,水果店开业促销手段有哪些
    JavaSE基础语法思维导图
    源码解析Synchronous Queue 这种特立独行的队列
    软件测试学习(四)自动测试和测试工具、缺陷轰炸、外包测试、计划测试工作、编写和跟踪测试用例
    关于 C/C++ 中的 switch 语句,您可能不知道
    python functools.wraps保留被装饰函数属性
  • 原文地址:https://blog.csdn.net/NRatel/article/details/127931997