当前位置:   article > 正文

Unity UI 框架相关的一些思考_unity yiui

unity yiui

 开源地址: 

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<TextAsset>(scriptAssetPath));

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

Alt+1:Add(Set)

Alt+2:Drelete(Remove)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/601003
推荐阅读
相关标签
  

闽ICP备14008679号