在以往的开发中,尤其是一些初学者在书写UI脚本的时候,比如说脚本中需要获取游戏场景中的UI控件,大家都会习惯性的在脚本中定义一个公开变量(也就是public),然后将脚本挂载在物体上,那么就可以直接将控件拖到变量上了。其实像这种拖拽的做法,很多公司商业开发中也是使用拖拽,也不能说拖拽就不好,只是这种自带的拖拽,他需要你的脚本继承monobehaivour才能挂载在物体上,那么很多公司的UI部分都会使用lua脚本进行开发,那么就无法使用这种自带的拖拽方式了,但是一般公司都会自己封装一套拖拽扩展的。
那么我个人是比较习惯使用代码来进行绑定的,因为这样可以省去拖拽的操作,控件一多的话我个人是不太喜欢的,一直拖来拖去。
所以我们今天就来一起实现一个编辑器扩展,让我们可以不需要去定义公开变量以及不用在拖拽控件。
比如说我们现在要制作一个登录面板,那我们需要获取到这个登录面板下的登录按钮对吧?
那么有两种方法,第一种呢是在脚本中定义一个公开变量。然后将其拖进去
图片那么第二种方法,则是通过路径去查找,使用transform.Find方法,参数是路径。
使用这种方法,就不需要去拖拽了,我们的变量也不需要公开。
所以我们今天要做的就是实现一个编辑器扩展,可以自动的去生成这些定义变量以及查找变量路径,并根据路径去获取相应控件的代码。这样一来,我们就再也不用去拖拽控件以及写这些重复性的代码了。
所有编辑器扩展的脚本都要放在Editor文件夹下,没有自行创建。接着,我们为我们这个扩展创建一个脚本,这里我取名叫UICodeGenerator(UI代码生成器)。
先引入UnityEditor命名空间,然后让脚本继承Editor。
接下里开始编写这个脚本了,首先我们要让玩家可以使用,如何使用呢?这里就要介绍一个特性了。
加了这个特性的意思就是,首先括号中有三个参数,第一个是路径,第二个是是否点击前调用,第三个是显示优先级。我们注重第一个参数,路径,MenuItem实际上是菜单选项的意思,我们这个代码的意思就是,在这个路径下给用户提供一个用于调用我们这个UICodeGenerate方法的地方,当用户点击这个路径的这个选项,就会调用咱们的这个方法。
右击Hierarchy视图中的物体,就会看到我们刚才定义的这个菜单选项了,所以我们现在的工作就是要完善我们的UICodeGenerate方法。
我们的大体逻辑呢,是去遍历该物体下的所有子物体,然后保存路径,根据路径按照我们的规则去生成代码,然后这些代码,我们会将他复制到剪切板,由用户自行粘贴。为什么不直接生成脚本呢?这个我们最后再来回答。
首先,获取用户选中的物体。然后content是用来保存我们生成的代码的,我们先把所有的代码都加进这个content,最后将content的内容复制到剪切板即可。
至于上面的判断,这是我根据自己的项目做的判断,我要求选中的物体名称必须以Panel结尾,这个可以去掉。个人风格而已
看到这个ctrlsType字典,看看AddCtrlType写的是什么。
这里存储的是你需要查找的控件类型,之所以抽出来这么写是为了方便扩展和修改,可以随意增加删除。
这个ctrlsDic是用来保存找到了的控件的路径和控件类型的。
接下来就是主要逻辑了。其实也不难,就是去遍历刚才我们所写的所需要查找的控件类型,然后使用GetComponentsInChildren获取该物体所有该类型的子控件。这里有一个判断,判断该控件名称是否以prefix开头。
只有以这个开头加下划线的控件才会进行绑定,这个也是可修改的,所以单独列举出来。目的是筛选掉那些不需要绑定的控件,比如说背景图等等。然后去遍历这个子控件,并对各个子控件进行一个while循环,当父物体名称不等于我们选中的物体的名称才继续循环。意思就是当父物体的名称和我们选中的物体的名称相同,就证明不需要继续循环了,因为我们的路径是以该物体为根节点的,我们只需要一直遍历,然后用斜杠把各个物体的名称衔接起来就是路径了,然后将路径和控件类型保存到刚才的ctrlsDic字典中即可。
往我们需要生成的代码中添加一行代码,控件类型,然后是名称,然后换行。
就相当于是Button btn;也就是定义控件的代码。
完整的看一下name这个变量。这里做了一个操作,就是将名称中的Auto换成了我们ctrlsType中该控件类型对应的前缀。也就是说,如果有一个按钮叫Auto_login,那么我们的name就是btn_lgoin。
我们定义变量的部分就结束了,然后我们还要继续写查找控件部分的代码。
首先是先给一下注释,并且给下region特性,这个不懂的可以百度下,就是将该部分查找代码可进行折叠。我们将查找的代码都写在一个AutoBindController方法中,用户直接在Start 或者Awake中调用一下即可。
实际上代码逻辑真的都非常简单,上面已经保存了路径了,我们只需要将路径获取出来,然后根据Find这个方法的书写规范来书写字符串就可以了,说白了都是一些切割字符串的方法,这里看不懂的也只能去自行百度了。
最后需要引入这个类
这个类是需要引入System.Text命名空间的,创建一个TextEditor对象,将content的内容赋值给他,然后调用SelectAll也就是选中所有字符串内容,然后调用Copy,就是复制到剪切板了。然后给下提示。
演示一下,比如我现在创建了一个登录面板预制体。
那么我去创建一个登录面板脚本
引入UI命名空间,接着右键面板预制体
选择CodeGenerate选项。
出现这个提示,应该是没问题了。
然后前往脚本。点击粘贴。
折叠一下并在Awake方法中调用一下就可以了。省去了拖拽的操作,将路径交给系统自己书写,避免了自己书写路径出现的错误。
最后说一下刚才留下的问题,为什么不直接生成脚本,而是将代码复制到剪切板中呢?原因是
如果说我们做登录面板做了登录和注册两个功能,然后策划说要增加一个忘记密码的按钮,那怎么办呢?如果我们这里写了直接生成代码文件,会导致我们已经书写的逻辑被覆盖掉,那么我们就必须先保存现有的逻辑,然后进行替换比较麻烦。当然了,其实可以避免,就是使用partical关键字,可以把一个类分成两个脚本进行书写,将空间绑定的脚本分离开,但是这样会导致脚本增多。还有一个原因就是生成cs文件的话需要动态编译,写起生成代码的逻辑不像上面这么简单,而我们复制到剪切板可以直接将代码都写成字符串,而且也不用担心新增控件的情况,只要删掉之前的,然后粘贴新生成的代码即可。
最后附上完整代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Linq;
using UnityEngine.UI;
using System;
using System.Text;
/// <summary>
/// 自动生成UI控件绑定代码
/// </summary>
public class UICodeGenerator:Editor
{
/// <summary>
/// 需要被绑定的UI控件名称中所需前缀,可修改
/// </summary>
static string prefix = "Auto";
/// <summary>
/// 添加需要绑定的控件类型和对应的控件名前缀,可修改
/// 需要被绑定的控件需上面的字符串开头,生成的控件名则自动将上面的前缀转换为下面对应的类型前缀
/// 示例:控件名Auto_login 代码中则是:btn_login
/// </summary>
/// <returns></returns>
static Dictionary<Type, string> AddCtrlType()
{
Dictionary<Type, string> dic = new Dictionary<Type, string>();
dic.Add(typeof(Button), "btn");
dic.Add(typeof(Image), "img");
dic.Add(typeof(RawImage), "rimg");
dic.Add(typeof(Toggle), "tog");
dic.Add(typeof(InputField), "input");
dic.Add(typeof(Text), "txt");
dic.Add(typeof(Slider), "slr");
dic.Add(typeof(ScrollRect), "srlRect");
return dic;
}
/// <summary>
/// 生成代码方法,请勿修改,UI面板名称需以Panel结尾
/// 在Hirerarchy视图中右键需要生成绑定控件的面板预制体,再选择CodeGenerate,代码则会自动复制到剪切板
/// </summary>
[MenuItem("GameObject/CodeGenerate",false,1)]
static void UICodeGenerate()
{
if (Selection.gameObjects.Length < 1)
return;
GameObject gameObject = Selection.gameObjects.First();
if (!gameObject.name.EndsWith("Panel"))
return;
StringBuilder content = new StringBuilder();
Dictionary<Type, string> ctrlsType = AddCtrlType();
Dictionary<string, Type> ctrlsDic = new Dictionary<string, Type>();
foreach(Type type in ctrlsType.Keys)
{
Component[] components = gameObject.transform.GetComponentsInChildren(type);
if (components.Length < 1)
continue;
for(int i = 0; i < components.Length; i++)
{
if (!components[i].transform.name.StartsWith(prefix))
continue;
string name = ctrlsType[type] + components[i].transform.name.Substring(prefix.Length, components[i].transform.name.Length- prefix.Length);
string path = components[i].transform.name;
Transform transform = components[i].transform;
while (!transform.parent.name.Equals(gameObject.name))
{
path = transform.parent.name + "/" + path;
transform = transform.parent;
}
ctrlsDic.Add(ctrlsType[type] + path, type);
content.Append(type.Name + "\t" + name + ";");
content.Append("\n");
}
}
content.Append("\n");
content.Append("#region\t绑定UI控件\t请在面板的初始化方法中调用该方法");
content.Append("\n");
content.Append("void\tAutoBindController()");
content.Append("\n");
content.Append("{");
content.Append("\n");
foreach(var path in ctrlsDic.Keys)
{
string prefixTemp = ctrlsType[ctrlsDic[path]];
string realPath = path.Substring(prefixTemp.Length, path.Length - prefixTemp.Length);
string nameTemp = realPath.Split('/')[realPath.Split('/').Length - 1];
string name = prefixTemp + nameTemp.Substring(prefix.Length, nameTemp.Length - prefix.Length);
content.Append("\t" + name + "\t=\t" + "transform.Find(\"" + realPath + "\").GetComponent<" + ctrlsDic[path].Name + ">();");
content.Append("\n");
}
content.Append("\t}");
content.Append("\n");
content.Append("#endregion");
TextEditor text = new TextEditor();
text.text = content.ToString();
text.SelectAll();
text.Copy();
Debugger.Log(">>>>>>>>>>>>>>复制成功,请前往UI视图层脚本粘贴代码<<<<<<<<<<<<<<<<<<");
}
}
代码都在这里了。可以自行尝试。