【Unity 框架】QFramework v1.0 使用指南 工具篇:06. UIKit 界面管理&快速开发解决方案 | Unity 游戏框架 | Unity 游戏开发 | Unity 独立游戏

UI Kit 简介

UI Kit 是一套界面管理&快速开发解决方案

UI Kit 的特性如下:

  • 界面管理
  • 层级管理
  • 代码生成及组件自动绑定(底层用的 ViewController)

UI Kit 基本使用

UI Kit 本身有一套推荐使用的工作流程,而此工作流程的设计是为了使每个界面只负责展示数据和监听用户输入,界面与界面之间互相独立,并且可独立测试。

下面我们将介绍如何制作一个游戏主页(UIBasicPanel)。

首先我们先创建一个场景:TestUIBasicPanel,如下图所示:

image-20220725171613899

在这里大家要注意一下,UI Kit 推荐每个界面创建一个对应的测试场景,要保证每个界面是可以独立测试的。

接着打开 TestUIBasicPanel 如下所示:

image-20220725171641152

我们拖出来一个 UIRoot prefab,如下所示:

image-20220725171556290

这里非常清晰地可以看到 UI Kit 所支持的所有层级。

接着我们在 Design 层级下创建一个 Panel(右击 Design->UI->Panel) ,并命名为 UIBasicPanel,如下所示:

image-20220725171752004

这里要说一点,Design 层级,顾名思义就是用来做设计的层级,什么是设计?就是拼界面,这个层级就是专门用来拼界面的,Design 层级会在运行的时候会自动隐藏掉自己以及所有的子节点。

OK,接下来,我们将 UIBasic 制作成 prefab,将其放到 Assets/Art/UIPrefabs 目录下,如果没有这个目录就自己手动创建一下。

放入后如下图所示:

image-20220725172338703

Assets/Art/UIPrefab 这个目录是怎么来的呢?它是 QFramework 约定的专门放置 UI 界面 prefab 的位置。而 Assets/Art 是框架推荐存放资源的位置,当然关于资源的存放位置只是推荐,而不是强制的。

但是 UI 界面的 prefab 必须放在 Assets/Art/UIPrefab 目录下,因为这个部分在代码生成的时候需要。

那么有的童鞋可能会问,Assets/Art/UIPrefab 这个路径可以不可以更改?

当然可以,更改的方式也很简单,就是打开包管理面板(QFramework/Preference ctrl + e),打开后可以看到如下面板:

image-20220725172030417

详细的设置方式在上边介绍了,这里就不多介绍了。

接下来需要将 UIHomePanel prefab 标记为 AssetBundle,如下图所示:

image-20220725172438374

标记成功后。

会看到如下结果:

image-20220725172140857

接着,我们在这里要确保一件事情,就是 Res Kit 需要保证当前环境是模拟环境(Simulation Mode),具体看面板中的如下选项是否是勾上即可。

image-20220725172213241

确保勾上之后,我们就开始生成代码,具体操作如下所示(右键->@UI-Kit Create UI Code):

image-20220725172505171

点击之后等待编译,编译结束后,我们看到如下结果:

脚本生成成功

image-20220725172532517

脚本自动挂载了 UIBasicPanel Prefab 上

image-20220725172550535

到此,代码生成部分就介绍完了。

接着,我们想办法让这个场景独立运行。

现在,我们直接运行场景,是不会加载任何界面的,如下所示:

image-20220725172721472

如何让这个场景加载 UIBasicPanel 呢?

很简单,使用 UIPanelTester 如下所示:

image-20220725172923702

按照图中样子设置就好,然后运行场景。
结果如下:

image-20220725173003435

图中成功加载了改界面。

这样,最基本的 UIBasicPanel 测试场景就算搭建完了,同时我们是完全按照 QFramework 推荐的工作流程完成的。

虽然步骤会稍微繁琐一点,但是用一段时间大家就会觉得这是值得的。

OK,接下来我们来介绍控件的自动绑定功能。

控件的自动绑定功能

我们在 UIBasicPanel 上添加一些按钮,并在每个按钮上挂上 Bind 脚本,如下所示:

image-20220725173212119

接着 Apply UIBasicPanel,如下所示:

image-20220725173259294

这里要注意,一定要选定 UIBasicPanel 再进行 Apply,千万别选成 UIRoot 了。

Apply 之后,再次生成一次代码,操作如下所示:

image-20220725172505171

生成之后,结果如下:

image-20220725191039907

接着,我们打开 UIHomePanel.cs 脚本,试着写一些代码:

using UnityEngine;
using UnityEngine.UI;
using QFramework;

namespace QFramework.Example
{
    
    
	public class UIBasicPanelData : UIPanelData
	{
    
    
	}
	public partial class UIBasicPanel : UIPanel
	{
    
    
		protected override void OnInit(IUIData uiData = null)
		{
    
    
			mData = uiData as UIBasicPanelData ?? new UIBasicPanelData();
			
			BtnStart.onClick.AddListener(() =>
			{
    
    
				Debug.Log("开始游戏");
			});
		}
		
		protected override void OnOpen(IUIData uiData = null)
		{
    
    
		}
		
		protected override void OnShow()
		{
    
    
		}
		
		protected override void OnHide()
		{
    
    
		}
		
		protected override void OnClose()
		{
    
    
		}
	}
}

代码很简单,主要是在 OnInit 的时候注册了 BtnStart 按钮。

接着我们运行场景,接着点击 BtnStart 按钮,得到结果如下:
image.png

这样控件自动绑定功能就介绍完了。

自动绑定的功能与 View Controller + Bind 是使用的是同一套机制。

打开、关闭界面

我们运行 UIBasicPanel 是通过 UIPanelTester 实现的。

UIPanelTester 是一个 UI 界面的测试器,它只能在编辑器环境下运行。

真正打开一个 UI 界面,是通过 UIKit.OpenPanel 这个 API 完成的。

只需要写如下代码即可:

UIKit.OpenPanel<UIBasicPanel>();

代码非常简单。

而我们要关闭掉一个 UI 界面也比较容易,代码如下:

UIKit.ClosePanel<UIBasicPanel>();

如果是在一个界面内部关掉自己的话,代码如下:

this.CloseSelf(); // this 继承自 UIPanel 

OK,到此我们接触了 3 个 API:

  • UIKit.OpenPanel<T>();
  • UIKit.ClosePanel<T>();
  • UIPanel.CloseSelf();

后边的两个没什么好讲的,很简单,但是第一个 API 比较重要,因为它有一些参数我们可以填。

UIKit.OpenPanel

UIKit.OpenPanel 的参数定义及重载如下:

public static T OpenPanel<T>(UILevel canvasLevel = UILevel.Common, IUIData uiData = null,
            string assetBundleName = null,
            string prefabName = null) where T : UIPanel
{
    
    
	...
}

public static T OpenPanel<T>(IUIData uiData, PanelOpenType panelOpenType = PanelOpenType.Single,
            string assetBundleName = null,
            string prefabName = null) where T : UIPanel
{
    
    
	...
}

public static UIPanel OpenPanel(string panelName, UILevel level = UILevel.Common, string assetBundleName = null)
{
    
    
	...
}

所有参数如下:

  • canvasLevel:界面在哪个层级打开
    • 默认值:Common
  • uiData:打开时可以给界面传的初始数据
    • 默认值:null
  • assetBundleName:界面资源所在的 assetBundle 名
    • 默认值:null
  • prefabName:如果界面名字和 prefab 名字不同,则以这个参数为准去加载界面资源
    • 默认值:null

都有默认值,所以这四个参数都可以不用传。

不过这四个 API 在某种情况下非常实用。

下边举一些例子。

// 在 Forward 层级打开
UIKit.OpenPanel<UIBasicPanel>(UILevel.Forward);

// 传递初始数据给 UIHomePanel
UIKit.OpenPanel<UIBasicPanel>(new UIHomePanelData()
{
    
    
    Coin = 10
});
            
// 从 UIHomePanelTest.prefab 加载界面 
UIKit.OpenPanel<UIBasicPanel>(prefabName: "UIBasicPanel");

都比较容易理解。

有的童鞋可能会问,我们给 UIHomePanel 传递的 UIHomePanelData,在哪里使用呢?

答案是在,OnInit 和 OnOpen 中,如下所示:

namespace QFramework.Example
{
    
    
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;
    using UnityEngine.UI;
    
    
    public class UIBasicPanelData : QFramework.UIPanelData
    {
    
    
        public int Coin;
    }
    
    public partial class UIBasicPanel : QFramework.UIPanel
    {
    
    
        protected override void OnInit(QFramework.IUIData uiData)
        {
    
    
            mData = uiData as UIBasicPanelData ?? new UIBasicPanelData();
            // please add init code here
            
            // 外边传进来的,第一次初始化的时候使用
            Debug.Log(mData.Coin);
        }
        
        protected override void OnOpen(QFramework.IUIData uiData)
        {
    
    
            // 每次 OpenPanel 的时候使用
            Debug.Log((uiData as UIBasicPanelData).Coin);
        }
        
        protected override void OnShow()
        {
    
    

        }
        
        protected override void OnHide()
        {
    
    
        }
        
        protected override void OnClose()
        {
    
    
        }
    }
}

为什么要这样做呢?

笔者认为,界面有两种显示数据的用法,一种是有的界面是需要从外边填充的,比如警告、弹框、或者道具信息页面等。另一种界面是需要自己获取数据并展示的,比如游戏中的主角金币、等级、经验值等。

如果界面的数据都从外边填充,那么这个界面会拥有更好的可复用性。

当然需要一个可复用性的界面还是需要一个普通界面就看大家的需求了,并不是说有可复用性的界面就是好的。

异步加载界面

StartCoroutine(UIKit.OpenPanelAsync<UIHomePanel>());
// 或者
UIKit.OpenPanelAsync<UIHomePanel>().ToAction().Start(this);

在 WebGL 平台上, AssetBundle 加载资源只支持异步加载,所以为此提供了 UIKit 的异步加载支持。

UIPanel 生命周期

我们先看下 UIBasicPanel 的代码,如下:

namespace QFramework.Example
{
    
    
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;
    using UnityEngine.UI;
    
    
    public class UIBasicPanelData : QFramework.UIPanelData
    {
    
    
    }
    
    public partial class UIBasicPanel : QFramework.UIPanel
    {
    
            
        protected override void OnInit(QFramework.IUIData uiData)
        {
    
    
            mData = uiData as UIHomePanelData ?? new UIHomePanelData();
            // please add init code here
            
            
        }
        
        protected override void OnOpen(QFramework.IUIData uiData)
        {
    
    
        }
        
        protected override void OnShow()
        {
    
    

        }
        
        protected override void OnHide()
        {
    
    
        }
        
        protected override void OnClose()
        {
    
    
        }
    }
}

默认的生命周期函数如下:

  • OnInit
  • OnOpen
  • OnShow
  • OnHide
  • OnClose

OnInit 则是在 UIPanel 所在的 prefab 初始化的时候进行调用的,在调用 UIKit.OpenPanel 时,只要在 UIKit 中没有对应的缓存界面时,就会调用一次 OnInit 这个周期。

OnOpen 就是每次 UIKit.OpenPanel 调用时,就会调用。

OnShow 实际上调用时机与 UIKit.OpenPanel 是一样的,只不过 OnShow 是最初版本遗留下拉的 API,所以就保留了。当然还有 UIMgr.ShowPanel 调用时,OnShow 会被调用

OnHide 则是在 UIKit.HidePanel 调用时,OnHide 会被调用。

最后 OnClose 就是在 UIKit.ClosePanel 调用时,就会触发,实际上 OnClose 相当于 OnDestory 这个周期。

大概就这些,其中 UIKit.OpenPanel 会触发资源的加载和初始化操作,而 UIKit.ClosePanel 则会触发卸载和销毁操作,只要记得这两点就好。

笔者基本上就只会用到 OnInit 和 OnClose 这些周期,偶尔会用一用 OnOpen。

OK,此篇的内容就这些。

UIKit 剩下的常用 API

UIKit.Root.SetResolution

参数定义如下:

image.png

对应 UIRoot 上的 Canvas Scaler 如下:

image.png

大部分项目,用这个 API 做屏幕适配足够了。

UIKit.Root.Camera

获取 UIRoot 的摄像机。

var uiCamera = UIKit.Root.Camera;

UIKit.Stack.Push、UIPanel.Back(Pop)

有的时候,UI 需要实现一个 UI 界面的堆栈,以便于支持返回上一页这样的操作。

这个时候就可以用 Push 和 UIPanel.Back 实现。

示例代码:

UIKit.Stack.Push(this); // this 是 Panel
// UIHomePanel 需要确保是打开的状态,如果不打开会报错。
UIKit.Stack.Push<UIHomePanel>();
            
this.Back(); // 弹出 UIHomePanel
this.Back(); // 弹出 this

非常简单。

UIPanel 自动生成工具

在此篇的最开始,笔者手动创建了一套围绕 UIBasicPanel 的测试、开发场景,其过程比较繁琐。

为了解决这个问题,笔者写了一个简单的 UIPanel 自动生成工具。

接下来看下它的基本使用流程。

基本使用

首先,快捷键 ctrl + e 打开 PackageKit 面板,如下:

image-20220725213353833

在上图中界面名字的输入框中输入 Game/UIGamePanel,然后点击创建 UI Panel,如下所示:

image-20220725213736831

输入之后可以看到即将生成文件的预览。

在这个面板中,我们还可以设置 分辨率与适配对齐,还有模块的目录,如果不想在更目录创建按照规范生成文件,也可以在其他子目录中创建。

我们点击 “创建 UI Panel” 这个按钮。

点击之后结果如下:

image-20220725214053092

相关的 prefab,场景、脚本都生成好了,就连 AssetBundle 也都标记好了,如下:

image-20220725214155564

这就是这个工具的一个用处,非常方便,解决了笔者大量的开发工作量。

在上一篇,我们了解了界面的打开和关闭相关的 API。

在这一篇,我们了解一下 UI Kit 中的 子界面/子控件—UI Element

UI Element 简介

在前篇,我们了解到,一个 UIPanel 是可以自动绑定几个 子控件的(Bind)。但是当一个界面结构比较复杂的时候,不可能一个 UIPanel 管理数十个 Bind,这时候就需要对 Bind 进行一些打组操作。我们的 UIElement 就可以登场了。

UIElement 基本使用

使用方式非常简单,就是将 Bind 中的 标记类型 改成 Element即可,如下所示。

image.png

image.png

并且要给 生成类名 填写一个名字,这个名字决定生成的类的名字。这里填写了 UIAboutSubPanel。

之后进行 Apply 操作。

image-20220728141929443

注意这里 Apply 的是 UIBasicPanel。

接着生成代码, 如下:

image-20220728142010223

等待编译后,如下所示:

image-20220728142048854

BtnClose 由 UIAboutSubPanel 管理了

image-20220728142125763

我们看下脚本目录:

image-20220728142202239

目录生成了一个新的文件夹,是以父 Panel (UIBasicPanel)为名的。

打开 UIAboutSubPanel 脚本,代码如下所示:

/****************************************************************************
 * 2022.7 LIANGXIEWIN
 ****************************************************************************/

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using QFramework;

namespace QFramework.Example
{
    
    
	public partial class SubPanel1 : UIElement
	{
    
    
		private void Awake()
		{
    
    
		}

		protected override void OnBeforeDestroy()
		{
    
    
		}
	}
}

再看下 UILoginView.Designer.cs 脚本,如下所示:

/****************************************************************************
 * 2022.7 LIANGXIEWIN
 ****************************************************************************/

using UnityEngine;
using UnityEngine.UI;
using QFramework;

namespace QFramework.Example
{
    
    
	public partial class SubPanel1
	{
    
    
		[SerializeField] public UnityEngine.UI.Button BtnStart2;
		[SerializeField] public UnityEngine.UI.Button BtnStart3;

		public void Clear()
		{
    
    
			BtnStart2 = null;
			BtnStart3 = null;
		}

		public override string ComponentName
		{
    
    
			get {
    
     return "SubPanel1";}
		}
	}
}

结构与之前的 UIBasicPanel 非常相似。

接下来,就可以写一些与子模块相关的逻辑了,关于 UIElement 的基本使用就介绍到这里。

同一个类型的界面打开多个

UIKit.OpenPanel<UIMultiPanel>(new UIMultiPanelData(), PanelOpenType.Multiple);

如何自定义界面加载方式?

继承 AbstractPanelLoaderPool 类,再实现一个 IPanelLoader 的类,参考代码如下:

using System;
using UnityEngine;

namespace QFramework.Example
{
    
    
    public class CustomPanelLoaderExample : MonoBehaviour
    {
    
    
        public class ResourcesPanelLoaderPool : AbstractPanelLoaderPool
        {
    
    
            /// <summary>
            /// Load Panel from Resources
            /// </summary>
            public class ResourcesPanelLoader : IPanelLoader
            {
    
    
                private GameObject mPanelPrefab;

                public GameObject LoadPanelPrefab(PanelSearchKeys panelSearchKeys)
                {
    
    
                    mPanelPrefab = Resources.Load<GameObject>(panelSearchKeys.GameObjName);
                    return mPanelPrefab;
                }

                public void LoadPanelPrefabAsync(PanelSearchKeys panelSearchKeys, Action<GameObject> onPanelLoad)
                {
    
    
                    var request = Resources.LoadAsync<GameObject>(panelSearchKeys.GameObjName);

                    request.completed += operation => {
    
     onPanelLoad(request.asset as GameObject); };
                }

                public void Unload()
                {
    
    
                    mPanelPrefab = null;
                }
            }

            protected override IPanelLoader CreatePanelLoader()
            {
    
    
                return new ResourcesPanelLoader();
            }
        }

        void Start()
        {
    
    
            // 游戏启动时,设置一次
            UIKit.Config.PanelLoaderPool = new ResourcesPanelLoaderPool();
        }
    }
}

如果想要支持 其他方式加载界面则可以通过此方式定制。

另外,QFramework 中的 UIKit 默认使用 ResKit 的方式加载界面。

可以在 QFramework 源码中看到如下代码:

using System;
using UnityEngine;

namespace QFramework
{
    
    
    public class UIKitWithResKitInit
    {
    
    
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
        public static void Init()
        {
    
    
            UIKit.Config.PanelLoaderPool = new ResKitPanelLoaderPool();
        }
    }
    
    ...
}

如果想要使用自定义的方式加载界面,需要将以上代码注释掉。

好了,关于 UIKit 自定义加载界面就简单介绍到这里。

UI Kit 小结

在这一章,UI Kit 的核心功能,我们都接触过了,如下:

  • UIPanel/UIElement 代码生成
  • UIKit 常用 API
    • UIKit.OpenPanel(Async)
    • UIKit.ClosePanel
    • UIKit.CloseSelf
    • UIKit.SetResolution
    • UIKit.Stack.Push、UIPanel.Back(Pop)
  • UIPanel 生命周期
  • UIPanel 测试场景生成工具

只要掌握了以上这些,基本上开发一些界面就没啥问题了。

关于 UIKit 就介绍到这里。

更多内容

猜你喜欢

转载自blog.csdn.net/u010125551/article/details/127343509