跳转至

UIBinding

介绍

当一个GameObject搭载了脚本UIBinding之后,脚本可以自动探测到该GameObject本身及所有子物体中特定的Component(UI组件居多,也有第三方插件等)以及物体本身的GameObject、Transform

被检测到的Component会被存储在一个List中,这个List可以本身以及其中的元素都可以传给lua中,用以UI开发等

例如,在Unity中设计好了UI Prefab,Prefab搭载了UIBinding这个脚本,脚本会探测到该对象中符合规则的Component存在一个list中。当在lua中进行UI逻辑的编写时,就可以通过特定方式直接获取到list中捕捉到的Component,便于开发,下面会有更详细的例子

使用方法

首先要把UIBinding脚本挂载到GameObject上

挂在UIBinding的GameObject可以看作是UI中的Root节点,当在Hierarchy界面看到挂载UIBinding的GameObject左侧有一个绿色的#代表添加成功,该对象已经成为了一个Root根节点

脚本用一个List来存储所有的可以被检测到的组件,lua最终得到的也是这个List

在Inspector中会将node中的所有元素显示出来

例如该UIPrefab GMModifyAttribute 中的所有nodes元素都显示在了Inspector中

添加

添加方式是在Root中直接创捷你想要的物体,比如创建一个Button(因为项目中用到的是自己写的Button,所以创建的Button为Engine.UIButton),然后将其命名为btnAbc,可以看到该Button组件已经被父物体Root所捕获到了,原因是btnAbc左侧的绿色@标识,这代表它是Root中的一个节点node,状态为Binded即已绑定

非自动模式

勾选掉Auto选项,即进入手动模式,Component不会自动检测,而是手动选择

原先的绿色@标识将会变为 + 或 -,代表是否要添加到nodes中,当左侧为 - 时,代表已经进入nodes中

如果想取消,则点击 - 即可,- 会变成 +,同时可以看到脚本选择性的获取到了Component

删除

如果想删除子物体的话,把该子物体的GameObject删掉就好了,Root在每一帧都会检测,然后将其从List中移除

捕获条件

如果想要被Root所捕获,子物体GameObject必须符合以下几个要求

1.允许的Component类型

可以被检测到的Component包括如下,后续是可以继续添加想要能被捕获的Component

Component 命名规则
UnityEngine.UI.Text txt
UnityEngine.UI.Image img
UnityEngine.UI.RawImage rmg
UnityEngine.UI.ScrollRect srl
UnityEngine.UI.Toggle tgl
UnityEngine.UI.ToggleGroup tgg
UnityEngine.UI.InputField inp
UnityEngine.UI.Slider sld
UnityEngine.UI.Dropdown dro
Engine.UIButton btn
EmojiText lit
SuperTextMesh stm
TextMeshProUGUI tmp
RectTransform rct
Transform trn
GameObject neg

2.命名规则

根据想要添加的GameObject上搭载的Component的类型,名字的前三位是有规定的

以上面的Button为例,因为搭载的是Button,所以GameObject的名字前三位就应该是btn,以此类推

命名规则类似大驼峰命名法,HelloWorld这样

规则由一个正则表达式确定

static Regex nameRegex = new Regex("^[a-z]{3}([A-Z][a-z0-9]+)+$");

多层级挂载

一个已经搭载了UIBinding的GameObject的子物体也是可以搭载UIBinding的

这样的两个Root是互相独立的,父物体Root不会把子物体Root的Component加载到自己的Component列表中

Lua中的使用 - 重点

脚本中的Nodes通过Surface.lua来传送到lua逻辑中

例如UI,lua中所有UI的基类BaseUIPanel中的self.view就是Root节点中的nodes,nodes中存储的所有Component都可以通过self.view.(GameObject的名字)的方式来调用

下面是具体例子

以上面的MainUIPanel为例,当我设计好了这个Prefab之后,挂载UIBinding脚本,将想要获取的Component改成要求的名字,在Prefab预览界面中就该是这样子的,可以看到Button都被获取到了

btnPet的逻辑是打开游戏的宠物界面,所以lua中的逻辑就应该是给btnPet加一个点击事件监听

在MainUIModule.lua中(MainUIPanel的逻辑在这里),当UI在运行时创建完毕后,需要给btnPet上监听,所以要这么做

function M:_OnCreateChildFinished(params)

    -- do somehing

    -- 获取到component
    self.view.btnPet:SetOnClick( function()
        UIManager.Show(NM.md.pet)
    end)
end

SetOnClik是Engine.UIButton中的一个函数,直接调用意味着self.view.btnPet获取到的就是Engine.UIButton本身

其他UI也是如此,可以将获取到的Component直接使用,调用C#端的函数进行逻辑编写

多层级

如果Root中包含另外一个Root,在lua中如何创建父子关系?

正常情况下,父物体与子物体是两个独立的Prefab,父物体通过AddChild来将子物体实例化然后加到自己的子物体table中

但是如果在Prefab中就已经成为了父子关系,则情况就不大一样,涉及到C#和lua绑定的两种方式

正常情况如下代码所示

local createParams = {
    path = "AnswerRight",
    parent = self.view.negEffect,
    addOrder = NM.UIRenderOrderEnum.uiWindow,
}
self:AddChild("effect", UI.UIEffect, createParams, true, true)

前两个参数分别是是子物体的类名和类型,createParams中的path是子物体Prefab资源路径。所以由此可见,runtime中添加子物体是通过Prefab实例化之后再确认父子关系

多层级情况则是下面是例子,如图可见,Prefab中就已存在父子关系

例如AttributeDiagramShort的功能是显示角色属性五维图,但是五个属性的维度本身自己也是个Prefab即AttributeDiagramShortItem,所以就涉及到了多层级调用的问题

首先,父物体子物体都有自己的UI逻辑。父物体是AttributeDiagram.lua,子物体是父类的子类

local ITEM = class(BaseUIPanel, "AttributeDiagramItem")

-- ITEM 逻辑
-- do something

然后,父物体通过AddChild将五个ITEM作为子物体

function P:_OnCreateFinished()
    -- 读取数据config
    local cfgs = ConfigManager.role.GetAdventureAttrsCfg()

    for i, data in ipairs(cfgs) do
        self:AddChild(i, ITEM, {
            gameObject = self.view["rctItem"..i],       -- 通过gameObject进行AddChild
            data = data,
            index = ENAME_TO_INDEX[data.ename] or i,
        }):Show()
    end
end

AddChild的第三个参数中的prefab变为了gameObject,gameObject为UIBinding获取到的五个ITEM

这样就能将预制体中的五个子物体加入到父物体的Child中

其他

还可以根据Component的名称首字母进行排序,通过点击sort按键执行

check则可以将nodes中的Component逐个进行检查,检查是否合规或者是存在,从而刷新nodes中合规的元素

原理

UIBinding.cs

概览

核心就是两个事件注册,一个是在Unity编辑器初始化阶段注册的监听Hierarchy界面的结构变化,一个是脚本挂载阶段注册的Hierarchy界面的逐帧执行函数

nodes用以存储所有检测到的符合要求的Component

回到一开始,当Hierarchy目录发生变化后,编辑器中所有搭载到GameObject/Prefab上的UIBinding,都会遍历所有的子物体,逐一判断子物体是否符合要求,符合要求便可加入到nodes

逐帧执行的内容则是,显示在Hierarchy界面中的UIBinding遍历nodes中的Component,判断出node类型,根据类型在其挂载的GameObject旁绘制标记

至于前者如何找到编辑器中所有的UIBinding,则需要静态列表当作缓存池来存储

下面细说

初始阶段

当Unity编辑器刚开始初始化的时候,就会注册一个事件,这个事件监听Hierarchy界面的结构变化

通过事件EditorApplication.hierarchyChanged

注册的是一个委托,即每当Hierarchy界面的结构变化,脚本都会执行这个委托,即Hierarchy变化阶段

而当脚本挂载上GameObject的那一刻,UIBinding就会加入到静态列表中

同时向Unity编辑器注册一个事件,即Hierarchy目录面板为焦点(鼠标在面板上)时每帧执行一个函数,即帧执行阶段

通过事件EditorApplication.hierarchyWindowItemOnGUI

Hierarchy变化阶段

委托通过循环遍历存储UIBinding的静态缓存列表,以及递归的方式,深层遍历UIBinding下的所有子物体,逐一判断子物体是否符合要求

通过检测GameObject.name是否符合正则表达式,然后检测GameObject中是否含有符合要求的Component,全部符合则Add进nodes中,否则都没有效果

帧执行阶段

该阶段主要是绘图和逐帧检测当前nodes中元素类型

获取Hierarchy中所有的GameObject,用一个枚举来定义各种类型,检测该GameObject在UIBinding中属于何种类型,类型一共有四种:

  • Root - 根节点
  • Free - 未绑定
  • Binded - 已绑定
  • Willing - 特殊

第四种类型Willing基本不会出现,出现了会debug

如果在nodes元素上找到了该有的Component,则代表没问题,已绑定

最后,根据类型的不同,在Hierarchy中的GameObject左侧绘制绿色标记

留给lua的接口

UIBinding类中的函数有一部分是留给lua的接口,lua通过这些函数来获取nodes中的元素或者nodes本身

例如QueryNode(string name),lua传给函数Component在nodes中的名字,C#通过name去核对nodes中的名称来找到那个Component,然后传到lua中

除了通过名字,也可以通过索引来获取nodes中的元素

Surface.lua

项目中的UI一般是通过UIManager.lua来管理控制生成的,在UIManager的Show中会调用UI中的Create,当获取到UI的预制体实例化出来的GameObject或者是本来就存在的GameObject之后,通过Surface.lua来将GameObject与lua进行绑定

UI绑定分两种

共有的部分是获取GameObject上的一些其他的信息,例如RectTransform,GameObject等

至于不同的地方

通过Prefab加载实例化

lua类中资源路径实例化出的GameObject,Prefab资源路径

如果是用的此方法,则会通过缓存来获取Component

Surface中有一个table作为UIBinding的缓存

该缓存以Prefab资源路径string作为唯一标识当作Key,此Prefab实例化出的所有GameObject都使用这个缓存池

如果未缓存过,则将GameObject上的UIBinding中nodes中所有元素根据索引顺序转换成一个int索引table,即以nodes元素的名字作为key,存储该元素在nodes中的索引值

索引table再根据Prefab string存储到缓存池中

调用则是通过缓存池进行调用,根据Prefab路径找到之前存的索引table,获取索引值,根据索引值调用C#函数获得Component

通过GameObjec直接绑定

即 目录 多层级调用 中的内容,父Root想控制子Root的Component

子Root的GameObject就需要直接绑定UIBinding,从而跳过Prefab缓存,因为这些GameObject并没有Prefab资源路径作为唯一标识

所以当调用他们的Component时,直接通过nodes元素名字调用,通过C#的QueryNode(string name)