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这样
规则由一个正则表达式确定
多层级挂载
一个已经搭载了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,子物体是父类的子类
然后,父物体通过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)