ZMUIL4——自动化系统

自动化系统要做的工作

  • 在拼接完UI面板后,自动化系统可以根据特定的解析规则解析UI面板,得到各个需要管理的UI控件的控件类型和控件名
    根据以上的解析结果自动生成对应的组件查找脚本/组件数据脚本以及Window逻辑类脚本
    自动化系统会自动完成繁杂的关联组件,监听函数声明与添加,重写生命周期函数等工作,让我们可重心放在UI控件交互逻辑的编写上

  • 组件查找脚本/组件数据脚本负责获取并管理UI控件,供Window逻辑类调用UI控件,初始化方法里将Window逻辑类里监听函数添加到控件中

  • Window逻辑类是我们编写界面的交互的脚本,我们直接在该脚本内生成的各个函数内编写逻辑即可,对外接口函数在API Function​​代码块内编写
    自动化系统会自动让Window逻辑类继承WindowBase​​,重写各个生命周期函数,预留对外的API Function​​代码块,声明各个UI控件的监听函数
    当UI窗口添加控件时,可以重新生成组件查找脚本/组件数据脚本,再生成Window逻辑脚本,原来在Window逻辑脚本编写的内容不会被覆盖

  • 组件查找脚本和组件数据脚本的作用是一致的,区别在于:

    1. 组件查找脚本会在UI窗口刚加载出来时,根据解析出来的路径使用Transform.Find​查找并关联组件
    2. 组件数据脚本继承MonoBehaviour​,在脚本编译完毕后会自动的UI窗口对象挂载自己,并通过解析结果将各个组件绑定到自己身上

    由于组件查找脚本是在运行时使用Transform.Find关联组件,因此性能不如编译结束后就自动关联UI控件的组件数据脚本

  • 自动化系统有两种解析组件数据的规则

    1. [Button]​​中括号为规范,中间为组件类型进行解析。
    2. 以打Tag的方式进行解析,Tag需要表明该物体所属组件类型。

一、自动化系统配置文件

自动化系统需要配置文件指定使用哪种脚本,使用哪种解析方式,脚本的生成位置,存放解析数据的字符串的键,以及确认对象需要解析的Tag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using UnityEngine;

public enum GeneratorType
{
Find, //组件查找
Bind, //组件绑定
}

public enum ParseType
{
Name, //名字解析
Tag, //Tag解析
}

public class GeneratorConfig
{
//组件绑定脚本生成后存放路径
public static string BindComponentGeneratorPath = Application.dataPath + "/ZMUIFrameWork/Scripts/BindComponent";
//组件查找脚本生成后存放路径
public static string FindComponentGeneratorPath = Application.dataPath + "/ZMUIFrameWork/Scripts/FindComponent";
//窗口逻辑脚本生成后存放路径
public static string WindowGeneratorPath = Application.dataPath + "/ZMUIFrameWork/Scripts/Window";
//存储解析得到的UI控件数据的键,用于PlayerPrefs
public static string OBJDATALIST_KEY = "objdataList";
//使用组件绑定脚本还是组件查找脚本
public static GeneratorType GeneratorType = GeneratorType.Bind;
//解析规则是使用对象名还是Tag
public static ParseType ParseType = ParseType.Name;
//解析规则使用Tag时,当对象使用以下数组中的Tag时就解析它
public static string[] TAGArr = { "Image", "RawImage", "Text", "Button", "Toggle", "Slider", "Dropdown", "InputField", "Canvas", "Panel", "ScrollRect" };
}

二、组件数据查找脚本的生成

组件查找脚本负责获取并管理UI控件,供Window逻辑类调用UI控件,初始化方法里将Window逻辑类里监听函数添加到控件中

组件查找脚本代码需要生成如下部分:

  • 脚本为自动生成的提示,命名空间的引用
  • 命名空间与数据查找类的声明
  • 数据查找类要包括所有的需要管理的UI控件的字段
  • 数据查找类要包括初始化方法,其中包括使用Transform.Find​查找并管理组件的语句,以及将对应的Window逻辑类的监听函数添加到UI控件的语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//自动化脚本生成示例

/*-----------------------------------
*Title: UI自动化组件查找代码工具
*Author: 铸梦
*Date:2024/1/25 14:52:13
*Description: 变量需要以[Text]括号加组件类型格式进行声明,然后右键窗口物体——一键生成UI组件查找脚本即可
*注意,以下文件是自动生成的,任何手动修改都会被下次生成覆盖,若手动修改后,尽量避免自动生成
*-----------------------------------*/
using UnityEngine.UI;
using UnityEngine;

namespace ZMUIFrameWork
{
public class TempWindowUIComponent
{
public Button CloseButton;

public void InitComponent(WindowBase target)
{
//组件查找
CloseButton = target.transform.Find("UIContent/[Button]Close").GetComponent<Button>();


//组件事件绑定
TempWindow mWindow = (TempWindow)target;
target.AddButtonClickListener(CloseButton, mWindow.OnCloseButtonClick);
}
}
}

组件数据查找脚本的生成主要在GeneratorFindComponentTool​内实现,它继承Editor
它需要为右键菜单提供“生成组件查找脚本”选项(快捷键Shift+U) ,因此需要为一个函数添加[MenuItem]​特性作为入口函数(#U​是指定快捷键)

Editor 类是用于拓展和定制 Unity 内置组件的编辑器功能

image

对窗口对象右键选择该选项后对选中的窗口对象进行解析,生成组件数据查找脚本字符串,创建脚本的主函数执行步骤如下:

1
2
3
4
5
6
7
8
9
10
11
//生成组件查找脚本方法,选中游戏对象后在右键菜单点击“生成组件查找脚本”调用
[MenuItem("GameObject/生成组件查找脚本(Shift+U) #U", false, 0)]
private static void CreateFindComponentScripts()
{
1.获取在编辑器里选中的内容,判断是否为游戏对象,不是则报错并直接返回空
2.设置脚本的生成路径
3.调用PresWindowNodeData或PerseWindowNodeData,将获取到的窗口传入,解析数据
4.将解析到的数据使用PlayerPrefs持久化,后续生成Window数据类脚本还会用到
5.通过解析的内容生成对应脚本字符串
6.将脚本字符串与生成脚本路径传入预览窗口显示
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class EditorObjectData
{
public int instanceID;
public string fieldName;
public string fieldType;
}

public class GeneratorFindComponentTool : Editor
{
public static Dictionary<int, string> objFindPathDic; //Key是物体的instanceId,value是物体的查找路径(可通过Transform.Find来寻找对象)
public static List<EditorObjectData> objDataList; //查找对象的数据

[MenuItem("GameObject/生成组件查找脚本(Shift+U) #U", false, 0)]
private static void CreateFindComponentScripts()
{
//获取在编辑器里选中的内容
GameObject obj = Selection.objects.First() as GameObject;
if (obj == null)
{
Debug.LogError("需要选择GameObject");
return;
}
objDataList = new List<EditorObjectData>();
objFindPathDic = new Dictionary<int, string>();

//设置脚本的生成路径
if (!Directory.Exists(GeneratorConfig.FindComponentGeneratorPath))
{
Directory.CreateDirectory(GeneratorConfig.FindComponentGeneratorPath);
}
//解析窗口所有UI组件数据
if (GeneratorConfig.ParseType == ParseType.Tag)
ParseWindowDataByTag(obj.transform, obj.name);
else
PresWindowNodeData(obj.transform, obj.name);
//储存字段名称
string dataListJson = JsonConvert.SerializeObject(objDataList);
PlayerPrefs.SetString(GeneratorConfig.OBJDATALIST_KEY, dataListJson);
//生成C#脚本文件
string csScriptText = CreateCS(obj.name);
string csPath = GeneratorConfig.FindComponentGeneratorPath + "/" + obj.name + "UIComponent.cs";
//显示预览窗口
UIWindowEditor.ShowWindow(csScriptText, csPath);
}

// 解析窗口节点数据
public static void PresWindowNodeData(Transform transform, string WindowName)...

// 通过Tag解析窗口节点数据
public static void ParseWindowDataByTag(Transform transform, string WindowName)...

// 生成C#脚本字符串
public static string CreateCS(string name)...
}

1.解析窗口节点数据

脚本文本的生成需要对窗口对象解析如下数据:

  • UI控件的类型
  • UI控件的名字
  • UI控件对象相对于窗口对象的查找路径

解析方法的思路如下(以解析对象名为例)

1
2
3
4
5
6
7
8
9
10
11
//解析窗口节点数据,数据存入到字典与列表,参数一是窗口的Transform,参数二是窗口名(用于确认第一次调用该函数的窗口对象)
public static void PresWindowNodeData(Transform transform, string WindowName)
{
//1.遍历该游戏对象所拥有的子游戏对象,如果有子对象,执行下面的流程,没有就会跳过
//2.如果子游戏对象名字为“[类型名]字段名”,进入解析流程
//3.从游戏对象名中截取类型名和字段名,再获取这个游戏对象的InstanceID,再存入到对应的EditorObjectData对象内,
//4.将EditorObjectData对象存入到objDataList内
//5.向上遍历游戏对象的父节点,直到遍历到最初的传入的窗口对象,获取它们的名字,拼接为查找路径,用于Transform.Find查找对象
//6.以游戏对象的InstanceID为键,查找路径为值,存入到objFindPathDic
//7.将遍历的这个游戏对象再次传入到PresWindowNodeData(),进行递归解析,将所有的子对象都执行上述流程(参数二与最初传入的一致)
}

我们可以通过两种规则让UI控件可以被解析出类型与名字:

  • 将UI控件对象名命名为[控件类型]控件名​,如:[Button]Close​,解析时针对对象名进行解析,得到类型与名字信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    GameObject obj = transform.GetChild(i).gameObject;                    //获取窗口下的对象
    string name = obj.name;
    //如果对象名包括"[类型名]",说明该对象需要作为UI组件进行解析
    if (name.Contains("[") && name.Contains("]"))
    {
    int index = name.IndexOf("]") + 1; //得到"]"后第一个字符的索引
    string fieldName = name.Substring(index, name.Length - index); //获取字段名字
    string fieldType = name.Substring(1, index - 2); //获取字段类型
    //后略...
    }
  • 将各种UI控件的类型作为Tag,让UI控件对象使用这些Tag,解析窗口时发现使用控件类型Tag的对象就获取Tag名作为类型,对象名来作为名字

    1
    2
    3
    4
    5
    6
    7
    8
    9
    GameObject obj = transform.GetChild(i).gameObject;        //获取窗口下的对象
    string tagName = obj.tag;
    //如果Tag是特定的UI控件Tag,说明该对象需要作为UI组件进行解析
    if (GeneratorConfig.TAGArr.Contains(tagName))
    {
    string fieldName = obj.name; //获取字段名字
    string fieldType = tagName; //获取字段类型
    //后略...
    }

    其中GeneratorConfig.TAGArr​是配置文件中设定好的需要解析的对象的Tag

组件查找脚本还需要获取UI控件对象相对于窗口对象的查找路径,
因此需要对传入的对象向上遍历,获取游戏对象的父节点名字,直到遍历到最初的传入的窗口对象,将遍历到的对象名拼接起来,得到查找路径

最终得到的解析方法为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
public static Dictionary<int, string> objFindPathDic;   //Key是物体的instanceId,value是物体的查找路径(可通过Transform.Find来寻找对象)
public static List<EditorObjectData> objDataList; //查找对象的数据

/// <summary>
/// 解析窗口节点数据
/// </summary>
/// <param name="transform">要解析的窗口的Transform</param>
/// <param name="WindowName">窗口名</param>
public static void PresWindowNodeData(Transform transform, string WindowName)
{
//当传入的对象下已无子对象,这个函数流程都会跳过
for (int i = 0; i < transform.childCount; i++)
{
GameObject obj = transform.GetChild(i).gameObject;
string name = obj.name;
//如果对象名包括"[类型名]",说明该对象需要作为UI组件进行解析
if (name.Contains("[") && name.Contains("]"))
{
//字段名与字段类型的获取
int index = name.IndexOf("]") + 1; //得到"]"后第一个字符的索引
string fieldName = name.Substring(index, name.Length - index); //获取字段名字
string fieldType = name.Substring(1, index - 2); //获取字段类型
//将获取到的字段名与类型名以及insID存入到列表内
objDataList.Add(new EditorObjectData { fieldName = fieldName, fieldType = fieldType, instanceID = obj.GetInstanceID() });

//计算该节点的查找路径,向上遍历游戏对象的父节点,直到遍历到最初的传入的窗口对象,获取它们的名字,拼接为查找路径
string objPath = name; //该游戏对象的查找路径
bool isFindOver = false; //查找路径是否拼接完成
Transform parent = obj.transform; //当前遍历到的父对象
//这里的循环可以理解为记录已经向上遍历的层数,每走完一次这个循环流程,查找路径都会多一段,j < 20即最高就遍历20层
for (int j = 0; j < 20; j++)
{
//这里的循环是每次都从本游戏对象出发,向上计数,判断是否查找完整的逻辑也在这里
//踩坑,切勿搞反了这里的k <= j,否则导致循环空转,浪费性能
for (int k = 0; k <= j; k++)
{
//当k == j,说明需要到了需要进行判断的层级
if (k == j)
{
parent = parent.parent; //使parent指向它的父对象,接下来用于确认是否再次向上获取父对象名字
//对比父对象名字是否为最初传入的窗口名,若相等,即父节点是当前窗口,如果父节点是当前窗口,说明查找路径已经完整
if (string.Equals(parent.name, WindowName))
{
isFindOver = true;
break;
}
//若不相等,说明查找路径不完整,在路径字符串前拼上本次遍历的父对象的名字,继续循环
else
{
objPath = objPath.Insert(0, parent.name + "/");
}
}
} //end for k
//若查找结束,说明这个控件的查找路径已经完整,应当跳出循环
if (isFindOver)
{
break;
}
} //end for j
//将拼接完成的路径传入到字典内
objFindPathDic.Add(obj.GetInstanceID(), objPath);
}
//传入对象,重复上述流程,以递归的形式将所有的子对象全部解析一遍
PresWindowNodeData(transform.GetChild(i), WindowName);
} //end for i
}

/// <summary>
/// 通过Tag解析窗口节点数据
/// </summary>
/// <param name="transform">要解析的窗口的Transform</param>
/// <param name="WindowName">窗口名</param>
public static void ParseWindowDataByTag(Transform transform, string WindowName)
{
//当传入的对象下已无子对象,这里的for循环不执行
for (int i = 0; i < transform.childCount; i++)
{
GameObject obj = transform.GetChild(i).gameObject;
string tagName = obj.tag;
//如果对象名包括"[类型名]",说明该对象需要作为UI组件进行解析
if (GeneratorConfig.TAGArr.Contains(tagName))
{
string fieldName = obj.name; //获取字段名字
string fieldType = tagName; //获取字段类型

objDataList.Add(new EditorObjectData { fieldName = fieldName, fieldType = fieldType, instanceID = obj.GetInstanceID() });

//计算该节点的查找路径
string objPath = tagName;
bool isFindOver = false;
Transform parent = obj.transform;
for (int j = 0; j < 20; j++)
{
for (int k = 0; k <= j; k++)
{
if (k == j)
{
parent = parent.parent; //使临时变量parent指向它的父对象
//对比两者名字,若相等,父节点是当前窗口,如果父节点是当前窗口,说明查找已经结束
if (string.Equals(parent.name, WindowName))
{
isFindOver = true;
break;
}
//若不相等,说明查找继续,在路径字符串前拼上父对象的名字
else
{
objPath = objPath.Insert(0, parent.name + "/");
}
}
}
//若查找结束,说明这个控件的查找路径已经完整,应当跳出循环
if (isFindOver)
{
break;
}
}
objFindPathDic.Add(obj.GetInstanceID(), objPath);
}
ParseWindowDataByTag(transform.GetChild(i), WindowName);
}
}

值得一提的是,解析方法执行完毕回到主函数后,主函数会执行将解析结果持久化的语句,目的是为了生成Window逻辑类脚本时也可以使用这些数据

2.生成脚本字符串

当窗口解析完成后,就需要根据解析结果,按照上面给出的组件查找脚本文本格式,生成脚本字符串,之后即可将字符串传入到预览窗口内供预览

声明字段与初始化方法里的调用Find​语句和调用Window逻辑类添加监听函数的语句都会循环遍历之前解析出来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/// <summary>
/// 生成C#脚本字符串
/// </summary>
/// <param name="name">窗口名</param>
/// <returns>脚本字符串</returns>
public static string CreateCS(string name)
{
StringBuilder scriptText = new StringBuilder();
string nameSpaceName = "ZMUIFrameWork";
//添加引用
scriptText.AppendLine("/*-----------------------------------");
scriptText.AppendLine(" *Title: UI自动化组件查找代码工具");
scriptText.AppendLine(" *Author: 铸梦");
scriptText.AppendLine(" *Date:" + System.DateTime.Now);
scriptText.AppendLine(" *Description: 变量需要以[Text]括号加组件类型格式进行声明,然后右键窗口物体——一键生成UI组件查找脚本即可");
scriptText.AppendLine(" *注意,以下文件是自动生成的,任何手动修改都会被下次生成覆盖,若手动修改后,尽量避免自动生成");
scriptText.AppendLine(" *-----------------------------------*/");
scriptText.AppendLine("using UnityEngine.UI;");
scriptText.AppendLine("using UnityEngine;");
scriptText.AppendLine();

//生成命名空间
if (!string.IsNullOrEmpty(nameSpaceName))
{
scriptText.AppendLine($"namespace {nameSpaceName}");
scriptText.AppendLine("{");
}
scriptText.AppendLine($" public class {name + "UIComponent"}");
scriptText.AppendLine(" {");

//根据字段数据列表 声明字段
foreach (var item in objDataList)
{
scriptText.AppendLine(" public " + item.fieldType + " " + item.fieldName + item.fieldType + ";\r\n"); //写\r\n是因为万恶的CRLF
}

//声明初始化组件接口
scriptText.AppendLine(" public void InitComponent(WindowBase target)");
scriptText.AppendLine(" {");
scriptText.AppendLine(" //组件查找");
//根据查找路径字典 和字段数据列表生成组件查找代码;
foreach (var item in objFindPathDic)
{
EditorObjectData itemData = GetEditorObjectData(item.Key);
string relFieldName = itemData.fieldName + itemData.fieldType;
if (string.Equals("GameObject", itemData.fieldType))
{
scriptText.AppendLine($" {relFieldName} = target.transform.Find(\"{item.Value}\").gameObject;");
}
else if (string.Equals("Transform", itemData.fieldType))
{
scriptText.AppendLine($" {relFieldName} = target.transform.Find(\"{item.Value}\").transform;");
}
else
{
scriptText.AppendLine($" {relFieldName} = target.transform.Find(\"{item.Value}\").GetComponent<{itemData.fieldType}>();");
}
}
scriptText.AppendLine(" ");
scriptText.AppendLine(" ");
scriptText.AppendLine(" //组件事件绑定");
//得到逻辑类 WindowBase 强转为目标派生类
scriptText.AppendLine($" {name} mWindow = ({name})target;");

//生成UI事件绑定代码
foreach (var item in objDataList)
{
string type = item.fieldType;
string methodName = item.fieldName;
string suffix;
if (type.Contains("Button"))
{
suffix = "Click";
scriptText.AppendLine($" target.AddButtonClickListener({methodName}{type}, mWindow.On{methodName}Button{suffix});");
}
if (type.Contains("InputField"))
{
scriptText.AppendLine($" target.AddInputFieldListener({methodName}{type}, mWindow.On{methodName}InputChange, mWindow.On{methodName}InputEnd);");
}
if (type.Contains("Toggle"))
{
suffix = "Change";
scriptText.AppendLine($" target.AddToggleClickListener({methodName}{type}, mWindow.On{methodName}Toggle{suffix});");
}
}
scriptText.AppendLine(" }");
scriptText.AppendLine(" }");
if (!string.IsNullOrEmpty(nameSpaceName))
{
scriptText.AppendLine("}");
}
return scriptText.ToString();
}

//根据UI控件的instanceID,获取该UI控件的解析数据
public static EditorObjectData GetEditorObjectData(int instanceID)
{
foreach (var item in objDataList)
{
if (item.instanceID == instanceID)
{
return item;
}
}
return null;
}

GeneratorFindComponentTool代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
using Newtonsoft.Json;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;

public class GeneratorFindComponentTool : Editor
{
public static Dictionary<int, string> objFindPathDic; //Key是物体的instanceId,value是物体的查找路径(可通过Transform.Find来寻找对象)
public static List<EditorObjectData> objDataList; //查找对象的数据

[MenuItem("GameObject/生成组件查找脚本(Shift+U) #U", false, 0)]
private static void CreateFindComponentScripts()
{
//获取在编辑器里选中的内容
GameObject obj = Selection.objects.First() as GameObject;
if (obj == null)
{
Debug.LogError("需要选择GameObject");
return;
}
objDataList = new List<EditorObjectData>();
objFindPathDic = new Dictionary<int, string>();

//设置脚本的生成路径
if (!Directory.Exists(GeneratorConfig.FindComponentGeneratorPath))
{
Directory.CreateDirectory(GeneratorConfig.FindComponentGeneratorPath);
}
//解析窗口所有UI组件数据
if (GeneratorConfig.ParseType == ParseType.Tag)
ParseWindowDataByTag(obj.transform, obj.name);
else
PresWindowNodeData(obj.transform, obj.name);
//储存字段名称
string dataListJson = JsonConvert.SerializeObject(objDataList);
PlayerPrefs.SetString(GeneratorConfig.OBJDATALIST_KEY, dataListJson);
//生成C#脚本文件
string csScriptText = CreateCS(obj.name);
string csPath = GeneratorConfig.FindComponentGeneratorPath + "/" + obj.name + "UIComponent.cs";
UIWindowEditor.ShowWindow(csScriptText, csPath);
}

/// <summary>
/// 解析窗口节点数据
/// </summary>
/// <param name="transform">要解析的窗口的Transform</param>
/// <param name="WindowName">窗口名</param>
public static void PresWindowNodeData(Transform transform, string WindowName)
{
//当传入的对象下已无子对象,这里的for循环不执行
for (int i = 0; i < transform.childCount; i++)
{
GameObject obj = transform.GetChild(i).gameObject;
string name = obj.name;
//如果对象名包括"[类型名]",说明该对象需要作为UI组件进行解析
if (name.Contains("[") && name.Contains("]"))
{
int index = name.IndexOf("]") + 1; //得到"]"后第一个字符的索引
string fieldName = name.Substring(index, name.Length - index); //获取字段名字
string fieldType = name.Substring(1, index - 2); //获取字段类型

objDataList.Add(new EditorObjectData { fieldName = fieldName, fieldType = fieldType, instanceID = obj.GetInstanceID() });

//计算该节点的查找路径
string objPath = name;
bool isFindOver = false;
Transform parent = obj.transform;
for (int j = 0; j < 20; j++)
{
for (int k = 0; k <= j; k++)
{
if (k == j)
{
parent = parent.parent; //使临时变量parent指向它的父对象
//对比两者名字,若相等,父节点是当前窗口,如果父节点是当前窗口,说明查找已经结束
if (string.Equals(parent.name, WindowName))
{
isFindOver = true;
break;
}
//若不相等,说明查找继续,在路径字符串前拼上父对象的名字
else
{
objPath = objPath.Insert(0, parent.name + "/");
}
}
}
//若查找结束,说明这个控件的查找路径已经完整,应当跳出循环
if (isFindOver)
{
break;
}
}
objFindPathDic.Add(obj.GetInstanceID(), objPath);
}
PresWindowNodeData(transform.GetChild(i), WindowName);
}
}

/// <summary>
/// 通过Tag解析窗口节点数据
/// </summary>
/// <param name="transform">要解析的窗口的Transform</param>
/// <param name="WindowName">窗口名</param>
public static void ParseWindowDataByTag(Transform transform, string WindowName)
{
//当传入的对象下已无子对象,这里的for循环不执行
for (int i = 0; i < transform.childCount; i++)
{
GameObject obj = transform.GetChild(i).gameObject;
string tagName = obj.tag;
//如果对象名包括"[类型名]",说明该对象需要作为UI组件进行解析
if (GeneratorConfig.TAGArr.Contains(tagName))
{
string fieldName = obj.name; //获取字段名字
string fieldType = tagName; //获取字段类型

objDataList.Add(new EditorObjectData { fieldName = fieldName, fieldType = fieldType, instanceID = obj.GetInstanceID() });

//计算该节点的查找路径
string objPath = tagName;
bool isFindOver = false;
Transform parent = obj.transform;
for (int j = 0; j < 20; j++)
{
for (int k = 0; k <= j; k++)
{
if (k == j)
{
parent = parent.parent; //使临时变量parent指向它的父对象
//对比两者名字,若相等,父节点是当前窗口,如果父节点是当前窗口,说明查找已经结束
if (string.Equals(parent.name, WindowName))
{
isFindOver = true;
break;
}
//若不相等,说明查找继续,在路径字符串前拼上父对象的名字
else
{
objPath = objPath.Insert(0, parent.name + "/");
}
}
}
//若查找结束,说明这个控件的查找路径已经完整,应当跳出循环
if (isFindOver)
{
break;
}
}
objFindPathDic.Add(obj.GetInstanceID(), objPath);
}
ParseWindowDataByTag(transform.GetChild(i), WindowName);
}
}

/// <summary>
/// 生成C#脚本字符串
/// </summary>
/// <param name="name">窗口名</param>
/// <returns>脚本字符串</returns>
public static string CreateCS(string name)
{
StringBuilder scriptText = new StringBuilder();
string nameSpaceName = "ZMUIFrameWork";
//添加引用
scriptText.AppendLine("/*-----------------------------------");
scriptText.AppendLine(" *Title: UI自动化组件查找代码工具");
scriptText.AppendLine(" *Author: 铸梦");
scriptText.AppendLine(" *Date:" + System.DateTime.Now);
scriptText.AppendLine(" *Description: 变量需要以[Text]括号加组件类型格式进行声明,然后右键窗口物体——一键生成UI组件查找脚本即可");
scriptText.AppendLine(" *注意,以下文件是自动生成的,任何手动修改都会被下次生成覆盖,若手动修改后,尽量避免自动生成");
scriptText.AppendLine(" *-----------------------------------*/");
scriptText.AppendLine("using UnityEngine.UI;");
scriptText.AppendLine("using UnityEngine;");
scriptText.AppendLine();

//生成命名空间
if (!string.IsNullOrEmpty(nameSpaceName))
{
scriptText.AppendLine($"namespace {nameSpaceName}");
scriptText.AppendLine("{");
}
scriptText.AppendLine($" public class {name + "UIComponent"}");
scriptText.AppendLine(" {");

//根据字段数据列表 声明字段
foreach (var item in objDataList)
{
scriptText.AppendLine(" public " + item.fieldType + " " + item.fieldName + item.fieldType + ";\r\n"); //写\r\n是因为万恶的CRLF
}

//声明初始化组件接口
scriptText.AppendLine(" public void InitComponent(WindowBase target)");
scriptText.AppendLine(" {");
scriptText.AppendLine(" //组件查找");
//根据查找路径字典 和字段数据列表生成组件查找代码;
foreach (var item in objFindPathDic)
{
EditorObjectData itemData = GetEditorObjectData(item.Key);
string relFieldName = itemData.fieldName + itemData.fieldType;
if (string.Equals("GameObject", itemData.fieldType))
{
scriptText.AppendLine($" {relFieldName} = target.transform.Find(\"{item.Value}\").gameObject;");
}
else if (string.Equals("Transform", itemData.fieldType))
{
scriptText.AppendLine($" {relFieldName} = target.transform.Find(\"{item.Value}\").transform;");
}
else
{
scriptText.AppendLine($" {relFieldName} = target.transform.Find(\"{item.Value}\").GetComponent<{itemData.fieldType}>();");
}
}
scriptText.AppendLine(" ");
scriptText.AppendLine(" ");
scriptText.AppendLine(" //组件事件绑定");
//得到逻辑类 WindowBase 强转为目标派生类
scriptText.AppendLine($" {name} mWindow = ({name})target;");

//生成UI事件绑定代码
foreach (var item in objDataList)
{
string type = item.fieldType;
string methodName = item.fieldName;
string suffix;
if (type.Contains("Button"))
{
suffix = "Click";
scriptText.AppendLine($" target.AddButtonClickListener({methodName}{type}, mWindow.On{methodName}Button{suffix});");
}
if (type.Contains("InputField"))
{
scriptText.AppendLine($" target.AddInputFieldListener({methodName}{type}, mWindow.On{methodName}InputChange, mWindow.On{methodName}InputEnd);");
}
if (type.Contains("Toggle"))
{
suffix = "Change";
scriptText.AppendLine($" target.AddToggleClickListener({methodName}{type}, mWindow.On{methodName}Toggle{suffix});");
}
}
scriptText.AppendLine(" }");
scriptText.AppendLine(" }");
if (!string.IsNullOrEmpty(nameSpaceName))
{
scriptText.AppendLine("}");
}
return scriptText.ToString();
}

public static EditorObjectData GetEditorObjectData(int instanceID)
{
foreach (var item in objDataList)
{
if (item.instanceID == instanceID)
{
return item;
}
}
return null;
}
}

public class EditorObjectData
{
public int instanceID;
public string fieldName;
public string fieldType;
}

三、组件数据绑定脚本的生成

组件数据脚本同样是负责获取并管理UI控件,供Window逻辑类调用UI控件,初始化方法里将Window逻辑类里监听函数添加到控件中

组件数据脚本代码需要生成如下部分:

  • 脚本为自动生成的提示,命名空间的引用
  • 命名空间与数据查找类的声明
  • 数据查找类要包括所有的需要管理的UI控件的字段
  • 数据查找类要包括初始化方法,其中包括将对应的Window逻辑类的监听函数添加到UI控件的语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//自动化脚本生成示例

/*-----------------------------------
*Title: UI自动化组件生成代码生成工具
*Author: 铸梦
*Date:2024/1/25 14:53:08
*Description: 变量需要以[Text]括号加组件类型格式进行声明,然后右键窗口物体——一键生成UI数据组件脚本即可
*注意,以下文件是自动生成的,任何手动修改都会被下次生成覆盖,若手动修改后,尽量避免自动生成
*-----------------------------------*/
using UnityEngine.UI;
using UnityEngine;

namespace ZMUIFrameWork
{
public class TempWindow2DataComponent : MonoBehaviour
{
public Button CloseButton;

public void InitComponent(WindowBase target)
{
//组件事件绑定
TempWindow2 mWindow = (TempWindow2)target;
target.AddButtonClickListener(CloseButton, mWindow.OnCloseButtonClick);
}
}
}

组件数据脚本与组件查找脚本相比,只是少了使用Find​方法获取UI控件的语句,而前者的获取组件逻辑将在编译完成就执行,而不是运行时执行

组件数据查找脚本的生成主要在GeneratorBindComponentTool​内实现,它继承Editor
它需要为右键菜单提供“生成组件数据脚本”选项(快捷键Shift+B) ,因此需要为一个函数添加[MenuItem]​特性作为入口函数(#B​是指定快捷键)

image

对窗口对象右键选择该选项后对选中的窗口对象进行解析,主函数执行步骤如下:

1
2
3
4
5
6
7
8
9
10
11
12
//生成组件数据脚本方法,选中游戏对象后在右键菜单点击“生成组件数据脚本”调用
[MenuItem("GameObject/生成组件数据脚本(Shift+B) #B", false, 0)]
private static void CreateBindComponentScripts()
{
1.获取在编辑器里选中的内容,判断是否为游戏对象,不是则报错并直接返回空
2.设置脚本的生成路径
3.调用PresWindowNodeData或PerseWindowNodeData,将获取到的窗口传入,解析数据
4.将解析到的数据使用PlayerPrefs持久化,后续生成Window数据类脚本还会用到
5.通过解析的内容生成对应脚本字符串
6.将脚本字符串与生成脚本路径传入预览窗口显示
7.生成完成后使用EditorPrefs来持久化一个标记,使AddComponentToWindow()函数可以执行
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class GeneratorBindComponentTool : Editor
{
public static List<EditorObjectData> objDataList; //查找对象的数据

[MenuItem("GameObject/生成组件数据脚本(Shift+B) #B", false, 0)]
private static void CreateBindComponentScripts()
{
//获取在编辑器里选中的内容
GameObject obj = Selection.objects.First() as GameObject;
if (obj == null)
{
Debug.LogError("需要选择GameObject");
return;
}
objDataList = new List<EditorObjectData>();

//设置脚本的生成路径
if (!Directory.Exists(GeneratorConfig.BindComponentGeneratorPath))
{
Directory.CreateDirectory(GeneratorConfig.BindComponentGeneratorPath);
}
//解析窗口所有UI组件数据
if (GeneratorConfig.ParseType == ParseType.Tag)
ParseWindowDataByTag(obj.transform, obj.name);
else
PresWindowNodeData(obj.transform, obj.name);
//储存字段名称
string dataListJson = JsonConvert.SerializeObject(objDataList);
PlayerPrefs.SetString(GeneratorConfig.OBJDATALIST_KEY, dataListJson);
//生成C#脚本文件
string csScriptText = CreateCS(obj.name);
string csPath = GeneratorConfig.BindComponentGeneratorPath + "/" + obj.name + "DataComponent.cs";
UIWindowEditor.ShowWindow(csScriptText, csPath);
//生成完成后使用EditorPrefs来持久化一个标记,使AddComponentToWindow()函数可以执行
EditorPrefs.SetString("GeneratorClassName", obj.name + "DataComponent");
}

// 解析窗口节点数据
public static void PresWindowNodeData(Transform transform, string WindowName)...

public static void ParseWindowDataByTag(Transform transform, string WindowName)...

// 生成C#脚本字符串
public static string CreateCS(string name)...

public static EditorObjectData GetEditorObjectData(int instanceID)...

// 编译完成后Unity自动调用的方法
[UnityEditor.Callbacks.DidReloadScripts] //编译完成后,Unity会自动执行该函数的特性
public static void AddComponentToWindow()...
}

组件数据绑定脚本由于只是在脚本关联控件的方式有所不同,因此生成逻辑与组件数据查找脚本的生成逻辑高度相似,主要的区别在于:

  • 组件数据绑定脚本不需要解析UI控件对象的查找路径
  • 组件数据绑定脚本在生成脚本字符串完毕后会持久化一个标记,让自动挂载与关联方法在编译后生效
  • 在生成脚本字符串并编译脚本完毕后,会获取场景上的窗口对象,将脚本挂载到上面,同时利用反射,将UI控件关联到脚本的各个字段上

脚本自动挂载与关联

组件数据脚本的逻辑大体与上面查找脚本一致,核心区别在于控制在编译完成后就自动完成挂载与关联

当主函数将窗口解析完毕后,主函数会对在EditorPrefs​持久化一个标记,使AddComponentToWindow()​生效
AddComponentToWindow()​利用[UnityEditor.Callbacks.DidReloadScripts]​特性在每次编译后都会执行
当发现存在标记时,就会执行将生成的脚本挂载到窗口对象和将窗口的各个UI控件自动关联到脚本上的逻辑

这里使用了反射来获取生成的组件数据类,同时也使用反射来获取类的各个字段,将UI控件对象关联到挂载在窗口对象上的组件数据类对象

代码的思路如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 编译完成后Unity自动调用的方法
[UnityEditor.Callbacks.DidReloadScripts] //编译完成后,Unity会自动执行该函数的特性
public static void AddComponentToWindow()
{
if 不存在由CreateBindComponentScripts()方法中持久化的标记
return
读取标记,从标记中获取本次需要自动挂载脚本与管理组件的窗口名,进而获取组件数据类名(名字一般为“窗口名+DataComponent”)
通过"ZMUIFrameWork.组件数据类名",从"Assembly-CSharp"程序集内获取组件数据类类型
if 组件数据类类型 == null
报错
return
通过之前得到的窗口名,查找得到场景上的UI窗口对象
if UI窗口对象 == null
报错
return
根据之前得到的组件数据类类型,获取UI窗口对象上的组件数据脚本
如果组件数据脚本为空,根据之前得到的组件数据类类型,向UI窗口对象添加该脚本
读取之前由CreateBindComponentScripts()使用通过PlayerPrefs持久化的窗口解析数据
for 遍历通过反射组件数据类类型得到的所有字段
for 遍历解析得到的所有UI控件数据
if 字段名 == UI控件名+类型名
根据解析数据中UI控件的InstanceID获取场景上的这个UI控件对象
if 类型名 == GameObject
根据遍历到的字段,通过反射,将UI控件对象关联到组件数据类字段上
else
从UI控件对象获取控件脚本(例如Button脚本)
根据遍历到的字段,通过反射,将控件脚本关联到组件数据类字段上
处理完成后删除标记,防止重复执行这里的方法
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/// <summary>
/// 编译完成后Unity自动调用的方法
/// </summary>
[UnityEditor.Callbacks.DidReloadScripts] //编译完成后,Unity会自动执行该函数的特性
public static void AddComponentToWindow()
{
//如果当前生成的不是数据脚本的回调,就不处理
string className = EditorPrefs.GetString("GeneratorClassName");
if (string.IsNullOrEmpty(className))
{
return;
}
//通过反射的方式,从程序集中找到这个脚本,把它挂载到当前的物体上
//获取所有的程序集,找到CSharp程序集,再获取类所在的程序集路径
var assembiles = AppDomain.CurrentDomain.GetAssemblies();
var cSharpAssembly = assembiles.First(assembly => assembly.GetName().Name == "Assembly-CSharp");
string relClassName = "ZMUIFrameWork." + className;
Type type = cSharpAssembly.GetType(relClassName);
//若为空,就直接返回
if (type == null)
{
Debug.LogError($"{relClassName}: 未能在{cSharpAssembly}内找到该类,请检查是否存在该类!");
return;
}
//获取要挂载的那个物体
string windowObjName = className.Replace("DataComponent", "");
GameObject windowObj = GameObject.Find(windowObjName);
if (windowObj == null)
{
windowObj = GameObject.Find("UIRoot/" + windowObjName);
if (windowObj == null)
{
Debug.LogWarning($"{className}: 未在场景上找到需要绑定UI控件的Window,绑定操作未完成");
return;
}
}
//先获取现窗口上有没有挂载该数据组件,如果没有挂载再进行挂载
Component component = windowObj.GetComponent(type);
if (component == null)
{
component = windowObj.AddComponent(type);
}

//通过反射的方式,遍历数据列表,找到对应的字段并赋值
//获取对象数据列表
string dataListJson = PlayerPrefs.GetString(GeneratorConfig.OBJDATALIST_KEY);
List<EditorObjectData> objDataList = JsonConvert.DeserializeObject<List<EditorObjectData>>(dataListJson);
//获取脚本所有字段
FieldInfo[] fieldInfoList = type.GetFields();
foreach (var fieldInfo in fieldInfoList)
{
foreach (var objData in objDataList)
{
if (fieldInfo.Name == objData.fieldName + objData.fieldType)
{
//根据InstanceID找到对应的对象
GameObject uiObject = EditorUtility.InstanceIDToObject(objData.instanceID) as GameObject;
if (uiObject == null)
{
Debug.LogError($"[{objData.fieldType}]{objData.fieldName}: 未能通过生成组件数据脚本时保存的InstanceID找到此UI控件,可能需要重新执行生成组件数据脚本操作!");
continue;
}
//设置该字段所对应的对象
if (string.Equals(objData.fieldType, "GameObject"))
{
fieldInfo.SetValue(component, uiObject);
}
else
{
fieldInfo.SetValue(component, uiObject.GetComponent(objData.fieldType));
}
break;
}
}
}
//处理完成后删除标记,防止后续的非生成数据脚本的回调触发执行该函数
EditorPrefs.DeleteKey("GeneratorClassName");
}

GeneratorBindComponentTool代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEditor;
using UnityEngine;

public class GeneratorBindComponentTool : Editor
{
public static List<EditorObjectData> objDataList; //查找对象的数据

[MenuItem("GameObject/生成组件数据脚本(Shift+B) #B", false, 0)]
private static void CreateFindComponentScripts()
{
//获取在编辑器里选中的内容
GameObject obj = Selection.objects.First() as GameObject;
if (obj == null)
{
Debug.LogError("需要选择GameObject");
return;
}
objDataList = new List<EditorObjectData>();

//设置脚本的生成路径
if (!Directory.Exists(GeneratorConfig.BindComponentGeneratorPath))
{
Directory.CreateDirectory(GeneratorConfig.BindComponentGeneratorPath);
}
//解析窗口所有UI组件数据
if (GeneratorConfig.ParseType == ParseType.Tag)
ParseWindowDataByTag(obj.transform, obj.name);
else
PresWindowNodeData(obj.transform, obj.name);
//储存字段名称
string dataListJson = JsonConvert.SerializeObject(objDataList);
PlayerPrefs.SetString(GeneratorConfig.OBJDATALIST_KEY, dataListJson);
//生成C#脚本文件
string csScriptText = CreateCS(obj.name);
string csPath = GeneratorConfig.BindComponentGeneratorPath + "/" + obj.name + "DataComponent.cs";
UIWindowEditor.ShowWindow(csScriptText, csPath);
//生成完成后使用EditorPrefs来持久化一个标记,使AddComponentToWindow()函数可以执行
EditorPrefs.SetString("GeneratorClassName", obj.name + "DataComponent");
}

/// <summary>
/// 解析窗口节点数据
/// </summary>
/// <param name="transform">要解析的窗口的Transform</param>
/// <param name="WindowName">窗口名</param>
public static void PresWindowNodeData(Transform transform, string WindowName)
{
//当传入的对象下已无子对象,这里的for循环不执行
for (int i = 0; i < transform.childCount; i++)
{
GameObject obj = transform.GetChild(i).gameObject;
string name = obj.name;
//如果对象名包括"[类型名]",说明该对象需要作为UI组件进行解析
if (name.Contains("[") && name.Contains("]"))
{
int index = name.IndexOf("]") + 1; //得到"]"后第一个字符的索引
string fieldName = name.Substring(index, name.Length - index); //获取字段名字
string fieldType = name.Substring(1, index - 2); //获取字段类型

objDataList.Add(new EditorObjectData { fieldName = fieldName, fieldType = fieldType, instanceID = obj.GetInstanceID() });
}
PresWindowNodeData(transform.GetChild(i), WindowName);
}
}

public static void ParseWindowDataByTag(Transform transform, string WindowName)
{
//当传入的对象下已无子对象,这里的for循环不执行
for (int i = 0; i < transform.childCount; i++)
{
GameObject obj = transform.GetChild(i).gameObject;
string tagName = obj.tag;
//如果对象名包括"[类型名]",说明该对象需要作为UI组件进行解析
if (GeneratorConfig.TAGArr.Contains(tagName))
{
string fieldName = obj.name; //获取字段名字
string fieldType = tagName; //获取字段类型
objDataList.Add(new EditorObjectData { fieldName = fieldName, fieldType = fieldType, instanceID = obj.GetInstanceID() });
}
ParseWindowDataByTag(transform.GetChild(i), WindowName);
}
}

/// <summary>
/// 生成C#脚本字符串
/// </summary>
/// <param name="name">窗口名</param>
/// <returns>脚本字符串</returns>
public static string CreateCS(string name)
{
StringBuilder scriptText = new StringBuilder();
string nameSpaceName = "ZMUIFrameWork";
//添加引用
scriptText.AppendLine("/*-----------------------------------");
scriptText.AppendLine(" *Title: UI自动化组件生成代码生成工具");
scriptText.AppendLine(" *Author: 铸梦");
scriptText.AppendLine(" *Date:" + System.DateTime.Now);
scriptText.AppendLine(" *Description: 变量需要以[Text]括号加组件类型格式进行声明,然后右键窗口物体——一键生成UI数据组件脚本即可");
scriptText.AppendLine(" *注意,以下文件是自动生成的,任何手动修改都会被下次生成覆盖,若手动修改后,尽量避免自动生成");
scriptText.AppendLine(" *-----------------------------------*/");
scriptText.AppendLine("using UnityEngine.UI;");
scriptText.AppendLine("using UnityEngine;");
scriptText.AppendLine();

//生成命名空间
if (!string.IsNullOrEmpty(nameSpaceName))
{
scriptText.AppendLine($"namespace {nameSpaceName}");
scriptText.AppendLine("{");
}
scriptText.AppendLine($" public class {name + "DataComponent : MonoBehaviour"}");
scriptText.AppendLine(" {");

//根据字段数据列表 声明字段
foreach (var item in objDataList)
{
scriptText.AppendLine(" public " + item.fieldType + " " + item.fieldName + item.fieldType + ";\r\n"); //写\r\n是因为万恶的CRLF
}

//声明初始化组件接口
scriptText.AppendLine(" public void InitComponent(WindowBase target)");
scriptText.AppendLine(" {");

scriptText.AppendLine(" //组件事件绑定");
//得到逻辑类 WindowBase 强转为目标派生类
scriptText.AppendLine($" {name} mWindow = ({name})target;");

//生成UI事件绑定代码
foreach (var item in objDataList)
{
string type = item.fieldType;
string methodName = item.fieldName;
string suffix;
if (type.Contains("Button"))
{
suffix = "Click";
scriptText.AppendLine($" target.AddButtonClickListener({methodName}{type}, mWindow.On{methodName}Button{suffix});");
}
if (type.Contains("InputField"))
{
scriptText.AppendLine($" target.AddInputFieldListener({methodName}{type}, mWindow.On{methodName}InputChange, mWindow.On{methodName}InputEnd);");
}
if (type.Contains("Toggle"))
{
suffix = "Change";
scriptText.AppendLine($" target.AddToggleClickListener({methodName}{type}, mWindow.On{methodName}Toggle{suffix});");
}
}
scriptText.AppendLine(" }");
scriptText.AppendLine(" }");
if (!string.IsNullOrEmpty(nameSpaceName))
{
scriptText.AppendLine("}");
}
return scriptText.ToString();
}

public static EditorObjectData GetEditorObjectData(int instanceID)
{
foreach (var item in objDataList)
{
if (item.instanceID == instanceID)
{
return item;
}
}
return null;
}

/// <summary>
/// 编译完成后Unity自动调用的方法
/// </summary>
[UnityEditor.Callbacks.DidReloadScripts] //编译完成后,Unity会自动执行该函数的特性
public static void AddComponentToWindow()
{
//如果当前生成的不是数据脚本的回调,就不处理
string className = EditorPrefs.GetString("GeneratorClassName");
if (string.IsNullOrEmpty(className))
{
return;
}
//通过反射的方式,从程序集中找到这个脚本,把它挂载到当前的物体上
//获取所有的程序集,找到CSharp程序集,再获取类所在的程序集路径
var assembiles = AppDomain.CurrentDomain.GetAssemblies();
var cSharpAssembly = assembiles.First(assembly => assembly.GetName().Name == "Assembly-CSharp");
string relClassName = "ZMUIFrameWork." + className;
Type type = cSharpAssembly.GetType(relClassName);
//若为空,就直接返回
if (type == null)
{
Debug.LogError($"{relClassName}: 未能在{cSharpAssembly}内找到该类,请检查是否存在该类!");
return;
}
//获取要挂载的那个物体
string windowObjName = className.Replace("DataComponent", "");
GameObject windowObj = GameObject.Find(windowObjName);
if (windowObj == null)
{
windowObj = GameObject.Find("UIRoot/" + windowObjName);
if (windowObj == null)
{
Debug.LogWarning($"{className}: 未在场景上找到需要绑定UI控件的Window,绑定操作未完成");
return;
}
}
//先获取现窗口上有没有挂载该数据组件,如果没有挂载再进行挂载
Component component = windowObj.GetComponent(type);
if (component == null)
{
component = windowObj.AddComponent(type);
}

//通过反射的方式,遍历数据列表,找到对应的字段并赋值
//获取对象数据列表
string dataListJson = PlayerPrefs.GetString(GeneratorConfig.OBJDATALIST_KEY);
List<EditorObjectData> objDataList = JsonConvert.DeserializeObject<List<EditorObjectData>>(dataListJson);
//获取脚本所有字段
FieldInfo[] fieldInfoList = type.GetFields();
foreach (var fieldInfo in fieldInfoList)
{
foreach (var objData in objDataList)
{
if (fieldInfo.Name == objData.fieldName + objData.fieldType)
{
//根据InstanceID找到对应的对象
GameObject uiObject = EditorUtility.InstanceIDToObject(objData.instanceID) as GameObject;
if (uiObject == null)
{
Debug.LogError($"[{objData.fieldType}]{objData.fieldName}: 未能通过生成组件数据脚本时保存的InstanceID找到此UI控件,可能需要重新执行生成组件数据脚本操作!");
continue;
}
//设置该字段所对应的对象
if (string.Equals(objData.fieldType, "GameObject"))
{
fieldInfo.SetValue(component, uiObject);
}
else
{
fieldInfo.SetValue(component, uiObject.GetComponent(objData.fieldType));
}
break;
}
}
}
//处理完成后删除标记,防止后续的非生成数据脚本的回调触发执行该函数
EditorPrefs.DeleteKey("GeneratorClassName");
}
}

四、Window逻辑类的生成

Window逻辑类是我们编写UI窗口交互逻辑的脚本,自动化系统会帮我们创建脚本并自动生成模板代码,我们在模板里编写逻辑即可

组件数据脚本代码需要生成如下部分:

  • 脚本为自动生成的提示,命名空间的引用
  • Window逻辑类的声明,继承WindowBase
  • 声明组件查找类/组件数据类的字段,以便调用UI控件,使用组件查找类还是组件数据类由配置文件决定
  • 重写WindowBase​的生命周期函数,在OnAwake​内执行部分初始化代码,例如获取组件查找类/组件数据类并执行其初始化方法
  • 预留API Function​代码块,用于开发者在其中对外调用接口
  • 获取解析窗口数据,声明所有解析到的UI控件的监听函数,组件查找类/组件数据类会在初始化方法将所有监听函数添加到UI控件里
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//自动化脚本生成示例

/*-----------------------------------
*Title: UI自动化组件查找代码工具
*Author: 铸梦
*Date:2024/1/23 15:17:22
*Description: UI 表现层,该层只负责界面的交互,表现相关的更新,不允许编写任何业务逻辑代码!
*注意,以下文件是自动生成的,再次生成不会覆盖原有的代码,会在原有的代码上新增,可放心使用
*-----------------------------------*/
using UnityEngine.UI;
using UnityEngine;
using ZMUIFrameWork;

public class HallWindow : WindowBase
{
public HallWindowDataComponent dataComponent;

#region 声明周期函数
//调用机制与Mono Awake一致
public override void OnAwake()
{
dataComponent = gameObject.GetComponent<HallWindowDataComponent>();
dataComponent.InitComponent(this);
base.OnAwake();
}
//物体显示时执行
public override void OnShow()
{
base.OnShow();
}
//物体隐藏时执行
public override void OnHide()
{
base.OnHide();
}
//物体销毁时时执行
public override void OnDestroy()
{
base.OnDestroy();
}
#endregion

#region API Function

#endregion

#region UI组件事件
public void OnChatButtonClick()
{

}
public void OnSettingButtonClick()
{

}
public void OnUserInfoButtonClick()
{

}
public void OnFriendButtonClick()
{

}
#endregion

}

组件数据查找脚本的生成主要在GeneratorWindowTool​内实现,它继承Editor
它需要为右键菜单提供“生成Window逻辑类”选项(快捷键Shift+V) ,因此需要为一个函数添加[MenuItem]​特性作为入口函数(#V​是指定快捷键)

image

对窗口对象右键选择该选项后对选中的窗口对象生成对应的类脚本,主函数执行步骤如下:

1
2
3
4
5
6
7
8
[MenuItem("GameObject/生成Window逻辑脚本(Shift+V) #V", false, 0)]
private static void CreateFindComponentScripts()
{
1.获取在编辑器里选中的内容,判断是否为游戏对象,不是则报错并直接返回空
2.设置脚本的生成路径
3.生成对应脚本字符串,期间会获取之前解析的数据,制作方法字符串字典
4.将脚本字符串与生成脚本路径和方法字典传入预览窗口显示
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using Newtonsoft.Json;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;

public class GeneratorWindowTool : Editor
{
static Dictionary<string, string> methodDic = new Dictionary<string, string>();

[MenuItem("GameObject/生成Window逻辑脚本(Shift+V) #V", false, 0)]
private static void CreateFindComponentScripts()
{
//获取在编辑器里选中的内容
GameObject obj = Selection.objects.First() as GameObject;
if (obj == null)
{
Debug.LogError("需要选择GameObject");
return;
}

//设置脚本的生成路径
if (!Directory.Exists(GeneratorConfig.FindComponentGeneratorPath))
{
Directory.CreateDirectory(GeneratorConfig.FindComponentGeneratorPath);
}

//生成C#脚本文件
string csScriptText = CreateWindowCS(obj.name);
string csPath = GeneratorConfig.WindowGeneratorPath + "/" + obj.name + ".cs";
UIWindowEditor.ShowWindow(csScriptText, csPath, methodDic);
}

// 生成Window脚本
public static string CreateWindowCS(string name)...

// 生成UI事件方法字典
public static void CreateMethod(StringBuilder scriptText, ref Dictionary<string, string> methodDic, string methodName, string param = "")...
}

生成脚本字符串

按照上面给出的组件查找脚本文本格式,从PlayerPrefs读取持久化的解析结果,生成脚本字符串,之后即可将字符串传入到预览窗口内供预览

值得一提的是,与组件数据脚本/组件查找脚本的生成不同,这里生成的脚本字符串有多个
其中一个还是按照原本要求的格式生成的脚本字符串,包含所有的内容
而根据解析结果生成的监听函数的声明语句还会生成在另外的多个字符串内,一个监听函数生成一个字符串

监听函数声明字符串会以方法名("On+控件名+后缀(如Click)"​)为键,存入到字典内,随固定内容字符串一起传入到预览窗口,等待后面的处理

这样做是为了达到UI窗口添加组件而重新生成Window逻辑类脚本时,不覆盖原有开发者编写的语句和代码递增生成的效果
原理在后面的预览窗口部分解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
/// <summary>
/// 生成Window脚本
/// </summary>
/// <param name="name">窗口名</param>
/// <returns>脚本字符串</returns>
public static string CreateWindowCS(string name)
{
//反序列化并读取字段名称
string dataListJson = PlayerPrefs.GetString(GeneratorConfig.OBJDATALIST_KEY);
List<EditorObjectData> objDataList = JsonConvert.DeserializeObject<List<EditorObjectData>>(dataListJson);
methodDic.Clear();
StringBuilder scriptText = new StringBuilder();

//添加引用
scriptText.AppendLine("/*-----------------------------------");
scriptText.AppendLine(" *Title: UI自动化组件查找代码工具");
scriptText.AppendLine(" *Author: 铸梦");
scriptText.AppendLine(" *Date:" + System.DateTime.Now);
scriptText.AppendLine(" *Description: UI 表现层,该层只负责界面的交互,表现相关的更新,不允许编写任何业务逻辑代码!");
scriptText.AppendLine(" *注意,以下文件是自动生成的,再次生成不会覆盖原有的代码,会在原有的代码上新增,可放心使用");
scriptText.AppendLine(" *-----------------------------------*/");
scriptText.AppendLine("using UnityEngine.UI;");
scriptText.AppendLine("using UnityEngine;");
scriptText.AppendLine("using ZMUIFrameWork;");
scriptText.AppendLine();

//生成类名
scriptText.AppendLine($"public class {name} : WindowBase");
scriptText.AppendLine("{");

//根据生成组件类型的不同生成不同的字段
if (GeneratorConfig.GeneratorType == GeneratorType.Bind)
{
scriptText.AppendLine($" public {name}DataComponent dataComponent;");
}
else
{
scriptText.AppendLine($" public {name}UIComponent uiComponent = new {name}UIComponent();");
}

//生成声明周期函数
//Awake
scriptText.AppendLine(" ");
scriptText.AppendLine(" #region 声明周期函数");
scriptText.AppendLine(" //调用机制与Mono Awake一致");
scriptText.AppendLine(" public override void OnAwake()");
scriptText.AppendLine(" {");
if (GeneratorConfig.GeneratorType == GeneratorType.Bind)
{
scriptText.AppendLine($" dataComponent = gameObject.GetComponent<{name}DataComponent>();");
scriptText.AppendLine(" dataComponent.InitComponent(this);");
}
else
scriptText.AppendLine(" uiComponent.InitComponent(this);");
scriptText.AppendLine(" base.OnAwake();");
scriptText.AppendLine(" }");
//OnShow
scriptText.AppendLine(" //物体显示时执行");
scriptText.AppendLine(" public override void OnShow()");
scriptText.AppendLine(" {");
scriptText.AppendLine(" base.OnShow();");
scriptText.AppendLine(" }");
//OnHide
scriptText.AppendLine(" //物体隐藏时执行");
scriptText.AppendLine(" public override void OnHide()");
scriptText.AppendLine(" {");
scriptText.AppendLine(" base.OnHide();");
scriptText.AppendLine(" }");
//OnDestroy
scriptText.AppendLine(" //物体销毁时时执行");
scriptText.AppendLine(" public override void OnDestroy()");
scriptText.AppendLine(" {");
scriptText.AppendLine(" base.OnDestroy();");
scriptText.AppendLine(" }");
scriptText.AppendLine(" #endregion");
scriptText.AppendLine(" ");

//API Function
scriptText.AppendLine(" #region API Function");
scriptText.AppendLine(" ");
scriptText.AppendLine(" #endregion");
scriptText.AppendLine(" ");

//UI组件事件生成
scriptText.AppendLine(" #region UI组件事件");
foreach (var item in objDataList)
{
string type = item.fieldType;
string methodName = "On" + item.fieldName;
string suffix;
if (type.Contains("Button"))
{
suffix = "ButtonClick";
CreateMethod(scriptText, ref methodDic, methodName + suffix);
}
else if (type.Contains("InputField"))
{
suffix = "InputChange";
CreateMethod(scriptText, ref methodDic, methodName + suffix, "string text");
suffix = "InputEnd";
CreateMethod(scriptText, ref methodDic, methodName + suffix, "string text");
}
else if (type.Contains("Toggle"))
{
suffix = "ToggleChange";
CreateMethod(scriptText, ref methodDic, methodName + suffix, "bool state, Toggle toggle");
}
}
scriptText.AppendLine(" #endregion");
scriptText.AppendLine(" ");

scriptText.AppendLine("}");
return scriptText.ToString();
}

/// <summary>
/// 生成UI事件方法
/// </summary>
/// <param name="scriptText"></param>
/// <param name="methodDic"></param>
/// <param name="methodName"></param>
/// <param name="param"></param>
public static void CreateMethod(StringBuilder scriptText, ref Dictionary<string, string> methodDic, string methodName, string par
{
//声明UI组件事件
scriptText.AppendLine($" public void {methodName}({param})");
scriptText.AppendLine(" {");
//如果是关闭按钮,则自动添加一个HideWindow();语句
if (methodName == "OnCloseButtonClick")
{
scriptText.AppendLine(" HideWindow();");
}
else
{
scriptText.AppendLine();
}
scriptText.AppendLine(" }");
//存储UI组件事件 提供给后续新增代码使用
StringBuilder builder = new StringBuilder();
builder.AppendLine($" public void {methodName}({param})");
builder.AppendLine(" {");
builder.AppendLine(" ");
builder.AppendLine(" }");
methodDic.Add(methodName, builder.ToString());
}

五、代码预览窗口

在生成组件查找脚本/组件数据脚本或Window逻辑类脚本字符串后
我们可以暂时先将文本字符串输出到一个预览窗口供我们检查,确认文本内容无误后将文本生成脚本文件存储到工程内

image

代码预览窗口主要在UIWindowEditor​内实现,它继承EditorWindow​(因为需要创建编辑器窗口)
每当GeneratorFindComponentTool​、GeneratorBindComponentTool​及GeneratorWindowTool​生成字符串后
都会调用UIWindowEditor​的ShowWindow​方法,显示代码预览窗口,点击确认后,才会将文本写入文件并存储

EditorWindow 类是用于创建独立窗口的基类

之前GeneratorWindowTool​生成的多个字符串也要在这里进行最终处理,不覆盖原有开发者编写的语句和代码递增生成的效果也要在这里实现

1.Window逻辑类递增代码处理

GeneratorFindComponentTool​、GeneratorBindComponentTool​及GeneratorWindowTool​生成字符串后,
需要调用ShowWindow​方法传入字符串和文件路径,以便窗口输出文本与路径

GeneratorWindowTool​会传入第三个参数insertDic​,传入的是之前生成Window逻辑类生成出来的监听函数字符串
这意味着需要确认是否已存在文件,如果已经存在Window逻辑类脚本,那么只要将原来脚本中不存在的监听方法插入进去即可
若不存在文件或者是GeneratorFindComponentTool​、GeneratorBindComponentTool​传入的字符串,就把参数content​的字符串输出出去

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void ShowWindow(string content, string filePath, Dictionary<string, string> insertDic = null)
{
在指定位置创建窗口,窗口内容由OnGUI决定
窗口显示的字符串 = content
窗口显示的文件路径 = filePath
if filePath存在这个脚本文件 && insertDic != null //意味着传入的是Window逻辑类字符串,需要做进一步处理
通过filePath获取原始代码
for 遍历insertDic的方法名与监听函数字符串
if 原始代码不存在遍历到的insertDic的方法名
获取原始代码中"UI组件事件"与第一个"public"之间的索引
在索引处插入新增的监听函数字符串
显示窗口
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/// <summary>
/// 显示代码预览界面
/// </summary>
/// <param name="content">将要生成的代码内容</param>
/// <param name="filePath">脚本文件生成路径</param>
/// <param name="insertDic">插入方法代码字典</param>
public static void ShowWindow(string content, string filePath, Dictionary<string, string> insertDic = null)
{
//创建代码展示窗口
UIWindowEditor window = (UIWindowEditor)GetWindowWithRect(typeof(UIWindowEditor), new Rect(100, 50, 800, 700), true, "Window生成预览界面")
window.scriptContent = content;
window.filePath = filePath;
//处理代码新增代码
if (File.Exists(filePath) && insertDic != null)
{
//获取原始代码
string originScript = File.ReadAllText(filePath);
foreach (var item in insertDic)
{
//如果老代码中没有这个代码就进行插入操作
if (!originScript.Contains(item.Key))
{
int index = window.GetInsertIndex(originScript);
originScript = window.scriptContent = originScript.Insert(index, "\r\n" + item.Value + " ");
}
}
}
//显示窗口
window.Show();
}

/// <summary>
/// 获取插入代码的下标
/// </summary>
/// <param name="content">代码字符串</param>
/// <returns>返回UI事件组件下面的第一个public所在的位置,若不存在返回-1</returns>
public int GetInsertIndex(string content)
{
//找到UI事件组件下面的第一个public所在的位置,进行代码插入
Regex regex = new Regex("UI组件事件"); //正则表达式
Match match = regex.Match(content); //匹配结果

Regex regex1 = new Regex("public");
MatchCollection matchCollection = regex1.Matches(content); //匹配所有public的字符串

for (int i = 0; i < matchCollection.Count; i++)
{
//当public的下标正好大于UI组件事件的下标,即可插入代码
if (matchCollection[i].Index > match.Index)
{
return matchCollection[i].Index;
}
}
return -1;
}

需要注意的是,如果窗口的UI控件没有增加(不变或者删去某些UI控件),且生成过Window逻辑类并在里边编写了逻辑
再次生成Window逻辑类将会覆盖原有代码,因此如果不修改框架代码,则UI控件不增加时不要随便生成Window逻辑类,以免代码火葬场~

可以这样修改防止上述情况导致的覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void ShowWindow(string content, string filePath, Dictionary<string, string> insertDic = null)
{
//创建代码展示窗口
UIWindowEditor window = (UIWindowEditor)GetWindowWithRect(typeof(UIWindowEditor), new Rect(100, 50, 800, 700), true, "Window生成预览界面");
window.scriptContent = content;
window.filePath = filePath;
//处理代码新增代码
if (File.Exists(filePath) && insertDic != null)
{
//获取原始代码
string originScript = File.ReadAllText(filePath);
foreach (var item in insertDic)
{
//如果老代码中没有这个代码就进行插入操作
if (!originScript.Contains(item.Key))
{
int index = window.GetInsertIndex(originScript);
originScript = window.scriptContent = originScript.Insert(index, "\r\n" + item.Value + " ");
}
}
window.scriptContent = originScript; //不管循环中是否执行插入操作,先让输出的字符串等于原始代码,防止覆盖
}
//显示窗口
window.Show();
}

2.窗口绘制与确认生成按钮逻辑

预览窗口很简单,只有三个部分:代码预览框,文件路径框,确认按钮。窗口绘制语句主要是在OnGUI​内编写​​

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//绘制窗口代码
public void OnGUI()
{
//绘制ScrollView
scroll = EditorGUILayout.BeginScrollView(scroll, GUILayout.Height(600), GUILayout.Width(800));
EditorGUILayout.TextArea(scriptContent);
EditorGUILayout.EndScrollView();
EditorGUILayout.Space();
//绘制脚本生成路径
EditorGUILayout.BeginHorizontal();
EditorGUILayout.TextArea("脚本生成路径:" + filePath);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
//绘制按钮
EditorGUILayout.BeginHorizontal();
//当按下按钮时执行生成文件逻辑
if (GUILayout.Button("生成脚本", GUILayout.Height(30)))
{
//按钮事件
ButtonClick();
}
EditorGUILayout.EndHorizontal();
}

public void ButtonClick()...

确认按钮按下后,需要将生成工具类传入的字符串通过文件流写入到指定路径下的文件内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void ButtonClick()
{
if (File.Exists(filePath))
{
File.Delete(filePath); //如果已存在文件,将原文件删除
}
StreamWriter writer = File.CreateText(filePath); //开启文件流
writer.Write(scriptContent); //通过文件流将文本写入到文件内
writer.Flush();
writer.Close(); //关闭文件流
AssetDatabase.Refresh(); //刷新工程窗口
if (EditorUtility.DisplayDialog("自动化生成工具", "生成脚本成功!", "确认"))
{
//当按下弹出的提示窗口的按钮时,关闭窗口
Close();
}
}

UIWindowEditor代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;

public class UIWindowEditor : EditorWindow
{
private string scriptContent;
private string filePath;
private Vector2 scroll = new Vector2();

/// <summary>
/// 显示代码预览界面
/// </summary>
/// <param name="content">将要生成的代码内容</param>
/// <param name="filePath">脚本文件生成路径</param>
/// <param name="insertDic">插入方法代码字典</param>
public static void ShowWindow(string content, string filePath, Dictionary<string, string> insertDic = null)
{
//创建代码展示窗口
UIWindowEditor window = (UIWindowEditor)GetWindowWithRect(typeof(UIWindowEditor), new Rect(100, 50, 800, 700), true, "Window生成预览界面");
window.scriptContent = content;
window.filePath = filePath;
//处理代码新增代码
if (File.Exists(filePath) && insertDic != null)
{
//获取原始代码
string originScript = File.ReadAllText(filePath);
foreach (var item in insertDic)
{
//如果老代码中没有这个代码就进行插入操作
if (!originScript.Contains(item.Key))
{
int index = window.GetInsertIndex(originScript);
originScript = window.scriptContent = originScript.Insert(index, "\r\n" + item.Value + " ");
}
}
window.scriptContent = originScript;
}
//显示窗口
window.Show();
}

/// <summary>
/// 获取插入代码的下标
/// </summary>
/// <param name="content">代码字符串</param>
/// <returns>返回UI事件组件下面的第一个public所在的位置,若不存在返回-1</returns>
public int GetInsertIndex(string content)
{
//找到UI事件组件下面的第一个public所在的位置,进行代码插入
Regex regex = new Regex("UI组件事件"); //正则表达式
Match match = regex.Match(content); //匹配结果

Regex regex1 = new Regex("public");
MatchCollection matchCollection = regex1.Matches(content); //匹配所有public的字符串

for (int i = 0; i < matchCollection.Count; i++)
{
//当public的下标正好大于UI组件事件的下标,即可插入代码
if (matchCollection[i].Index > match.Index)
{
return matchCollection[i].Index;
}
}
return -1;
}

//绘制窗口代码
public void OnGUI()
{
//绘制ScrollView
scroll = EditorGUILayout.BeginScrollView(scroll, GUILayout.Height(600), GUILayout.Width(800));
EditorGUILayout.TextArea(scriptContent);
EditorGUILayout.EndScrollView();
EditorGUILayout.Space();
//绘制脚本生成路径
EditorGUILayout.BeginHorizontal();
EditorGUILayout.TextArea("脚本生成路径:" + filePath);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
//绘制按钮
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("生成脚本", GUILayout.Height(30)))
{
//按钮事件
ButtonClick();
}
EditorGUILayout.EndHorizontal();
}

public void ButtonClick()
{
if (File.Exists(filePath))
{
File.Delete(filePath);
}
StreamWriter writer = File.CreateText(filePath);
writer.Write(scriptContent);
writer.Flush();
writer.Close();
AssetDatabase.Refresh();
if (EditorUtility.DisplayDialog("自动化生成工具", "生成脚本成功!", "确认"))
{
Close();
}
}
}

六、动态加载不同路径下的UI预设体

在实际开发中,我们往往会将多个UI预设体文件存储在不同的文件夹内,而我们的加载方法要求只通过窗口名就可以加载预设体
因此我们需要一个动态计算预设体文件路径的手段,事先将不同预设体的文件路径计算出来,写入配置文件
这样,加载方法就可以根据窗口名,通过配置文件获取文件路径,以加载不同文件夹下的UI预设体

这里使用继承ScriptableObject​​的脚本配置文件,实现思路是

  1. 在脚本内声明windowRootArr​,写好Resources文件夹下哪些是用来存放窗口预制体的文件夹的路径
  2. 声明WindowData​类,包括窗口名和窗口预制体路径
  3. 在脚本内声明WindowData​类的列表windowDataList​,让窗口预制体路径可以通过窗口名获取
  4. 声明GetWindowPath​方法,让UIMoudle​的加载方法可以通过该方法获取窗口路径
  5. 声明GeneratorWindowConfig​方法,发现windowRootArr​中的文件夹下的预制体数量发生改变时就查找所有UI预制体加载路径
  6. 查找所有UI预制体加载路径是通过将windowRootArr​中的所有文件夹的.prefab文件的路径都记载下来存储到windowDataList​内
  7. 在编辑器环境下,让UIMoudle​每次执行初始化方法时就执行GeneratorWindowConfig​方法,以确保配置文件的路径是正确的

UIModule​下调用配置文件的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//UIModule

//初始化方法
public void Initialize()
{
//获取场景上的UI摄像机和UI根节点
mUICamera = GameObject.Find("UICamera").GetComponent<Camera>();
mUIRoot = GameObject.Find("UIRoot").transform;
mWindowConfig = Resources.Load<WindowConfig>("WindowConfig");
//打包出去后不会触发调用
#if UNITY_EDITOR
mWindowConfig.GeneratorWindowConfig();
#endif
}


//TODO... 临时资源加载,这里仅仅是因为教程方便而使用Resources加载,如有自己的资源加载框架,或后续学习使用加载框架,此处代码将会修改!
public GameObject TempLoadWindow(string windowName)
{
GameObject window = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>(mWindowConfig.GetWindowPath(windowName)), mUIRoot);
//window.transform.SetParent(mUIRoot);
window.transform.localScale = Vector3.one;
window.transform.localPosition = Vector3.zero;
window.transform.rotation = Quaternion.identity;
window.name = windowName;
return window;
}

WindowConfig代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

[System.Serializable]
public class WindowData
{
public string name;
public string path;
}

[CreateAssetMenu(fileName = "WindowConfig", menuName = "WindowConfig", order = 0)]
public class WindowConfig : ScriptableObject
{
//确认号
private string[] windowRootArr = new string[] { "Game", "Hall", "Window" } ;
public List<WindowData> windowDataList = new List<WindowData>();

//生成窗口预制体路径配置文件方法
public void GeneratorWindowConfig()
{
//检测预制体有没有新增,如果没有就不需要生成配置
int count = 0;
foreach (var item in windowRootArr)
{
string[] filePathArr = Directory.GetFiles(Application.dataPath + "/ZMUIFrameWork/Resources/" + item, "*.prefab", SearchOption.AllDirectories);
foreach (string path in filePathArr)
{
//过滤.meta文件
if (path.EndsWith(".meta"))
{
continue;
}
count += 1;
}
}
if (count == windowDataList.Count)
{
Debug.Log("预制体个数没有发生改变,不生成窗口配置");
return;
}

windowDataList.Clear();
foreach (var item in windowRootArr)
{
//获取预制体文件夹读取路径
string folder = Application.dataPath + "/ZMUIFrameWork/Resources/" + item;
//获取文件夹下的所有Prefab文件
string[] filePathArr = Directory.GetFiles(folder, "*.prefab", SearchOption.AllDirectories);
foreach (string path in filePathArr)
{
//过滤.meta文件
if (path.EndsWith(".meta"))
{
continue;
}
//获取预制体名字
string fileName = Path.GetFileNameWithoutExtension(path);
//计算文件读取路径
string filePath = item + "/" + fileName;
WindowData windowData = new WindowData { name = fileName, path = filePath };
windowDataList.Add(windowData);
}
}
}

//获取窗口预制体路径
public string GetWindowPath(string windowName)
{
foreach (var windowData in windowDataList)
{
if (string.Equals(windowData.name, windowName))
{
return windowData.path;
}
}
Debug.LogError($"{windowName}: 此Window预制体路径不存在于配置文件中,请检查预制体存放位置,或检查配置文件是否包括存放Window预制体的文件夹路径!");
return "";
}
}